common api - null equivalence (#15926)
closes https://github.com/twentyhq/core-team-issues/issues/1629 To do before requesting review : - filter update Migration to come in an other PR Strat : 1/ Null transformation - [x] Transform NULL equivalent value to NULL in field validation in common api - pre-query - with feature flag - [ ] Same logic in ORM (Not done, complex to handle feature flag here) - [x] Transform NULL value to equivalent in data formatting in ORM - post-query 2/ Migration (in other PR) for fieldMetadata not nullable with default defaultValue (empty string, ...) - [ ] Remove NOT NULL db constraint - [ ] Update record value to NULL - [ ] Update field metadata : isNullable:true - [ ] Update uniqueIndex whereClause (also for standard uniqueIndex) - [ ] Activate feature flag 3/ Update metadata creation - [x] No more default default value - [x] Update standard field nullability - [x] Remove index default whereClause for standard field 4/ Update filter - [x] When filtering on NULL or empty string, be sure all records are returned (the one with NULL + the one with "") 5/ Test - [ ] Strat. to do
This commit is contained in:
+67
@@ -0,0 +1,67 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
export const POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE = '';
|
||||
|
||||
export const DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE = '';
|
||||
|
||||
export const DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE = {};
|
||||
|
||||
export const POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE = '{}';
|
||||
|
||||
export const DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE = [];
|
||||
|
||||
export const POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE = '{}';
|
||||
|
||||
const DEFAULT_ADDRESS_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
addressStreet1: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
addressStreet2: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
addressCity: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
addressState: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
addressCountry: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
addressPostcode: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
const DEFAULT_EMAILS_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
primaryEmail: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
additionalEmails: DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
const DEFAULT_LINKS_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
primaryLinkUrl: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
primaryLinkLabel: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
secondaryLinks: DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
const DEFAULT_PHONES_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
primaryPhoneNumber: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
primaryPhoneCountryCode: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
primaryPhoneCallingCode: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
additionalPhones: DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
const DEFAULT_FULL_NAME_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
firstName: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
lastName: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
const DEFAULT_ACTOR_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
name: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
context: DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
const DEFAULT_RICH_TEXT_V2_FIELD_NULL_EQUIVALENT_VALUE = {
|
||||
markdown: DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
|
||||
export const DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE: Partial<
|
||||
Record<FieldMetadataType, Record<string, unknown>>
|
||||
> = {
|
||||
[FieldMetadataType.ADDRESS]: DEFAULT_ADDRESS_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
[FieldMetadataType.EMAILS]: DEFAULT_EMAILS_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
[FieldMetadataType.LINKS]: DEFAULT_LINKS_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
[FieldMetadataType.PHONES]: DEFAULT_PHONES_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
[FieldMetadataType.FULL_NAME]: DEFAULT_FULL_NAME_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
[FieldMetadataType.ACTOR]: DEFAULT_ACTOR_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
[FieldMetadataType.RICH_TEXT_V2]:
|
||||
DEFAULT_RICH_TEXT_V2_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
};
|
||||
+22
-9
@@ -44,6 +44,8 @@ import {
|
||||
CommonQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import { transformEmailsValue } from 'src/engine/core-modules/record-transformer/utils/transform-emails-value.util';
|
||||
import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
|
||||
@@ -55,7 +57,10 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
|
||||
|
||||
@Injectable()
|
||||
export class DataArgProcessor {
|
||||
constructor(private readonly recordPositionService: RecordPositionService) {}
|
||||
constructor(
|
||||
private readonly recordPositionService: RecordPositionService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async process({
|
||||
partialRecordInputs,
|
||||
@@ -76,6 +81,12 @@ export class DataArgProcessor {
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const isNullEquivalenceEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_NULL_EQUIVALENCE_ENABLED,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const processedRecords: Partial<ObjectRecord>[] = [];
|
||||
|
||||
for (const record of partialRecordInputs) {
|
||||
@@ -115,6 +126,7 @@ export class DataArgProcessor {
|
||||
fieldMetadata,
|
||||
key,
|
||||
value,
|
||||
isNullEquivalenceEnabled,
|
||||
);
|
||||
}
|
||||
processedRecords.push(processedRecord);
|
||||
@@ -139,6 +151,7 @@ export class DataArgProcessor {
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
key: string,
|
||||
value: unknown,
|
||||
isNullEquivalenceEnabled: boolean,
|
||||
): Promise<unknown> {
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.POSITION:
|
||||
@@ -154,7 +167,7 @@ export class DataArgProcessor {
|
||||
case FieldMetadataType.TEXT: {
|
||||
const validatedValue = validateTextFieldOrThrow(value, key);
|
||||
|
||||
return transformTextField(validatedValue);
|
||||
return transformTextField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
case FieldMetadataType.DATE:
|
||||
@@ -179,19 +192,19 @@ export class DataArgProcessor {
|
||||
fieldMetadata.options?.map((option) => option.value),
|
||||
);
|
||||
|
||||
return transformArrayField(validatedValue);
|
||||
return transformArrayField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.UUID:
|
||||
return validateUUIDFieldOrThrow(value, key);
|
||||
case FieldMetadataType.ARRAY: {
|
||||
const validatedValue = validateArrayFieldOrThrow(value, key);
|
||||
|
||||
return transformArrayField(validatedValue);
|
||||
return transformArrayField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.RAW_JSON: {
|
||||
const validatedValue = validateRawJsonFieldOrThrow(value, key);
|
||||
|
||||
return transformRawJsonField(validatedValue);
|
||||
return transformRawJsonField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.RELATION:
|
||||
case FieldMetadataType.MORPH_RELATION: {
|
||||
@@ -222,18 +235,18 @@ export class DataArgProcessor {
|
||||
case FieldMetadataType.EMAILS: {
|
||||
const validatedValue = validateEmailsFieldOrThrow(value, key);
|
||||
|
||||
return transformEmailsValue(validatedValue);
|
||||
return transformEmailsValue(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.FULL_NAME: {
|
||||
const validatedValue = validateFullNameFieldOrThrow(value, key);
|
||||
|
||||
return transformFullNameField(validatedValue);
|
||||
return transformFullNameField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
|
||||
case FieldMetadataType.ADDRESS: {
|
||||
const validatedValue = validateAddressFieldOrThrow(value, key);
|
||||
|
||||
return transformAddressField(validatedValue);
|
||||
return transformAddressField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.CURRENCY: {
|
||||
const validatedValue = validateCurrencyFieldOrThrow(value, key);
|
||||
@@ -243,7 +256,7 @@ export class DataArgProcessor {
|
||||
case FieldMetadataType.ACTOR: {
|
||||
const validatedValue = validateActorFieldOrThrow(value, key);
|
||||
|
||||
return transformActorField(validatedValue);
|
||||
return transformActorField(validatedValue, isNullEquivalenceEnabled);
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
const validatedValue = validateRichTextV2FieldOrThrow(value, key);
|
||||
|
||||
+11
@@ -2,16 +2,21 @@ import { isNull, isUndefined } from '@sniptt/guards';
|
||||
import { type FieldActorSource } from 'twenty-shared/types';
|
||||
|
||||
import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util';
|
||||
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
|
||||
|
||||
export const transformActorField = (
|
||||
value: {
|
||||
source?: FieldActorSource | null;
|
||||
context?: object | string | null;
|
||||
name?: string | null;
|
||||
workspaceMemberId?: string | null;
|
||||
} | null,
|
||||
isNullEquivalenceEnabled: boolean = false,
|
||||
): {
|
||||
source?: FieldActorSource | null;
|
||||
context?: object | string | null;
|
||||
name?: string | null;
|
||||
workspaceMemberId?: string | null;
|
||||
} | null => {
|
||||
if (isNull(value)) return null;
|
||||
|
||||
@@ -20,5 +25,11 @@ export const transformActorField = (
|
||||
context: isUndefined(value.context)
|
||||
? undefined
|
||||
: transformRawJsonField(value.context, isNullEquivalenceEnabled),
|
||||
name: isUndefined(value.name)
|
||||
? undefined
|
||||
: transformTextField(value.name, isNullEquivalenceEnabled),
|
||||
workspaceMemberId: isUndefined(value.workspaceMemberId)
|
||||
? undefined
|
||||
: value.workspaceMemberId,
|
||||
};
|
||||
};
|
||||
|
||||
+2
-4
@@ -1,4 +1,4 @@
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
|
||||
export const transformArrayField = (
|
||||
value: string | string[] | null,
|
||||
@@ -6,9 +6,7 @@ export const transformArrayField = (
|
||||
): string[] | null => {
|
||||
if (typeof value === 'string') return [value];
|
||||
|
||||
return isNullEquivalenceEnabled &&
|
||||
!isNull(value) &&
|
||||
Object.keys(value).length === 0
|
||||
return isNullEquivalenceEnabled && isNullEquivalentArrayFieldValue(value)
|
||||
? null
|
||||
: value;
|
||||
};
|
||||
|
||||
+2
-4
@@ -1,13 +1,11 @@
|
||||
//Json.parse() for RawJsonField is done in formatFieldMetadataValue in ORM
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isNullEquivalentRawJsonFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-raw-json-field-value.util';
|
||||
|
||||
export const transformRawJsonField = (
|
||||
value: object | string | null,
|
||||
isNullEquivalenceEnabled: boolean = false,
|
||||
): object | string | null => {
|
||||
return isNullEquivalenceEnabled &&
|
||||
!isNull(value) &&
|
||||
Object.keys(value).length === 0
|
||||
return isNullEquivalenceEnabled && isNullEquivalentRawJsonFieldValue(value)
|
||||
? null
|
||||
: value;
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isNullEquivalentTextFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-text-field-value.util';
|
||||
|
||||
export const transformTextField = (
|
||||
value: string | null,
|
||||
isNullEquivalenceEnabled: boolean = false,
|
||||
): string | null => {
|
||||
return isNullEquivalenceEnabled && !isNonEmptyString(value) ? null : value;
|
||||
return isNullEquivalenceEnabled && isNullEquivalentTextFieldValue(value)
|
||||
? null
|
||||
: value;
|
||||
};
|
||||
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
|
||||
import { findPostgresDefaultNullEquivalentValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/find-postgres-default-null-equivalent-value.util';
|
||||
|
||||
describe('findPostgresDefaultNullEquivalentValue', () => {
|
||||
describe('Simple Types', () => {
|
||||
describe('TEXT', () => {
|
||||
it('should return POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE for null', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(null, FieldMetadataType.TEXT),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE for empty string', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue('', FieldMetadataType.TEXT),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it("should return POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE for 'NULL'", () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'NULL',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return undefined for non-null equivalent value', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'value',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RAW_JSON', () => {
|
||||
it('should return POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE for null', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
null,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE for empty object', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
{},
|
||||
FieldMetadataType.RAW_JSON,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it("should return POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE for 'NULL'", () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'NULL',
|
||||
FieldMetadataType.RAW_JSON,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ARRAY', () => {
|
||||
it('should return POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE for null', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(null, FieldMetadataType.ARRAY),
|
||||
).toBe(POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE for empty array', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue([], FieldMetadataType.ARRAY),
|
||||
).toBe(POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it("should return POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE for 'NULL'", () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'NULL',
|
||||
FieldMetadataType.ARRAY,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACTOR', () => {
|
||||
it('should return text default for name', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.ACTOR,
|
||||
'name',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return json default for context', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
{},
|
||||
FieldMetadataType.ACTOR,
|
||||
'context',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADDRESS', () => {
|
||||
it.each([
|
||||
'addressStreet1',
|
||||
'addressStreet2',
|
||||
'addressCity',
|
||||
'addressState',
|
||||
'addressPostcode',
|
||||
'addressCountry',
|
||||
])('should return text default for %s', (key) => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.ADDRESS,
|
||||
key,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EMAILS', () => {
|
||||
it('should return text default for primaryEmail', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.EMAILS,
|
||||
'primaryEmail',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return array default for additionalEmails', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
[],
|
||||
FieldMetadataType.EMAILS,
|
||||
'additionalEmails',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LINKS', () => {
|
||||
it('should return text default for primaryLinkUrl', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.LINKS,
|
||||
'primaryLinkUrl',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return array default for secondaryLinks', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
[],
|
||||
FieldMetadataType.LINKS,
|
||||
'secondaryLinks',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHONES', () => {
|
||||
it('should return text default for primaryPhoneNumber', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.PHONES,
|
||||
'primaryPhoneNumber',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return array default for additionalPhones', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
[],
|
||||
FieldMetadataType.PHONES,
|
||||
'additionalPhones',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RICH_TEXT_V2', () => {
|
||||
it('should return json default for blocknote', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
{},
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
'blocknote',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
|
||||
it('should return text default for markdown', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
'markdown',
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FULL_NAME', () => {
|
||||
it.each(['firstName', 'lastName'])(
|
||||
'should return text default for %s',
|
||||
(key) => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
key,
|
||||
),
|
||||
).toBe(POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for unknown type', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(null, 'UNKNOWN' as any),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for unknown composite key', () => {
|
||||
expect(
|
||||
findPostgresDefaultNullEquivalentValue(
|
||||
'',
|
||||
FieldMetadataType.ACTOR,
|
||||
'unknown',
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
|
||||
describe('isNullEquivalentArrayFieldValue', () => {
|
||||
describe('null-equivalent values', () => {
|
||||
it('should return true when value is null', () => {
|
||||
const result = isNullEquivalentArrayFieldValue(null);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when value is an empty array', () => {
|
||||
const result = isNullEquivalentArrayFieldValue([]);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-null-equivalent values', () => {
|
||||
it('should return false when value is undefined', () => {
|
||||
const result = isNullEquivalentArrayFieldValue(undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('should return false when value is an array with one item', () => {
|
||||
const result = isNullEquivalentArrayFieldValue(['item']);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is a string', () => {
|
||||
const result = isNullEquivalentArrayFieldValue('hello');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is an empty string', () => {
|
||||
const result = isNullEquivalentArrayFieldValue('');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { isNullEquivalentRawJsonFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-raw-json-field-value.util';
|
||||
|
||||
describe('isNullEquivalentRawJsonFieldValue', () => {
|
||||
describe('null-equivalent values', () => {
|
||||
it('should return true when value is null', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue(null);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when value is an empty object', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue({});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when value is an empty array', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue([]);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-null-equivalent values', () => {
|
||||
it('should return false when value is undefined', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue(undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('should return false when value is an object with properties', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue({ key: 'value' });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is a string', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue('hello');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is an empty string', () => {
|
||||
const result = isNullEquivalentRawJsonFieldValue('');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { isNullEquivalentTextFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-text-field-value.util';
|
||||
|
||||
describe('isNullEquivalentTextFieldValue', () => {
|
||||
describe('null-equivalent values', () => {
|
||||
it('should return true when value is an empty string', () => {
|
||||
const result = isNullEquivalentTextFieldValue('');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when value is null', () => {
|
||||
const result = isNullEquivalentTextFieldValue(null);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-null-equivalent values', () => {
|
||||
it('should return false when value is a non-empty string', () => {
|
||||
const result = isNullEquivalentTextFieldValue('hello');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when value is undefined', () => {
|
||||
const result = isNullEquivalentTextFieldValue(undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
|
||||
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
|
||||
import { isNullEquivalentRawJsonFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-raw-json-field-value.util';
|
||||
import { isNullEquivalentTextFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-text-field-value.util';
|
||||
|
||||
export const findPostgresDefaultNullEquivalentValue = (
|
||||
value: unknown,
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
key?: string,
|
||||
) => {
|
||||
switch (fieldMetadataType) {
|
||||
case FieldMetadataType.TEXT:
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return isNullEquivalentRawJsonFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case FieldMetadataType.ARRAY:
|
||||
return isNullEquivalentArrayFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case FieldMetadataType.ACTOR: {
|
||||
switch (key) {
|
||||
case 'name':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'context':
|
||||
return isNullEquivalentRawJsonFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.ADDRESS: {
|
||||
switch (key) {
|
||||
case 'addressStreet1':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressStreet2':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressCity':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressState':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressPostcode':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'addressCountry':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.EMAILS: {
|
||||
switch (key) {
|
||||
case 'primaryEmail':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'additionalEmails':
|
||||
return isNullEquivalentArrayFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.LINKS: {
|
||||
switch (key) {
|
||||
case 'primaryLinkUrl':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'primaryLinkLabel':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'secondaryLinks':
|
||||
return isNullEquivalentArrayFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.PHONES: {
|
||||
switch (key) {
|
||||
case 'primaryPhoneNumber':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'primaryPhoneCountryCode':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'primaryPhoneCallingCode':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'additionalPhones':
|
||||
return isNullEquivalentArrayFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.RICH_TEXT_V2: {
|
||||
switch (key) {
|
||||
case 'blocknote':
|
||||
return isNullEquivalentRawJsonFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'markdown':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case FieldMetadataType.FULL_NAME: {
|
||||
switch (key) {
|
||||
case 'firstName':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
case 'lastName':
|
||||
return isNullEquivalentTextFieldValue(value) || value === 'NULL'
|
||||
? POSTGRES_DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const isNullEquivalentArrayFieldValue = (value: unknown): boolean => {
|
||||
return (Array.isArray(value) && value.length === 0) || value === null;
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isEmptyObject } from 'twenty-shared/utils';
|
||||
|
||||
export const isNullEquivalentRawJsonFieldValue = (value: unknown): boolean => {
|
||||
if (isNull(value)) return true;
|
||||
|
||||
return isEmptyObject(value);
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { isNull } from '@sniptt/guards';
|
||||
|
||||
import { DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE } from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
|
||||
|
||||
export const isNullEquivalentTextFieldValue = (value: unknown): boolean => {
|
||||
if (isNull(value)) return true;
|
||||
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value === DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE
|
||||
);
|
||||
};
|
||||
+8
@@ -3,6 +3,8 @@ import { FieldActorSource } from 'twenty-shared/types';
|
||||
|
||||
import { validateRatingAndSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util';
|
||||
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
|
||||
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
|
||||
import { validateUUIDFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util';
|
||||
import {
|
||||
CommonQueryRunnerException,
|
||||
CommonQueryRunnerExceptionCode,
|
||||
@@ -28,6 +30,12 @@ export const validateActorFieldOrThrow = (
|
||||
case 'context':
|
||||
validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
|
||||
break;
|
||||
case 'name':
|
||||
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
|
||||
break;
|
||||
case 'workspaceMemberId':
|
||||
validateUUIDFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
|
||||
break;
|
||||
default:
|
||||
throw new CommonQueryRunnerException(
|
||||
`Invalid subfield ${subField} for actor field "${fieldName}"`,
|
||||
|
||||
Reference in New Issue
Block a user