Fix phone unique constraints (#20261)
## Summary Closes #20195 Fix phone field unique constraints so phone numbers are considered unique by both `primaryPhoneNumber` and `primaryPhoneCallingCode`. - Include `primaryPhoneCallingCode` in the shared phone composite unique constraint metadata - Align the frontend settings composite field config with the backend metadata - Return all included unique composite subfields when building create-many conflict fields - Match composite unique conflict fields as a group during create-many upserts ## Root Cause Phone composite metadata only marked `primaryPhoneNumber` as part of the unique constraint. That made different international phone numbers with the same national number conflict, for example `+1 123456789` and `+32 123456789`. ## Test Plan - `yarn workspace twenty-shared build` - `jest --runTestsByPath <index action handler and create-many utility specs>` - `prettier --check <touched files>` - `oxlint --type-aware <touched files>` - `nx run twenty-shared:typecheck` - `nx run twenty-server:typecheck` - `nx run twenty-front:typecheck` --------- Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
import { nullifyEmptyActorDefaultValue } from '../nullify-empty-actor-default-value.util';
|
||||
|
||||
describe('nullifyEmptyActorDefaultValue', () => {
|
||||
it('returns null when all sub-fields are null or empty-string equivalents', () => {
|
||||
expect(
|
||||
nullifyEmptyActorDefaultValue({
|
||||
source: null,
|
||||
workspaceMemberId: null,
|
||||
name: "''",
|
||||
context: null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when source has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyActorDefaultValue({
|
||||
source: 'MANUAL',
|
||||
workspaceMemberId: null,
|
||||
name: "''",
|
||||
context: null,
|
||||
}),
|
||||
).toEqual({
|
||||
source: 'MANUAL',
|
||||
workspaceMemberId: null,
|
||||
name: null,
|
||||
context: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { nullifyEmptyAddressDefaultValue } from '../nullify-empty-address-default-value.util';
|
||||
|
||||
describe('nullifyEmptyAddressDefaultValue', () => {
|
||||
it('returns null when all sub-fields are empty-string equivalents or null', () => {
|
||||
expect(
|
||||
nullifyEmptyAddressDefaultValue({
|
||||
addressStreet1: "''",
|
||||
addressStreet2: '',
|
||||
addressCity: '',
|
||||
addressState: null,
|
||||
addressCountry: null,
|
||||
addressPostcode: null,
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when addressCity has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyAddressDefaultValue({
|
||||
addressStreet1: "''",
|
||||
addressStreet2: null,
|
||||
addressCity: 'Paris',
|
||||
addressState: '',
|
||||
addressCountry: null,
|
||||
addressPostcode: null,
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
}),
|
||||
).toEqual({
|
||||
addressStreet1: null,
|
||||
addressStreet2: null,
|
||||
addressCity: 'Paris',
|
||||
addressState: null,
|
||||
addressCountry: null,
|
||||
addressPostcode: null,
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns object when only numeric coords are set', () => {
|
||||
expect(
|
||||
nullifyEmptyAddressDefaultValue({
|
||||
addressStreet1: null,
|
||||
addressStreet2: null,
|
||||
addressCity: null,
|
||||
addressState: null,
|
||||
addressCountry: null,
|
||||
addressPostcode: null,
|
||||
addressLat: 48.8566,
|
||||
addressLng: 2.3522,
|
||||
}),
|
||||
).toEqual({
|
||||
addressStreet1: null,
|
||||
addressStreet2: null,
|
||||
addressCity: null,
|
||||
addressState: null,
|
||||
addressCountry: null,
|
||||
addressPostcode: null,
|
||||
addressLat: 48.8566,
|
||||
addressLng: 2.3522,
|
||||
});
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { nullifyEmptyCurrencyDefaultValue } from '../nullify-empty-currency-default-value.util';
|
||||
|
||||
describe('nullifyEmptyCurrencyDefaultValue', () => {
|
||||
it('returns null when both sub-fields are null or empty-string equivalent', () => {
|
||||
expect(
|
||||
nullifyEmptyCurrencyDefaultValue({
|
||||
amountMicros: null,
|
||||
currencyCode: "''",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when amountMicros has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyCurrencyDefaultValue({
|
||||
amountMicros: 5000000,
|
||||
currencyCode: "''",
|
||||
}),
|
||||
).toEqual({ amountMicros: 5000000, currencyCode: null });
|
||||
});
|
||||
|
||||
it('returns normalized object when currencyCode has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyCurrencyDefaultValue({
|
||||
amountMicros: null,
|
||||
currencyCode: 'EUR',
|
||||
}),
|
||||
).toEqual({ amountMicros: null, currencyCode: 'EUR' });
|
||||
});
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { nullifyEmptyEmailsDefaultValue } from '../nullify-empty-emails-default-value.util';
|
||||
|
||||
describe('nullifyEmptyEmailsDefaultValue', () => {
|
||||
it('returns null when all sub-fields are empty-string equivalents', () => {
|
||||
expect(
|
||||
nullifyEmptyEmailsDefaultValue({
|
||||
primaryEmail: "''",
|
||||
additionalEmails: [],
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when primaryEmail has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyEmailsDefaultValue({
|
||||
primaryEmail: 'user@example.com',
|
||||
additionalEmails: [],
|
||||
}),
|
||||
).toEqual({ primaryEmail: 'user@example.com', additionalEmails: null });
|
||||
});
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { nullifyEmptyFullNameDefaultValue } from '../nullify-empty-full-name-default-value.util';
|
||||
|
||||
describe('nullifyEmptyFullNameDefaultValue', () => {
|
||||
it('returns null when both sub-fields are empty-string equivalents', () => {
|
||||
expect(
|
||||
nullifyEmptyFullNameDefaultValue({ firstName: "''", lastName: '' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when lastName has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyFullNameDefaultValue({ firstName: "''", lastName: 'Doe' }),
|
||||
).toEqual({ firstName: null, lastName: 'Doe' });
|
||||
});
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { nullifyEmptyLinksDefaultValue } from '../nullify-empty-links-default-value.util';
|
||||
|
||||
describe('nullifyEmptyLinksDefaultValue', () => {
|
||||
it('returns null when all sub-fields are empty-string equivalents', () => {
|
||||
expect(
|
||||
nullifyEmptyLinksDefaultValue({
|
||||
primaryLinkLabel: '',
|
||||
primaryLinkUrl: "''",
|
||||
secondaryLinks: null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when primaryLinkUrl has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyLinksDefaultValue({
|
||||
primaryLinkLabel: "''",
|
||||
primaryLinkUrl: 'https://twenty.com',
|
||||
secondaryLinks: null,
|
||||
}),
|
||||
).toEqual({
|
||||
primaryLinkLabel: null,
|
||||
primaryLinkUrl: 'https://twenty.com',
|
||||
secondaryLinks: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { nullifyEmptyPhonesDefaultValue } from '../nullify-empty-phones-default-value.util';
|
||||
|
||||
describe('nullifyEmptyPhonesDefaultValue', () => {
|
||||
it('returns null when all fields are null-equivalent', () => {
|
||||
expect(
|
||||
nullifyEmptyPhonesDefaultValue({
|
||||
primaryPhoneNumber: "''",
|
||||
primaryPhoneCountryCode: "''",
|
||||
primaryPhoneCallingCode: null,
|
||||
additionalPhones: null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when primaryPhoneNumber has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyPhonesDefaultValue({
|
||||
primaryPhoneNumber: '+33612345678',
|
||||
primaryPhoneCountryCode: "''",
|
||||
primaryPhoneCallingCode: '',
|
||||
additionalPhones: null,
|
||||
}),
|
||||
).toEqual({
|
||||
primaryPhoneNumber: '+33612345678',
|
||||
primaryPhoneCountryCode: null,
|
||||
primaryPhoneCallingCode: null,
|
||||
additionalPhones: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { nullifyEmptyRichTextDefaultValue } from '../nullify-empty-rich-text-default-value.util';
|
||||
|
||||
describe('nullifyEmptyRichTextDefaultValue', () => {
|
||||
it('returns null when both sub-fields are empty-string equivalents', () => {
|
||||
expect(
|
||||
nullifyEmptyRichTextDefaultValue({ blocknote: "''", markdown: '' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns normalized object when blocknote has a value', () => {
|
||||
expect(
|
||||
nullifyEmptyRichTextDefaultValue({ blocknote: '[]', markdown: "''" }),
|
||||
).toEqual({ blocknote: '[]', markdown: null });
|
||||
});
|
||||
});
|
||||
+10
-1
@@ -5,6 +5,8 @@ import { type FlatApplication } from 'src/engine/core-modules/application/types/
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { nullifyEmptyCompositeDefaultValue } from 'src/engine/metadata-modules/flat-field-metadata/utils/nullify-empty-composite-default-value.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
||||
type GetDefaultFlatFieldMetadataArgs = {
|
||||
@@ -23,6 +25,8 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
);
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const resolvedDefaultValue =
|
||||
defaultValue ?? generateDefaultValue(createFieldInput.type);
|
||||
|
||||
return {
|
||||
description: createFieldInput.description ?? null,
|
||||
@@ -42,7 +46,12 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
type: createFieldInput.type,
|
||||
universalIdentifier: createFieldInput.universalIdentifier ?? v4(),
|
||||
options: createFieldInput.options ?? null,
|
||||
defaultValue: defaultValue ?? generateDefaultValue(createFieldInput.type),
|
||||
defaultValue: isCompositeFieldMetadataType(createFieldInput.type)
|
||||
? nullifyEmptyCompositeDefaultValue({
|
||||
defaultValue: resolvedDefaultValue,
|
||||
fieldType: createFieldInput.type,
|
||||
})
|
||||
: resolvedDefaultValue,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
isUIReadOnly: createFieldInput.isUIReadOnly ?? false,
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const isNullEquivalentTextDefaultValue = (value: unknown): boolean =>
|
||||
value === "''" || value === '';
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyActorDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
source?: string | null;
|
||||
workspaceMemberId?: string | null;
|
||||
name?: string | null;
|
||||
context?: object | null;
|
||||
};
|
||||
|
||||
const source = v.source ?? null;
|
||||
const workspaceMemberId = v.workspaceMemberId ?? null;
|
||||
const name = isNullEquivalentTextDefaultValue(v.name)
|
||||
? null
|
||||
: (v.name ?? null);
|
||||
const context = v.context ?? null;
|
||||
|
||||
if (
|
||||
source === null &&
|
||||
workspaceMemberId === null &&
|
||||
name === null &&
|
||||
context === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { source, workspaceMemberId, name, context };
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyAddressDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
addressStreet1?: string | null;
|
||||
addressStreet2?: string | null;
|
||||
addressCity?: string | null;
|
||||
addressState?: string | null;
|
||||
addressCountry?: string | null;
|
||||
addressPostcode?: string | null;
|
||||
addressLat?: number | null;
|
||||
addressLng?: number | null;
|
||||
};
|
||||
|
||||
const addressStreet1 = isNullEquivalentTextDefaultValue(v.addressStreet1)
|
||||
? null
|
||||
: (v.addressStreet1 ?? null);
|
||||
const addressStreet2 = isNullEquivalentTextDefaultValue(v.addressStreet2)
|
||||
? null
|
||||
: (v.addressStreet2 ?? null);
|
||||
const addressCity = isNullEquivalentTextDefaultValue(v.addressCity)
|
||||
? null
|
||||
: (v.addressCity ?? null);
|
||||
const addressState = isNullEquivalentTextDefaultValue(v.addressState)
|
||||
? null
|
||||
: (v.addressState ?? null);
|
||||
const addressCountry = isNullEquivalentTextDefaultValue(v.addressCountry)
|
||||
? null
|
||||
: (v.addressCountry ?? null);
|
||||
const addressPostcode = isNullEquivalentTextDefaultValue(v.addressPostcode)
|
||||
? null
|
||||
: (v.addressPostcode ?? null);
|
||||
const addressLat = v.addressLat ?? null;
|
||||
const addressLng = v.addressLng ?? null;
|
||||
|
||||
if (
|
||||
addressStreet1 === null &&
|
||||
addressStreet2 === null &&
|
||||
addressCity === null &&
|
||||
addressState === null &&
|
||||
addressCountry === null &&
|
||||
addressPostcode === null &&
|
||||
addressLat === null &&
|
||||
addressLng === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
addressStreet1,
|
||||
addressStreet2,
|
||||
addressCity,
|
||||
addressState,
|
||||
addressCountry,
|
||||
addressPostcode,
|
||||
addressLat,
|
||||
addressLng,
|
||||
};
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type FieldMetadataDefaultValueForAnyType,
|
||||
} from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type';
|
||||
|
||||
import { nullifyEmptyActorDefaultValue } from './nullify-empty-actor-default-value.util';
|
||||
import { nullifyEmptyAddressDefaultValue } from './nullify-empty-address-default-value.util';
|
||||
import { nullifyEmptyCurrencyDefaultValue } from './nullify-empty-currency-default-value.util';
|
||||
import { nullifyEmptyEmailsDefaultValue } from './nullify-empty-emails-default-value.util';
|
||||
import { nullifyEmptyFullNameDefaultValue } from './nullify-empty-full-name-default-value.util';
|
||||
import { nullifyEmptyLinksDefaultValue } from './nullify-empty-links-default-value.util';
|
||||
import { nullifyEmptyPhonesDefaultValue } from './nullify-empty-phones-default-value.util';
|
||||
import { nullifyEmptyRichTextDefaultValue } from './nullify-empty-rich-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyCompositeDefaultValue = ({
|
||||
defaultValue,
|
||||
fieldType,
|
||||
}: {
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType;
|
||||
fieldType: CompositeFieldMetadataType;
|
||||
}): FieldMetadataDefaultValueForAnyType => {
|
||||
switch (fieldType) {
|
||||
case FieldMetadataType.PHONES:
|
||||
return nullifyEmptyPhonesDefaultValue(defaultValue);
|
||||
case FieldMetadataType.EMAILS:
|
||||
return nullifyEmptyEmailsDefaultValue(defaultValue);
|
||||
case FieldMetadataType.LINKS:
|
||||
return nullifyEmptyLinksDefaultValue(defaultValue);
|
||||
case FieldMetadataType.ADDRESS:
|
||||
return nullifyEmptyAddressDefaultValue(defaultValue);
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
return nullifyEmptyFullNameDefaultValue(defaultValue);
|
||||
case FieldMetadataType.ACTOR:
|
||||
return nullifyEmptyActorDefaultValue(defaultValue);
|
||||
case FieldMetadataType.CURRENCY:
|
||||
return nullifyEmptyCurrencyDefaultValue(defaultValue);
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
return nullifyEmptyRichTextDefaultValue(defaultValue);
|
||||
default:
|
||||
assertUnreachable(fieldType);
|
||||
}
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyCurrencyDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
amountMicros?: number | null;
|
||||
currencyCode?: string | null;
|
||||
};
|
||||
|
||||
const amountMicros = v.amountMicros ?? null;
|
||||
const currencyCode = isNullEquivalentTextDefaultValue(v.currencyCode)
|
||||
? null
|
||||
: (v.currencyCode ?? null);
|
||||
|
||||
if (amountMicros === null && currencyCode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { amountMicros, currencyCode };
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyEmailsDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
primaryEmail?: string | null;
|
||||
additionalEmails?: object | null;
|
||||
};
|
||||
|
||||
const primaryEmail = isNullEquivalentTextDefaultValue(v.primaryEmail)
|
||||
? null
|
||||
: (v.primaryEmail ?? null);
|
||||
const additionalEmails = isNullEquivalentArrayFieldValue(v.additionalEmails)
|
||||
? null
|
||||
: (v.additionalEmails ?? null);
|
||||
|
||||
if (primaryEmail === null && additionalEmails === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { primaryEmail, additionalEmails };
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyFullNameDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
};
|
||||
|
||||
const firstName = isNullEquivalentTextDefaultValue(v.firstName)
|
||||
? null
|
||||
: (v.firstName ?? null);
|
||||
const lastName = isNullEquivalentTextDefaultValue(v.lastName)
|
||||
? null
|
||||
: (v.lastName ?? null);
|
||||
|
||||
if (firstName === null && lastName === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { firstName, lastName };
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyLinksDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
primaryLinkLabel?: string | null;
|
||||
primaryLinkUrl?: string | null;
|
||||
secondaryLinks?: object | null;
|
||||
};
|
||||
|
||||
const primaryLinkLabel = isNullEquivalentTextDefaultValue(v.primaryLinkLabel)
|
||||
? null
|
||||
: (v.primaryLinkLabel ?? null);
|
||||
const primaryLinkUrl = isNullEquivalentTextDefaultValue(v.primaryLinkUrl)
|
||||
? null
|
||||
: (v.primaryLinkUrl ?? null);
|
||||
const secondaryLinks = isNullEquivalentArrayFieldValue(v.secondaryLinks)
|
||||
? null
|
||||
: (v.secondaryLinks ?? null);
|
||||
|
||||
if (
|
||||
primaryLinkLabel === null &&
|
||||
primaryLinkUrl === null &&
|
||||
secondaryLinks === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { primaryLinkLabel, primaryLinkUrl, secondaryLinks };
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyPhonesDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
primaryPhoneNumber?: string | null;
|
||||
primaryPhoneCountryCode?: string | null;
|
||||
primaryPhoneCallingCode?: string | null;
|
||||
additionalPhones?: object | null;
|
||||
};
|
||||
|
||||
const primaryPhoneNumber = isNullEquivalentTextDefaultValue(
|
||||
v.primaryPhoneNumber,
|
||||
)
|
||||
? null
|
||||
: (v.primaryPhoneNumber ?? null);
|
||||
const primaryPhoneCountryCode = isNullEquivalentTextDefaultValue(
|
||||
v.primaryPhoneCountryCode,
|
||||
)
|
||||
? null
|
||||
: (v.primaryPhoneCountryCode ?? null);
|
||||
const primaryPhoneCallingCode = isNullEquivalentTextDefaultValue(
|
||||
v.primaryPhoneCallingCode,
|
||||
)
|
||||
? null
|
||||
: (v.primaryPhoneCallingCode ?? null);
|
||||
const additionalPhones = isNullEquivalentArrayFieldValue(v.additionalPhones)
|
||||
? null
|
||||
: (v.additionalPhones ?? null);
|
||||
|
||||
if (
|
||||
primaryPhoneNumber === null &&
|
||||
primaryPhoneCountryCode === null &&
|
||||
primaryPhoneCallingCode === null &&
|
||||
additionalPhones === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
primaryPhoneNumber,
|
||||
primaryPhoneCountryCode,
|
||||
primaryPhoneCallingCode,
|
||||
additionalPhones,
|
||||
};
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNullEquivalentTextDefaultValue } from './is-null-equivalent-text-default-value.util';
|
||||
|
||||
export const nullifyEmptyRichTextDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): FieldMetadataDefaultValueForAnyType => {
|
||||
if (!isDefined(defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = defaultValue as {
|
||||
blocknote?: string | null;
|
||||
markdown?: string | null;
|
||||
};
|
||||
|
||||
const blocknote = isNullEquivalentTextDefaultValue(v.blocknote)
|
||||
? null
|
||||
: (v.blocknote ?? null);
|
||||
const markdown = isNullEquivalentTextDefaultValue(v.markdown)
|
||||
? null
|
||||
: (v.markdown ?? null);
|
||||
|
||||
if (blocknote === null && markdown === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { blocknote, markdown };
|
||||
};
|
||||
+13
@@ -13,6 +13,8 @@ import {
|
||||
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
|
||||
import { type FlatFieldMetadataEditableProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { nullifyEmptyCompositeDefaultValue } from 'src/engine/metadata-modules/flat-field-metadata/utils/nullify-empty-composite-default-value.util';
|
||||
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
|
||||
|
||||
type SanitizeRawUpdateFieldInputArgs = {
|
||||
@@ -45,6 +47,17 @@ export const sanitizeRawUpdateFieldInput = ({
|
||||
...option,
|
||||
}));
|
||||
|
||||
if (
|
||||
updatedEditableFieldProperties.defaultValue !== undefined &&
|
||||
isCompositeFieldMetadataType(existingFlatFieldMetadata.type)
|
||||
) {
|
||||
updatedEditableFieldProperties.defaultValue =
|
||||
nullifyEmptyCompositeDefaultValue({
|
||||
defaultValue: updatedEditableFieldProperties.defaultValue,
|
||||
fieldType: existingFlatFieldMetadata.type,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isStandardField || isSystemBuild) {
|
||||
return {
|
||||
updatedEditableFieldProperties,
|
||||
|
||||
Reference in New Issue
Block a user