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:
@@ -1275,6 +1275,7 @@ export enum FeatureFlagKey {
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED',
|
||||
IS_NULL_EQUIVALENCE_ENABLED = 'IS_NULL_EQUIVALENCE_ENABLED',
|
||||
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
|
||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
|
||||
@@ -1258,6 +1258,7 @@ export enum FeatureFlagKey {
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED',
|
||||
IS_NULL_EQUIVALENCE_ENABLED = 'IS_NULL_EQUIVALENCE_ENABLED',
|
||||
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
|
||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
|
||||
+1
-2
@@ -7,8 +7,7 @@ import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/
|
||||
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
import { capitalize, isEmptyObject } from 'twenty-shared/utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export type GetRecordFromCacheArgs = {
|
||||
|
||||
+1
-2
@@ -40,9 +40,8 @@ import {
|
||||
type TSVectorFilter,
|
||||
type UUIDFilter,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
|
||||
const isLeafFilter = (
|
||||
filter: RecordGqlOperationFilter,
|
||||
|
||||
+5
-2
@@ -8,12 +8,15 @@ import {
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { parsePhoneNumberWithError, type CountryCode } from 'libphonenumber-js';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
assertUnreachable,
|
||||
isDefined,
|
||||
isEmptyObject,
|
||||
} from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
import { castToString } from '~/utils/castToString';
|
||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
type BuildRecordFromImportedStructuredRowArgs = {
|
||||
|
||||
+1
-2
@@ -7,9 +7,8 @@ import { getPreviousSteps } from '@/workflow/workflow-steps/utils/getWorkflowPre
|
||||
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
import { filterOutputSchema } from '@/workflow/workflow-variables/utils/filterOutputSchema';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
|
||||
export const useAvailableVariablesInWorkflowStep = ({
|
||||
shouldDisplayRecordFields,
|
||||
|
||||
@@ -16,6 +16,7 @@ IS_IMAP_SMTP_CALDAV_ENABLED=false
|
||||
CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
||||
MESSAGING_PROVIDER_MICROSOFT_ENABLED=false
|
||||
CALENDAR_PROVIDER_MICROSOFT_ENABLED=false
|
||||
TELEMETRY_ENABLED=false
|
||||
|
||||
AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
||||
AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
|
||||
|
||||
+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}"`,
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
import { compositeTypeDefinitions } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { type WhereExpressionBuilder } from 'typeorm';
|
||||
import { compositeTypeDefinitions } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
@@ -60,12 +60,12 @@ export class GraphqlQueryFilterFieldParser {
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const { sql, params } = computeWhereConditionParts({
|
||||
operator,
|
||||
objectNameSingular,
|
||||
key,
|
||||
value,
|
||||
fieldMetadataType: fieldMetadata.type,
|
||||
});
|
||||
|
||||
if (isFirst) {
|
||||
@@ -125,7 +125,9 @@ export class GraphqlQueryFilterFieldParser {
|
||||
operator,
|
||||
objectNameSingular,
|
||||
key: fullFieldName,
|
||||
subFieldKey,
|
||||
value,
|
||||
fieldMetadataType: fieldMetadata.type,
|
||||
});
|
||||
|
||||
if (isFirst && index === 0) {
|
||||
|
||||
+27
-7
@@ -1,5 +1,8 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { findPostgresDefaultNullEquivalentValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/find-postgres-default-null-equivalent-value.util';
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
@@ -15,30 +18,45 @@ export const computeWhereConditionParts = ({
|
||||
operator,
|
||||
objectNameSingular,
|
||||
key,
|
||||
subFieldKey,
|
||||
value,
|
||||
fieldMetadataType,
|
||||
}: {
|
||||
operator: string;
|
||||
objectNameSingular: string;
|
||||
key: string;
|
||||
subFieldKey?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any;
|
||||
fieldMetadataType: FieldMetadataType;
|
||||
}): WhereConditionParts => {
|
||||
const uuid = Math.random().toString(36).slice(2, 7);
|
||||
|
||||
const secondUuid = Math.random().toString(36).slice(2, 7);
|
||||
|
||||
//TODO : Remove filter null equivalence injection once feature flag removed + null equivalence transformation added in ORM
|
||||
const nullEquivalentFieldValue = findPostgresDefaultNullEquivalentValue(
|
||||
value,
|
||||
fieldMetadataType,
|
||||
subFieldKey,
|
||||
);
|
||||
|
||||
const hasNullEquivalentFieldValue = isDefined(nullEquivalentFieldValue);
|
||||
|
||||
switch (operator) {
|
||||
case 'isEmptyArray':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" = '{}'`,
|
||||
sql: `"${objectNameSingular}"."${key}" = '{}'${hasNullEquivalentFieldValue ? ` OR "${objectNameSingular}"."${key}" IS NULL` : ''}`,
|
||||
params: {},
|
||||
};
|
||||
case 'eq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
|
||||
sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}${hasNullEquivalentFieldValue ? ` OR "${objectNameSingular}"."${key}" IS NULL` : ''}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'neq':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" != :${key}${uuid}`,
|
||||
sql: `"${objectNameSingular}"."${key}" != :${key}${uuid}${hasNullEquivalentFieldValue ? ` OR "${objectNameSingular}"."${key}" IS NOT NULL` : ''}`,
|
||||
params: { [`${key}${uuid}`]: value },
|
||||
};
|
||||
case 'gt':
|
||||
@@ -68,17 +86,19 @@ export const computeWhereConditionParts = ({
|
||||
};
|
||||
case 'is':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}" IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`,
|
||||
params: {},
|
||||
sql: `"${objectNameSingular}"."${key}" IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}${hasNullEquivalentFieldValue ? ` OR "${objectNameSingular}"."${key}" = :${key}${secondUuid}` : ''}`,
|
||||
params: hasNullEquivalentFieldValue
|
||||
? { [`${key}${secondUuid}`]: nullEquivalentFieldValue }
|
||||
: {},
|
||||
};
|
||||
case 'like':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`,
|
||||
sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}${hasNullEquivalentFieldValue ? ` OR "${objectNameSingular}"."${key}" IS NULL` : ''}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'ilike':
|
||||
return {
|
||||
sql: `"${objectNameSingular}"."${key}"::text ILIKE :${key}${uuid}`,
|
||||
sql: `"${objectNameSingular}"."${key}"::text ILIKE :${key}${uuid}${hasNullEquivalentFieldValue ? ` OR "${objectNameSingular}"."${key}" IS NULL` : ''}`,
|
||||
params: { [`${key}${uuid}`]: `${value}` },
|
||||
};
|
||||
case 'startsWith':
|
||||
|
||||
+1
@@ -16,4 +16,5 @@ export enum FeatureFlagKey {
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED = 'IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED',
|
||||
IS_NULL_EQUIVALENCE_ENABLED = 'IS_NULL_EQUIVALENCE_ENABLED',
|
||||
}
|
||||
|
||||
+2
@@ -128,11 +128,13 @@ export class UpsertRecordService {
|
||||
? conflictPathsUniqueFieldsToUpdate
|
||||
: ['id'];
|
||||
|
||||
//TODO : To delete once IS_NULL_EQUIVALENCE_ENABLED feature flag removed
|
||||
const indexPredicate = uniqueFieldsToUpdate
|
||||
.map((field) =>
|
||||
computeUniqueIndexWhereClause({
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
defaultValue: field.defaultValue,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
+17
-4
@@ -1,15 +1,21 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const transformEmailsValue = (value: any): any => {
|
||||
export const transformEmailsValue = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any,
|
||||
isNullEquivalenceEnabled: boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): any => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let additionalEmails = value?.additionalEmails;
|
||||
let additionalEmails: string | null = value?.additionalEmails;
|
||||
const primaryEmail = value?.primaryEmail
|
||||
? value.primaryEmail.toLowerCase()
|
||||
: '';
|
||||
: isNullEquivalenceEnabled
|
||||
? null
|
||||
: '';
|
||||
|
||||
if (additionalEmails) {
|
||||
try {
|
||||
@@ -22,6 +28,13 @@ export const transformEmailsValue = (value: any): any => {
|
||||
additionalEmails = JSON.stringify(
|
||||
emailArray.map((email) => email.toLowerCase()),
|
||||
);
|
||||
|
||||
if (isNullEquivalenceEnabled) {
|
||||
additionalEmails =
|
||||
Array.isArray(emailArray) && emailArray.length === 0
|
||||
? null
|
||||
: additionalEmails;
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,11 +1,11 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type LinkMetadataNullable } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
parseJson,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type LinkMetadataNullable } from 'twenty-shared/types';
|
||||
|
||||
import { removeEmptyLinks } from 'src/engine/core-modules/record-transformer/utils/remove-empty-links';
|
||||
|
||||
|
||||
+4
-4
@@ -5,6 +5,10 @@ import {
|
||||
parsePhoneNumberWithError,
|
||||
} from 'libphonenumber-js';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
type AdditionalPhoneMetadata,
|
||||
type PhonesMetadata,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
getCountryCodesForCallingCode,
|
||||
isDefined,
|
||||
@@ -12,10 +16,6 @@ import {
|
||||
parseJson,
|
||||
removeUndefinedFields,
|
||||
} from 'twenty-shared/utils';
|
||||
import {
|
||||
type AdditionalPhoneMetadata,
|
||||
type PhonesMetadata,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
RecordTransformerException,
|
||||
|
||||
+3
-13
@@ -1,22 +1,12 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
|
||||
|
||||
describe('generateNullable', () => {
|
||||
it('should generate a nullable value false for TEXT, EMAIL, PHONE no matter what the input is', () => {
|
||||
expect(generateNullable(FieldMetadataType.TEXT, false)).toEqual(false);
|
||||
|
||||
expect(generateNullable(FieldMetadataType.TEXT, true)).toEqual(false);
|
||||
|
||||
expect(generateNullable(FieldMetadataType.TEXT)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should should return true if no input is given', () => {
|
||||
expect(generateNullable(FieldMetadataType.DATE_TIME)).toEqual(true);
|
||||
expect(generateNullable()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should should return the input value if the input value is given', () => {
|
||||
expect(generateNullable(FieldMetadataType.DATE_TIME, true)).toEqual(true);
|
||||
expect(generateNullable(FieldMetadataType.DATE_TIME, false)).toEqual(false);
|
||||
expect(generateNullable(true)).toEqual(true);
|
||||
expect(generateNullable(false)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util';
|
||||
|
||||
describe('isValidUniqueFieldDefaultValueCombination', () => {
|
||||
it('should return true if the field has a custom default value and is not unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "'custom value'",
|
||||
isUnique: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the field has standard default value and is unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "''",
|
||||
isUnique: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the field has custom default value and is unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "'custom value'",
|
||||
isUnique: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+1
-42
@@ -1,4 +1,4 @@
|
||||
import { FieldMetadataType, FieldActorSource } from 'twenty-shared/types';
|
||||
import { FieldActorSource, FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
@@ -7,47 +7,6 @@ export function generateDefaultValue(
|
||||
type: FieldMetadataType,
|
||||
): FieldMetadataDefaultValue {
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
return "''" satisfies FieldMetadataDefaultValue<FieldMetadataType.TEXT>;
|
||||
case FieldMetadataType.EMAILS:
|
||||
return {
|
||||
primaryEmail: "''",
|
||||
additionalEmails: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.EMAILS>;
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
return {
|
||||
firstName: "''",
|
||||
lastName: "''",
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.FULL_NAME>;
|
||||
case FieldMetadataType.ADDRESS:
|
||||
return {
|
||||
addressStreet1: "''",
|
||||
addressStreet2: "''",
|
||||
addressCity: "''",
|
||||
addressState: "''",
|
||||
addressCountry: "''",
|
||||
addressPostcode: "''",
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.ADDRESS>;
|
||||
case FieldMetadataType.CURRENCY:
|
||||
return {
|
||||
amountMicros: null,
|
||||
currencyCode: "''",
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.CURRENCY>;
|
||||
case FieldMetadataType.LINKS:
|
||||
return {
|
||||
primaryLinkLabel: "''",
|
||||
primaryLinkUrl: "''",
|
||||
secondaryLinks: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.LINKS>;
|
||||
case FieldMetadataType.PHONES:
|
||||
return {
|
||||
primaryPhoneNumber: "''",
|
||||
primaryPhoneCountryCode: "''",
|
||||
primaryPhoneCallingCode: "''",
|
||||
additionalPhones: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.PHONES>;
|
||||
case FieldMetadataType.ACTOR:
|
||||
return {
|
||||
source: `'${FieldActorSource.MANUAL}'`,
|
||||
|
||||
+1
-8
@@ -1,6 +1,4 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
export function generateNullable(
|
||||
type: FieldMetadataType,
|
||||
inputNullableValue?: boolean,
|
||||
isRemoteCreation?: boolean,
|
||||
): boolean {
|
||||
@@ -8,10 +6,5 @@ export function generateNullable(
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
return false;
|
||||
default:
|
||||
return inputNullableValue ?? true;
|
||||
}
|
||||
return inputNullableValue ?? true;
|
||||
}
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import {
|
||||
compositeTypeDefinitions,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
export const isValidUniqueFieldDefaultValueCombination = ({
|
||||
defaultValue,
|
||||
isUnique,
|
||||
type,
|
||||
}: {
|
||||
defaultValue: FieldMetadataDefaultValue;
|
||||
isUnique: boolean;
|
||||
type: FieldMetadataType;
|
||||
}) => {
|
||||
if (!isUnique) return true;
|
||||
|
||||
const defaultDefaultValue = generateDefaultValue(type);
|
||||
|
||||
if (!isCompositeFieldMetadataType(type))
|
||||
return defaultValue === defaultDefaultValue;
|
||||
|
||||
const doUniquePropertiesHaveDefaultValues =
|
||||
compositeTypeDefinitions
|
||||
.get(type)
|
||||
?.properties.filter((property) => property.isIncludedInUniqueConstraint)
|
||||
.every(
|
||||
({ name }) =>
|
||||
(defaultValue as Record<string, string | null>)?.[name] ===
|
||||
(defaultDefaultValue as Record<string, string | null>)?.[name],
|
||||
) ?? false;
|
||||
|
||||
return doUniquePropertiesHaveDefaultValues;
|
||||
};
|
||||
-1
@@ -27,7 +27,6 @@ export const prepareCustomFieldMetadataForCreation = (
|
||||
objectMetadataId: fieldMetadataInput.objectMetadataId,
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
isNullable: generateNullable(
|
||||
fieldMetadataInput.type,
|
||||
fieldMetadataInput.isNullable,
|
||||
fieldMetadataInput.isRemoteCreation,
|
||||
),
|
||||
|
||||
-1
@@ -34,7 +34,6 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: createFieldInput.isLabelSyncedWithName ?? false,
|
||||
isNullable: generateNullable(
|
||||
createFieldInput.type,
|
||||
createFieldInput.isNullable,
|
||||
createFieldInput.isRemoteCreation,
|
||||
),
|
||||
|
||||
+1
-3
@@ -3,9 +3,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type CompositeType,
|
||||
compositeTypeDefinitions,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, type QueryRunner, Repository } from 'typeorm';
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-field-metadata.exception';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { type IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
@@ -197,7 +196,6 @@ export class IndexMetadataService {
|
||||
computeObjectTargetTable(objectMetadata),
|
||||
updatedFieldMetadata.name,
|
||||
])}`,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(updatedFieldMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('computeUniqueIndexWhereClause', () => {
|
||||
it('should return undefined if standard default value is not defined', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'testField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return a where clause for a an atomic type field', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testTextField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBe('"testTextField" != \'\'');
|
||||
});
|
||||
|
||||
it('should return a where clause for a composite type field', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
name: 'testEmailsField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBe('"testEmailsFieldPrimaryEmail" != \'\'');
|
||||
});
|
||||
});
|
||||
+10
-10
@@ -11,19 +11,19 @@ import {
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
IndexMetadataException,
|
||||
IndexMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-field-metadata.exception';
|
||||
|
||||
//TODO : To delete once IS_NULL_EQUIVALENCE_ENABLED feature flag removed
|
||||
export const computeUniqueIndexWhereClause = (
|
||||
fieldMetadata: Pick<FieldMetadataEntity, 'type' | 'name'>,
|
||||
fieldMetadata: Pick<FieldMetadataEntity, 'type' | 'name' | 'defaultValue'>,
|
||||
) => {
|
||||
const defaultDefaultValue = generateDefaultValue(fieldMetadata.type);
|
||||
const defaultValue = fieldMetadata.defaultValue;
|
||||
|
||||
if (!isDefined(defaultDefaultValue)) return;
|
||||
if (!isDefined(defaultValue)) return;
|
||||
|
||||
if (
|
||||
fieldMetadata.type === FieldMetadataType.RELATION ||
|
||||
@@ -36,7 +36,7 @@ export const computeUniqueIndexWhereClause = (
|
||||
}
|
||||
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return `"${fieldMetadata.name}" != ${defaultDefaultValue}`;
|
||||
return `"${fieldMetadata.name}" != ${defaultValue}`;
|
||||
}
|
||||
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
@@ -48,7 +48,7 @@ export const computeUniqueIndexWhereClause = (
|
||||
);
|
||||
}
|
||||
|
||||
const defaultDefaultValueProperties = Object.keys(defaultDefaultValue);
|
||||
const defaultDefaultValueProperties = Object.keys(defaultValue);
|
||||
|
||||
const columnNamesWithDefaultValues = compositeType.properties
|
||||
.filter(
|
||||
@@ -57,13 +57,13 @@ export const computeUniqueIndexWhereClause = (
|
||||
defaultDefaultValueProperties.includes(property.name),
|
||||
)
|
||||
.map((property) => {
|
||||
const defaultValue =
|
||||
defaultDefaultValue[property.name as keyof typeof defaultDefaultValue];
|
||||
const defaultValueProperty =
|
||||
defaultValue[property.name as keyof typeof defaultValue];
|
||||
|
||||
if (isNonEmptyString(defaultValue)) {
|
||||
if (isNonEmptyString(defaultValueProperty)) {
|
||||
return [
|
||||
computeCompositeColumnName(fieldMetadata, property),
|
||||
defaultValue,
|
||||
defaultValueProperty,
|
||||
];
|
||||
}
|
||||
})
|
||||
|
||||
+2
@@ -140,6 +140,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false,
|
||||
IS_DASHBOARD_V2_ENABLED: false,
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false,
|
||||
IS_NULL_EQUIVALENCE_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
emitMutationEvent: jest.fn(),
|
||||
@@ -167,6 +168,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false,
|
||||
IS_DASHBOARD_V2_ENABLED: false,
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false,
|
||||
IS_NULL_EQUIVALENCE_ENABLED: false,
|
||||
},
|
||||
permissionsPerRoleId: {},
|
||||
} as WorkspaceDataSource;
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
|
||||
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
compositeTypeDefinitions,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE,
|
||||
DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
|
||||
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
@@ -46,7 +53,11 @@ export function formatResult<T>(
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
|
||||
|
||||
const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key];
|
||||
const fieldMetadataId =
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName[key] ||
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName[
|
||||
compositePropertyArgs?.parentField ?? ''
|
||||
];
|
||||
|
||||
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[
|
||||
fieldMetadataId
|
||||
@@ -66,7 +77,7 @@ export function formatResult<T>(
|
||||
);
|
||||
} else if (fieldMetadata) {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
newData[key] = formatFieldMetadataValue(value, fieldMetadata);
|
||||
newData[key] = formatFieldMetadataValue(value, fieldMetadata.type);
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
newData[key] = value;
|
||||
@@ -99,7 +110,7 @@ export function formatResult<T>(
|
||||
);
|
||||
}
|
||||
|
||||
if (!compositePropertyArgs) {
|
||||
if (!compositePropertyArgs || !isDefined(fieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,7 +123,13 @@ export function formatResult<T>(
|
||||
}
|
||||
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
newData[parentField][compositeProperty.name] = value;
|
||||
newData[parentField][compositeProperty.name] = isNull(value)
|
||||
? transformCompositeFieldNullValue(
|
||||
value,
|
||||
compositeProperty.name,
|
||||
fieldMetadata,
|
||||
)
|
||||
: value;
|
||||
}
|
||||
|
||||
const fieldMetadataItemsOfTypeDateOnly = Object.values(
|
||||
@@ -162,17 +179,50 @@ export function getCompositeFieldMetadataMap(
|
||||
function formatFieldMetadataValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
) {
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
(fieldMetadata.type === FieldMetadataType.MULTI_SELECT ||
|
||||
fieldMetadata.type === FieldMetadataType.ARRAY)
|
||||
(fieldMetadataType === FieldMetadataType.MULTI_SELECT ||
|
||||
fieldMetadataType === FieldMetadataType.ARRAY)
|
||||
) {
|
||||
const cleanedValue = value.replace(/{|}/g, '').trim();
|
||||
|
||||
return cleanedValue ? cleanedValue.split(',') : [];
|
||||
}
|
||||
|
||||
if (isNull(value)) {
|
||||
if (
|
||||
fieldMetadataType === FieldMetadataType.MULTI_SELECT ||
|
||||
fieldMetadataType === FieldMetadataType.ARRAY
|
||||
) {
|
||||
return DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE;
|
||||
}
|
||||
|
||||
if (fieldMetadataType === FieldMetadataType.RAW_JSON) {
|
||||
return DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE;
|
||||
}
|
||||
|
||||
if (fieldMetadataType === FieldMetadataType.TEXT) {
|
||||
return DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function transformCompositeFieldNullValue(
|
||||
value: unknown,
|
||||
compositePropertyName: string,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
) {
|
||||
if (!isNull(value)) return value;
|
||||
|
||||
return (
|
||||
DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE[fieldMetadata.type]?.[
|
||||
compositePropertyName
|
||||
] ?? value
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ describe('WorkspaceMigrationIndexFactory', () => {
|
||||
expect(firstMigration.indexes[0].columns).toEqual(['simpleField']);
|
||||
expect(firstMigration.indexes[0].type).toBe('BTREE');
|
||||
expect(firstMigration.indexes[0].isUnique).toBe(true);
|
||||
expect(firstMigration.indexes[0].where).toBe('"simpleField" != \'\'');
|
||||
expect(firstMigration.indexes[0].where).toBeNull();
|
||||
});
|
||||
|
||||
it('should create index migrations for relation fields', async () => {
|
||||
|
||||
+2
-6
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type CompositeType,
|
||||
compositeTypeDefinitions,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -92,17 +92,13 @@ export const createIndexMigration = async (
|
||||
.flat()
|
||||
.filter(isDefined);
|
||||
|
||||
const defaultWhereClause = indexMetadata.isUnique
|
||||
? `${columns.map((column) => `"${column}"`).join(" != '' AND ")} != ''`
|
||||
: null;
|
||||
|
||||
return {
|
||||
name: indexMetadata.name,
|
||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||
isUnique: indexMetadata.isUnique,
|
||||
columns,
|
||||
type: indexMetadata.indexType,
|
||||
where: indexMetadata.indexWhereClause ?? defaultWhereClause,
|
||||
where: indexMetadata.indexWhereClause,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+1
-6
@@ -8,7 +8,6 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util';
|
||||
import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
|
||||
@@ -147,11 +146,7 @@ export class FlatIndexValidatorService {
|
||||
if (flatIndexToValidate.isUnique) {
|
||||
if (
|
||||
isDefined(relatedFlatField.defaultValue) &&
|
||||
!isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: relatedFlatField.defaultValue,
|
||||
isUnique: relatedFlatField.isUnique ?? false,
|
||||
type: relatedFlatField.type,
|
||||
})
|
||||
relatedFlatField.isUnique
|
||||
) {
|
||||
const fieldName = relatedFlatField.name;
|
||||
const fieldType = relatedFlatField.type;
|
||||
|
||||
+2
-1
@@ -1,8 +1,8 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
|
||||
@@ -68,6 +68,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Attachment type (deprecated - use fileCategory)`,
|
||||
icon: 'IconList',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
type: string;
|
||||
|
||||
@WorkspaceField({
|
||||
|
||||
+5
-3
@@ -1,10 +1,10 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
AddressMetadata,
|
||||
FieldMetadataType,
|
||||
LinksMetadata,
|
||||
RelationOnDeleteAction,
|
||||
type CurrencyMetadata,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
@@ -30,8 +30,8 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re
|
||||
import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import {
|
||||
type FieldTypeAndNameMetadata,
|
||||
getTsVectorColumnExpressionFromFields,
|
||||
type FieldTypeAndNameMetadata,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
@@ -71,6 +71,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`The company name`,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
name: string;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -84,6 +85,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
},
|
||||
})
|
||||
@WorkspaceIsUnique()
|
||||
@WorkspaceIsNullable()
|
||||
domainName: LinksMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
type RichTextV2Metadata,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
@@ -71,6 +71,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Note title`,
|
||||
icon: 'IconNotes',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
title: string;
|
||||
|
||||
@WorkspaceField({
|
||||
|
||||
+3
-2
@@ -1,9 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
type CurrencyMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
|
||||
@@ -63,6 +63,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`The opportunity name`,
|
||||
icon: 'IconTargetArrow',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
name: string;
|
||||
|
||||
@WorkspaceField({
|
||||
|
||||
+8
-3
@@ -1,10 +1,10 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
EmailsMetadata,
|
||||
FieldMetadataType,
|
||||
PhonesMetadata,
|
||||
RelationOnDeleteAction,
|
||||
type FullNameMetadata,
|
||||
type LinksMetadata,
|
||||
} from 'twenty-shared/types';
|
||||
@@ -31,8 +31,8 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re
|
||||
import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import {
|
||||
type FieldTypeAndNameMetadata,
|
||||
getTsVectorColumnExpressionFromFields,
|
||||
type FieldTypeAndNameMetadata,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
@@ -96,6 +96,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
},
|
||||
})
|
||||
@WorkspaceIsUnique()
|
||||
@WorkspaceIsNullable()
|
||||
emails: EmailsMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -125,6 +126,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Contact’s job title`,
|
||||
icon: 'IconBriefcase',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
jobTitle: string;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -147,6 +149,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
maxNumberOfValues: 1,
|
||||
},
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
phones: PhonesMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -156,6 +159,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Contact’s city`,
|
||||
icon: 'IconMap',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
city: string;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -166,6 +170,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconFileUpload',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNullable()
|
||||
avatarUrl: string;
|
||||
|
||||
@WorkspaceField({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
ActorMetadata,
|
||||
type RichTextV2Metadata,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
@@ -74,6 +74,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Task title`,
|
||||
icon: 'IconNotes',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
title: string;
|
||||
|
||||
@WorkspaceField({
|
||||
|
||||
-4
@@ -6,8 +6,6 @@ exports[`Create input validation - TEXT Gql create input - failure TEXT - should
|
||||
|
||||
exports[`Create input validation - TEXT Gql create input - failure TEXT - should fail with : {"textField":1} 1`] = `"String cannot represent a non string value: 1"`;
|
||||
|
||||
exports[`Create input validation - TEXT Gql create input - failure TEXT - should fail with : {"textField":null} 1`] = `"null value in column "textField" of relation "_apiInputValidationTestObject" violates not-null constraint"`;
|
||||
|
||||
exports[`Create input validation - TEXT Gql create input - failure TEXT - should fail with : {"textField":true} 1`] = `"String cannot represent a non string value: true"`;
|
||||
|
||||
exports[`Create input validation - TEXT Rest create input - failure TEXT - should fail with : {"textField":[]} 1`] = `"["Invalid string value [] for text field \\"textField\\""]"`;
|
||||
@@ -16,6 +14,4 @@ exports[`Create input validation - TEXT Rest create input - failure TEXT - shoul
|
||||
|
||||
exports[`Create input validation - TEXT Rest create input - failure TEXT - should fail with : {"textField":1} 1`] = `"["Invalid string value 1 for text field \\"textField\\""]"`;
|
||||
|
||||
exports[`Create input validation - TEXT Rest create input - failure TEXT - should fail with : {"textField":null} 1`] = `"["null value in column \\"textField\\" of relation \\"_apiInputValidationTestObject\\" violates not-null constraint"]"`;
|
||||
|
||||
exports[`Create input validation - TEXT Rest create input - failure TEXT - should fail with : {"textField":true} 1`] = `"["Invalid string value true for text field \\"textField\\""]"`;
|
||||
|
||||
-5
@@ -7,11 +7,6 @@ export const failingCreateInputByFieldMetadataType: {
|
||||
}[];
|
||||
} = {
|
||||
[FieldMetadataType.TEXT]: [
|
||||
{
|
||||
input: {
|
||||
textField: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
textField: {},
|
||||
|
||||
+22
-6
@@ -134,7 +134,10 @@ export const successfulCreateInputByFieldMetadataType: {
|
||||
rawJsonField: {},
|
||||
},
|
||||
validateInput: (record: Record<string, any>) => {
|
||||
return Object.keys(record.rawJsonField).length === 0;
|
||||
return (
|
||||
typeof record.rawJsonField === 'object' &&
|
||||
Object.keys(record.rawJsonField).length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -142,7 +145,10 @@ export const successfulCreateInputByFieldMetadataType: {
|
||||
rawJsonField: null,
|
||||
},
|
||||
validateInput: (record: Record<string, any>) => {
|
||||
return record.rawJsonField === null;
|
||||
return (
|
||||
typeof record.rawJsonField === 'object' &&
|
||||
Object.keys(record.rawJsonField).length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -182,7 +188,9 @@ export const successfulCreateInputByFieldMetadataType: {
|
||||
arrayField: [],
|
||||
},
|
||||
validateInput: (record: Record<string, any>) => {
|
||||
return record.arrayField.length === 0;
|
||||
return (
|
||||
Array.isArray(record.arrayField) && record.arrayField.length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -190,7 +198,9 @@ export const successfulCreateInputByFieldMetadataType: {
|
||||
arrayField: null,
|
||||
},
|
||||
validateInput: (record: Record<string, any>) => {
|
||||
return record.arrayField === null;
|
||||
return (
|
||||
Array.isArray(record.arrayField) && record.arrayField.length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -229,7 +239,10 @@ export const successfulCreateInputByFieldMetadataType: {
|
||||
multiSelectField: [],
|
||||
},
|
||||
validateInput: (record: Record<string, any>) => {
|
||||
return record.multiSelectField.length === 0;
|
||||
return (
|
||||
Array.isArray(record.multiSelectField) &&
|
||||
record.multiSelectField.length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -237,7 +250,10 @@ export const successfulCreateInputByFieldMetadataType: {
|
||||
multiSelectField: null,
|
||||
},
|
||||
validateInput: (record: Record<string, any>) => {
|
||||
return record.multiSelectField === null;
|
||||
return (
|
||||
Array.isArray(record.multiSelectField) &&
|
||||
record.multiSelectField.length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+9
-4
@@ -4,7 +4,7 @@ import {
|
||||
TEST_UUID_FIELD_VALUE,
|
||||
} from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
|
||||
|
||||
export const successfulFilterInputByFieldMetadataType: {
|
||||
[K in FieldMetadataTypesToTestForFilterInputValidation]: {
|
||||
@@ -803,7 +803,10 @@ export const successfulFilterInputByFieldMetadataType: {
|
||||
gqlFilterInput: { multiSelectField: { is: 'NULL' } },
|
||||
restFilterInput: 'multiSelectField[is]:NULL',
|
||||
validateFilter: (record: Record<string, any>) => {
|
||||
return record.multiSelectField === null;
|
||||
return (
|
||||
Array.isArray(record.multiSelectField) &&
|
||||
record.multiSelectField.length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -892,7 +895,7 @@ export const successfulFilterInputByFieldMetadataType: {
|
||||
gqlFilterInput: { rawJsonField: { is: 'NULL' } },
|
||||
restFilterInput: 'rawJsonField[is]:NULL',
|
||||
validateFilter: (record: Record<string, any>) => {
|
||||
return record.rawJsonField === null;
|
||||
return isEmptyObject(record.rawJsonField);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -923,7 +926,9 @@ export const successfulFilterInputByFieldMetadataType: {
|
||||
gqlFilterInput: { arrayField: { is: 'NULL' } },
|
||||
restFilterInput: 'arrayField[is]:NULL',
|
||||
validateFilter: (record: Record<string, any>) => {
|
||||
return record.arrayField === null;
|
||||
return (
|
||||
Array.isArray(record.arrayField) && record.arrayField.length === 0
|
||||
);
|
||||
},
|
||||
},
|
||||
//TODO - null and empty array should be equivalent
|
||||
|
||||
-1
@@ -55,7 +55,6 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TODO : Refacto-common - Uncomment this
|
||||
describe('Rest filter input - failure', () => {
|
||||
it.each(
|
||||
failingTestCases.map((testCase) => ({
|
||||
|
||||
+7
-7
@@ -40,7 +40,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -57,7 +57,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -75,7 +75,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -88,7 +88,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCountryCode: '' as CountryCode,
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -101,7 +101,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -113,7 +113,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: '' as CountryCode,
|
||||
primaryPhoneCallingCode: '',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -130,7 +130,7 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<CreatePhoneFieldMetadataTestCase
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: '' as CountryCode,
|
||||
primaryPhoneCallingCode: '',
|
||||
additionalPhones: null,
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -130,6 +130,7 @@ export { lowercaseUrlOriginAndRemoveTrailingSlash } from './url/lowercaseUrlOrig
|
||||
export { uuidToBase36 } from './uuidToBase36';
|
||||
export { assertIsDefinedOrThrow } from './validation/assertIsDefinedOrThrow';
|
||||
export { isDefined } from './validation/isDefined';
|
||||
export { isEmptyObject } from './validation/isEmptyObject';
|
||||
export { isLabelIdentifierFieldMetadataTypes } from './validation/isLabelIdentifierFieldMetadataTypes';
|
||||
export { isValidLocale } from './validation/isValidLocale';
|
||||
export { isValidUuid } from './validation/isValidUuid';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { isEmptyObject } from '../isEmptyObject';
|
||||
import { isEmptyObject } from '@/utils/validation/isEmptyObject';
|
||||
|
||||
describe('isEmptyObject', () => {
|
||||
it('should return true for empty object', () => {
|
||||
Reference in New Issue
Block a user