diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers.ts deleted file mode 100644 index 7b3b26e558a..00000000000 --- a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler'; - -export const CommonArgsHandlers = [CommonSelectedFieldsHandler]; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts new file mode 100644 index 00000000000..355d5988690 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts @@ -0,0 +1,4 @@ +import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor'; +import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory'; + +export const CommonArgsProcessors = [DataArgProcessor, QueryRunnerArgsFactory]; // TODO: Refacto-common Remove QueryRunnerArgsFactory diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts new file mode 100644 index 00000000000..0f41d7d166e --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts @@ -0,0 +1,271 @@ +import { Injectable } from '@nestjs/common'; + +import { isNull, isUndefined } from '@sniptt/guards'; +import { + FieldMetadataRelationSettings, + FieldMetadataType, + ObjectRecord, + RelationType, +} from 'twenty-shared/types'; +import { + assertIsDefinedOrThrow, + assertUnreachable, + isDefined, +} from 'twenty-shared/utils'; + +import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util'; +import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util'; +import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util'; +import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util'; +import { transformFullNameField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util'; +import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util'; +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'; +import { validateActorFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util'; +import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util'; +import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util'; +import { validateBooleanFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util'; +import { validateCurrencyFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util'; +import { validateDateAndDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util'; +import { validateEmailsFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util'; +import { validateFullNameFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util'; +import { validateLinksFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util'; +import { validateMultiSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util'; +import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util'; +import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util'; +import { validatePhonesFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util'; +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 { validateRichTextV2FieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-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, +} 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 { 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'; +import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; +import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; + +@Injectable() +export class DataArgProcessor { + constructor(private readonly recordPositionService: RecordPositionService) {} + + async process({ + partialRecordInputs, + authContext, + objectMetadataItemWithFieldMaps, + shouldBackfillPositionIfUndefined = true, + }: { + partialRecordInputs: Partial[] | undefined; + authContext: AuthContext; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + shouldBackfillPositionIfUndefined?: boolean; + }): Promise[]> { + if (!isDefined(partialRecordInputs)) { + return []; + } + + const workspace = authContext.workspace; + + assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); + + const processedRecords: Partial[] = []; + + for (const record of partialRecordInputs) { + const processedRecord: Partial = {}; + + for (const [key, value] of Object.entries(record)) { + const fieldMetadataId = + objectMetadataItemWithFieldMaps.fieldIdByName[key] || + objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key]; + + if (!isDefined(fieldMetadataId)) { + throw new CommonQueryRunnerException( + `Object ${objectMetadataItemWithFieldMaps.nameSingular} doesn't have any "${key}" field.`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + const fieldMetadata = + objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + + if ( + !isDefined(fieldMetadata.defaultValue) && + !fieldMetadata.isNullable && + isNull(value) + ) { + throw new CommonQueryRunnerException( + `Field ${key} is not nullable and has no default value.`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + if (isUndefined(value)) { + continue; + } + + processedRecord[key] = await this.processField( + fieldMetadata, + key, + value, + ); + } + processedRecords.push(processedRecord); + } + + const overriddenPositionRecords = + await this.recordPositionService.overridePositionOnRecords({ + partialRecordInputs: processedRecords, + workspaceId: workspace.id, + objectMetadata: { + isCustom: objectMetadataItemWithFieldMaps.isCustom, + nameSingular: objectMetadataItemWithFieldMaps.nameSingular, + fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName, + }, + shouldBackfillPositionIfUndefined, + }); + + return overriddenPositionRecords; + } + + private async processField( + fieldMetadata: FieldMetadataEntity, + key: string, + value: unknown, + ): Promise { + switch (fieldMetadata.type) { + case FieldMetadataType.POSITION: + return value; + case FieldMetadataType.NUMERIC: { + const validatedValue = validateNumericFieldOrThrow(value, key); + + return transformNumericField(validatedValue); + } + case FieldMetadataType.NUMBER: { + return validateNumberFieldOrThrow(value, key); + } + case FieldMetadataType.TEXT: { + const validatedValue = validateTextFieldOrThrow(value, key); + + return transformTextField(validatedValue); + } + case FieldMetadataType.DATE_TIME: + case FieldMetadataType.DATE: + return validateDateAndDateTimeFieldOrThrow(value, key); + case FieldMetadataType.BOOLEAN: + return validateBooleanFieldOrThrow(value, key); + case FieldMetadataType.RATING: + case FieldMetadataType.SELECT: { + validateRatingAndSelectFieldOrThrow( + value, + key, + fieldMetadata.options?.map((option) => option.value), + ); + + return value; + } + + case FieldMetadataType.MULTI_SELECT: { + const validatedValue = validateMultiSelectFieldOrThrow( + value, + key, + fieldMetadata.options?.map((option) => option.value), + ); + + return transformArrayField(validatedValue); + } + case FieldMetadataType.UUID: + return validateUUIDFieldOrThrow(value, key); + case FieldMetadataType.ARRAY: { + const validatedValue = validateArrayFieldOrThrow(value, key); + + return transformArrayField(validatedValue); + } + case FieldMetadataType.RAW_JSON: { + const validatedValue = validateRawJsonFieldOrThrow(value, key); + + return transformRawJsonField(validatedValue); + } + case FieldMetadataType.RELATION: + case FieldMetadataType.MORPH_RELATION: { + const fieldMetadataRelationSettings = + fieldMetadata.settings as FieldMetadataRelationSettings; + + if ( + fieldMetadataRelationSettings.relationType === + RelationType.ONE_TO_MANY + ) { + throw new CommonQueryRunnerException( + `One-to-many relation ${key} field does not support write operations.`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + if (key === fieldMetadataRelationSettings.joinColumnName) { + return validateUUIDFieldOrThrow(value, key); + } + + return value; + } + case FieldMetadataType.PHONES: { + const validatedValue = validatePhonesFieldOrThrow(value, key); + + return transformPhonesValue({ input: validatedValue }); + } + case FieldMetadataType.EMAILS: { + const validatedValue = validateEmailsFieldOrThrow(value, key); + + return transformEmailsValue(validatedValue); + } + case FieldMetadataType.FULL_NAME: { + const validatedValue = validateFullNameFieldOrThrow(value, key); + + return transformFullNameField(validatedValue); + } + + case FieldMetadataType.ADDRESS: { + const validatedValue = validateAddressFieldOrThrow(value, key); + + return transformAddressField(validatedValue); + } + case FieldMetadataType.CURRENCY: { + const validatedValue = validateCurrencyFieldOrThrow(value, key); + + return transformCurrencyField(validatedValue); + } + case FieldMetadataType.ACTOR: { + const validatedValue = validateActorFieldOrThrow(value, key); + + return transformActorField(validatedValue); + } + case FieldMetadataType.RICH_TEXT_V2: { + const validatedValue = validateRichTextV2FieldOrThrow(value, key); + + return await transformRichTextV2Value(validatedValue); + } + case FieldMetadataType.LINKS: { + const validatedValue = validateLinksFieldOrThrow(value, key); + + return transformLinksValue(validatedValue); + } + case FieldMetadataType.RICH_TEXT: + case FieldMetadataType.TS_VECTOR: + throw new CommonQueryRunnerException( + `${key} ${fieldMetadata.type}-typed field does not support write operations`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + default: + assertUnreachable( + fieldMetadata.type, + 'Should never occur, add validator for new field type', + ); + } + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-actor-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-actor-field.util.spec.ts new file mode 100644 index 00000000000..20bbfc6b05b --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-actor-field.util.spec.ts @@ -0,0 +1,84 @@ +import { FieldActorSource } from 'twenty-shared/types'; + +import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util'; + +describe('transformActorField', () => { + it('should return null when value is null', () => { + const result = transformActorField(null, true); + + expect(result).toBeNull(); + }); + + it('should transform actor with source only', () => { + const result = transformActorField( + { + source: FieldActorSource.EMAIL, + }, + true, + ); + + expect(result).toEqual({ + source: FieldActorSource.EMAIL, + }); + }); + + it('should transform actor with source and context', () => { + const result = transformActorField( + { + source: FieldActorSource.WORKFLOW, + context: { workflowId: '123', stepId: 'step-1' }, + }, + true, + ); + + expect(result).toEqual({ + source: FieldActorSource.WORKFLOW, + context: { workflowId: '123', stepId: 'step-1' }, + }); + }); + + it('should transform actor with null source', () => { + const result = transformActorField( + { + source: null, + context: { userId: '456' }, + }, + true, + ); + + expect(result).toEqual({ + source: null, + context: { userId: '456' }, + }); + }); + + it('should transform actor with null context', () => { + const result = transformActorField( + { + source: FieldActorSource.API, + context: null, + }, + true, + ); + + expect(result).toEqual({ + source: FieldActorSource.API, + context: null, + }); + }); + + it('should transform empty context object to null', () => { + const result = transformActorField( + { + source: FieldActorSource.EMAIL, + context: {}, + }, + true, + ); + + expect(result).toEqual({ + source: FieldActorSource.EMAIL, + context: null, + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-address-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-address-field.util.spec.ts new file mode 100644 index 00000000000..6366a5f3864 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-address-field.util.spec.ts @@ -0,0 +1,45 @@ +import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util'; + +describe('transformAddressField', () => { + it('should return null when value is null', () => { + const result = transformAddressField(null); + + expect(result).toBeNull(); + }); + + it('should return an empty object when value is an empty object', () => { + const result = transformAddressField({}); + + expect(result).toEqual({}); + }); + + it('should preserve undefined for fields that are not provided', () => { + const result = transformAddressField({ + addressStreet1: '123 Main St', + addressCity: 'San Francisco', + }); + + expect(result).toEqual({ + addressStreet1: '123 Main St', + addressCity: 'San Francisco', + }); + }); + + it('should handle mixed null, undefined, and valid values', () => { + const result = transformAddressField({ + addressStreet1: '123 Main St', + addressStreet2: null, + addressCity: 'San Francisco', + addressLat: 37.7749, + addressLng: null, + }); + + expect(result).toEqual({ + addressStreet1: '123 Main St', + addressStreet2: null, + addressCity: 'San Francisco', + addressLat: 37.7749, + addressLng: null, + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-array-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-array-field.util.spec.ts new file mode 100644 index 00000000000..b1400222343 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-array-field.util.spec.ts @@ -0,0 +1,27 @@ +import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util'; + +describe('transformArrayField', () => { + it('should return null when value is null', () => { + const result = transformArrayField(null, true); + + expect(result).toBeNull(); + }); + + it('should return null when value is an empty array', () => { + const result = transformArrayField([], true); + + expect(result).toBeNull(); + }); + + it('should return an array when value is a string', () => { + const result = transformArrayField('singleString', true); + + expect(result).toEqual(['singleString']); + }); + + it('should return an array when value is an array of strings', () => { + const result = transformArrayField(['string1', 'string2', 'string3'], true); + + expect(result).toEqual(['string1', 'string2', 'string3']); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-currency-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-currency-field.util.spec.ts new file mode 100644 index 00000000000..b577065803e --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-currency-field.util.spec.ts @@ -0,0 +1,54 @@ +import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util'; + +describe('transformCurrencyField', () => { + it('should return null when value is null', () => { + const result = transformCurrencyField(null); + + expect(result).toBeNull(); + }); + + it('should return empty object when value is empty object', () => { + const result = transformCurrencyField({}); + + expect(result).toEqual({}); + }); + + it('should transform amountMicros from number', () => { + const result = transformCurrencyField({ amountMicros: 1000 }); + + expect(result).toEqual({ amountMicros: 1000 }); + }); + + it('should transform amountMicros from string to number', () => { + const result = transformCurrencyField({ amountMicros: '1000' }); + + expect(result).toEqual({ amountMicros: 1000 }); + }); + + it('should transform currencyCode', () => { + const result = transformCurrencyField({ currencyCode: 'USD' }); + + expect(result).toEqual({ currencyCode: 'USD' }); + }); + + it('should transform both amountMicros and currencyCode', () => { + const result = transformCurrencyField({ + amountMicros: '2500', + currencyCode: 'EUR', + }); + + expect(result).toEqual({ amountMicros: 2500, currencyCode: 'EUR' }); + }); + + it('should handle null amountMicros', () => { + const result = transformCurrencyField({ amountMicros: null }); + + expect(result).toEqual({ amountMicros: null }); + }); + + it('should handle null currencyCode', () => { + const result = transformCurrencyField({ currencyCode: null }); + + expect(result).toEqual({ currencyCode: null }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-full-name-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-full-name-field.util.spec.ts new file mode 100644 index 00000000000..2f2716509b6 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-full-name-field.util.spec.ts @@ -0,0 +1,33 @@ +import { transformFullNameField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util'; + +describe('transformFullNameField', () => { + it('should return null when value is null', () => { + const result = transformFullNameField(null, true); + + expect(result).toBeNull(); + }); + + it('should return full name object with both fields', () => { + const value = { + firstName: 'John', + lastName: 'Doe', + }; + const result = transformFullNameField(value, true); + + expect(result).toEqual({ + firstName: 'John', + lastName: 'Doe', + }); + }); + + it('should return full name object with only lastName', () => { + const value = { + lastName: '', + }; + const result = transformFullNameField(value, true); + + expect(result).toEqual({ + lastName: null, + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-numeric-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-numeric-field.util.spec.ts new file mode 100644 index 00000000000..8541b968905 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-numeric-field.util.spec.ts @@ -0,0 +1,21 @@ +import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util'; + +describe('transformNumericField', () => { + it('should return null when value is null', () => { + const result = transformNumericField(null); + + expect(result).toBeNull(); + }); + + it('should return the number when value is a float', () => { + const result = transformNumericField(3.14159); + + expect(result).toBe(3.14159); + }); + + it('should transform a numeric string with decimals to a number', () => { + const result = transformNumericField('123.456'); + + expect(result).toBe(123.456); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-raw-json-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-raw-json-field.util.spec.ts new file mode 100644 index 00000000000..600c2a63aa0 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-raw-json-field.util.spec.ts @@ -0,0 +1,21 @@ +import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util'; + +describe('transformRawJsonField', () => { + it('should return null when value is null', () => { + const result = transformRawJsonField(null, true); + + expect(result).toBeNull(); + }); + + it('should return null when value is empty object', () => { + const result = transformRawJsonField({}, true); + + expect(result).toBeNull(); + }); + + it('should return the string when value is empty array', () => { + const result = transformRawJsonField([], true); + + expect(result).toBeNull(); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-text-field.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-text-field.util.spec.ts new file mode 100644 index 00000000000..cfdffb27839 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/__tests__/transform-text-field.util.spec.ts @@ -0,0 +1,21 @@ +import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util'; + +describe('transformTextField', () => { + it('should return null when value is null', () => { + const result = transformTextField(null, true); + + expect(result).toBeNull(); + }); + + it('should return null when value is empty string', () => { + const result = transformTextField('', true); + + expect(result).toBeNull(); + }); + + it('should return the string when value is a non-empty string', () => { + const result = transformTextField('hello world', true); + + expect(result).toBe('hello world'); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util.ts new file mode 100644 index 00000000000..11e9e3b4afa --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util.ts @@ -0,0 +1,24 @@ +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'; + +export const transformActorField = ( + value: { + source?: FieldActorSource | null; + context?: object | null; + } | null, + isNullEquivalenceEnabled: boolean = false, +): { + source?: FieldActorSource | null; + context?: object | null; +} | null => { + if (isNull(value)) return null; + + return { + source: value.source, + context: isUndefined(value.context) + ? undefined + : transformRawJsonField(value.context, isNullEquivalenceEnabled), + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util.ts new file mode 100644 index 00000000000..537f6d148bb --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util.ts @@ -0,0 +1,56 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util'; +import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util'; + +export const transformAddressField = ( + value: { + addressStreet1?: string | null; + addressStreet2?: string | null; + addressCity?: string | null; + addressState?: string | null; + addressPostcode?: string | null; + addressCountry?: string | null; + addressLat?: number | null; + addressLng?: number | null; + } | null, + isNullEquivalenceEnabled: boolean = false, +): { + addressStreet1?: string | null; + addressStreet2?: string | null; + addressCity?: string | null; + addressState?: string | null; + addressPostcode?: string | null; + addressCountry?: string | null; + addressLat?: number | null; + addressLng?: number | null; +} | null => { + if (isNull(value)) return null; + + return { + addressStreet1: isUndefined(value.addressStreet1) + ? undefined + : transformTextField(value.addressStreet1, isNullEquivalenceEnabled), + addressStreet2: isUndefined(value.addressStreet2) + ? undefined + : transformTextField(value.addressStreet2, isNullEquivalenceEnabled), + addressCity: isUndefined(value.addressCity) + ? undefined + : transformTextField(value.addressCity, isNullEquivalenceEnabled), + addressState: isUndefined(value.addressState) + ? undefined + : transformTextField(value.addressState, isNullEquivalenceEnabled), + addressPostcode: isUndefined(value.addressPostcode) + ? undefined + : transformTextField(value.addressPostcode, isNullEquivalenceEnabled), + addressCountry: isUndefined(value.addressCountry) + ? undefined + : transformTextField(value.addressCountry, isNullEquivalenceEnabled), + addressLat: isUndefined(value.addressLat) + ? undefined + : transformNumericField(value.addressLat), + addressLng: isUndefined(value.addressLng) + ? undefined + : transformNumericField(value.addressLng), + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util.ts new file mode 100644 index 00000000000..47c0d34672f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util.ts @@ -0,0 +1,14 @@ +import { isNull } from '@sniptt/guards'; + +export const transformArrayField = ( + value: string | string[] | null, + isNullEquivalenceEnabled: boolean = false, +): string[] | null => { + if (typeof value === 'string') return [value]; + + return isNullEquivalenceEnabled && + !isNull(value) && + Object.keys(value).length === 0 + ? null + : value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util.ts new file mode 100644 index 00000000000..7481f4b49d1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util.ts @@ -0,0 +1,26 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util'; +import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util'; + +export const transformCurrencyField = ( + value: { + amountMicros?: number | string | null; + currencyCode?: string | null; + } | null, + isNullEquivalenceEnabled: boolean = false, +): { + amountMicros?: number | null; + currencyCode?: string | null; +} | null => { + if (isNull(value)) return null; + + return { + amountMicros: isUndefined(value.amountMicros) + ? undefined + : transformNumericField(value.amountMicros), + currencyCode: isUndefined(value.currencyCode) + ? undefined + : transformTextField(value.currencyCode, isNullEquivalenceEnabled), + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util.ts new file mode 100644 index 00000000000..90dec4d07eb --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util.ts @@ -0,0 +1,25 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util'; + +export const transformFullNameField = ( + value: { + firstName?: string | null; + lastName?: string | null; + } | null, + isNullEquivalenceEnabled: boolean = false, +): { + firstName?: string | null; + lastName?: string | null; +} | null => { + if (isNull(value)) return null; + + return { + firstName: isUndefined(value.firstName) + ? undefined + : transformTextField(value.firstName, isNullEquivalenceEnabled), + lastName: isUndefined(value.lastName) + ? undefined + : transformTextField(value.lastName, isNullEquivalenceEnabled), + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util.ts new file mode 100644 index 00000000000..598990df689 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util.ts @@ -0,0 +1,7 @@ +import { isNull } from '@sniptt/guards'; + +export const transformNumericField = ( + value: number | string | null, +): number | null => { + return isNull(value) ? null : Number(value); +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util.ts new file mode 100644 index 00000000000..804e038b72e --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util.ts @@ -0,0 +1,12 @@ +import { isNull } from '@sniptt/guards'; + +export const transformRawJsonField = ( + value: object | null, + isNullEquivalenceEnabled: boolean = false, +): object | null => { + return isNullEquivalenceEnabled && + !isNull(value) && + Object.keys(value).length === 0 + ? null + : value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util.ts new file mode 100644 index 00000000000..059bd34e0a8 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util.ts @@ -0,0 +1,8 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +export const transformTextField = ( + value: string | null, + isNullEquivalenceEnabled: boolean = false, +): string | null => { + return isNullEquivalenceEnabled && !isNonEmptyString(value) ? null : value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-actor-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-actor-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..0c2f86754d8 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-actor-field-or-throw.util.spec.ts @@ -0,0 +1,84 @@ +import { FieldActorSource } from 'twenty-shared/types'; + +import { validateActorFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateActorFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateActorFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return valid actor object with source and context', () => { + const validActor = { + source: FieldActorSource.EMAIL, + context: { userId: '123', email: 'test@example.com' }, + }; + + const result = validateActorFieldOrThrow(validActor, 'testField'); + + expect(result).toEqual(validActor); + }); + + it('should accept empty context object', () => { + const validActor = { + source: FieldActorSource.EMAIL, + context: {}, + }; + + const result = validateActorFieldOrThrow(validActor, 'testField'); + + expect(result).toEqual(validActor); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => validateActorFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a string', () => { + expect(() => validateActorFieldOrThrow('invalid', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when source is invalid', () => { + const invalidActor = { + source: 'INVALID_SOURCE', + context: {}, + }; + + expect(() => + validateActorFieldOrThrow(invalidActor, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when context is a string', () => { + const invalidActor = { + source: FieldActorSource.EMAIL, + context: 'invalid', + }; + + expect(() => + validateActorFieldOrThrow(invalidActor, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when actor has invalid subfield', () => { + const invalidActor = { + source: FieldActorSource.EMAIL, + context: {}, + invalidField: 'invalid', + }; + + expect(() => + validateActorFieldOrThrow(invalidActor, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-address-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-address-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..da2c4b6c5dd --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-address-field-or-throw.util.spec.ts @@ -0,0 +1,159 @@ +import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateAddressFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateAddressFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return valid address object with all text fields', () => { + const value = { + addressStreet1: '123 Main St', + addressStreet2: 'Apt 4B', + addressCity: 'New York', + addressState: 'NY', + addressPostcode: '10001', + addressCountry: 'USA', + }; + const result = validateAddressFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + + it('should return valid address object with coordinates', () => { + const value = { + addressStreet1: '123 Main St', + addressCity: 'New York', + addressLat: 40.7128, + addressLng: -74.006, + }; + const result = validateAddressFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + + it('should return valid address object with null subfields', () => { + const value = { + addressStreet1: '123 Main St', + addressStreet2: null, + addressCity: 'New York', + addressState: null, + addressPostcode: null, + addressCountry: null, + addressLat: null, + addressLng: null, + }; + const result = validateAddressFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is not an object', () => { + expect(() => validateAddressFieldOrThrow('invalid', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an array', () => { + expect(() => + validateAddressFieldOrThrow(['123 Main St'], 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when addressStreet1 is not a string or number', () => { + const value = { + addressStreet1: { invalid: 'object' }, + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressStreet2 is not a string', () => { + const value = { + addressStreet2: 123, + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressCity is not a string', () => { + const value = { + addressCity: true, + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressState is not a string', () => { + const value = { + addressState: ['NY'], + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressPostcode is not a string', () => { + const value = { + addressPostcode: 10001, + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressCountry is not a string', () => { + const value = { + addressCountry: { code: 'US' }, + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressLat is not a number', () => { + const value = { + addressLat: 'not a number', + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when addressLng is not a number', () => { + const value = { + addressLng: 'not a number', + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when an invalid subfield is provided', () => { + const value = { + addressStreet1: '123 Main St', + invalidSubField: 'invalid', + }; + + expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-array-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-array-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..7766d612104 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-array-field-or-throw.util.spec.ts @@ -0,0 +1,55 @@ +import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateArrayFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateArrayFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the string when value is a string', () => { + const result = validateArrayFieldOrThrow('singleString', 'testField'); + + expect(result).toBe('singleString'); + }); + + it('should return the array when value is an empty array', () => { + const result = validateArrayFieldOrThrow([], 'testField'); + + expect(result).toEqual([]); + }); + + it('should return the array when value is an array of strings', () => { + const stringArray = ['string1', 'string2', 'string3']; + const result = validateArrayFieldOrThrow(stringArray, 'testField'); + + expect(result).toEqual(stringArray); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is a number', () => { + expect(() => validateArrayFieldOrThrow(123, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an object', () => { + const objectValue = { key: 'value' }; + + expect(() => validateArrayFieldOrThrow(objectValue, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an array containing objects', () => { + const arrayWithObjects = ['string1', { key: 'value' }, 'string2']; + + expect(() => + validateArrayFieldOrThrow(arrayWithObjects, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-boolean-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-boolean-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..078aad3b847 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-boolean-field-or-throw.util.spec.ts @@ -0,0 +1,70 @@ +import { validateBooleanFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateBooleanFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateBooleanFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return true when value is true', () => { + const result = validateBooleanFieldOrThrow(true, 'testField'); + + expect(result).toBe(true); + }); + + it('should return false when value is false', () => { + const result = validateBooleanFieldOrThrow(false, 'testField'); + + expect(result).toBe(false); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => validateBooleanFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a string "true"', () => { + expect(() => validateBooleanFieldOrThrow('true', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an empty string', () => { + expect(() => validateBooleanFieldOrThrow('', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a number 1', () => { + expect(() => validateBooleanFieldOrThrow(1, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a number 0', () => { + expect(() => validateBooleanFieldOrThrow(0, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an object', () => { + expect(() => validateBooleanFieldOrThrow({}, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a function', () => { + const functionValue = () => true; + + expect(() => + validateBooleanFieldOrThrow(functionValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-currency-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-currency-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..32cea3a0846 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-currency-field-or-throw.util.spec.ts @@ -0,0 +1,73 @@ +import { validateCurrencyFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateCurrencyFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateCurrencyFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the currency object when both amountMicros and currencyCode are valid', () => { + const currencyValue = { + amountMicros: 1000000, + currencyCode: 'USD', + }; + const result = validateCurrencyFieldOrThrow(currencyValue, 'testField'); + + expect(result).toEqual(currencyValue); + }); + + it('should return the currency object when only amountMicros is provided', () => { + const currencyValue = { + amountMicros: 5000000, + }; + const result = validateCurrencyFieldOrThrow(currencyValue, 'testField'); + + expect(result).toEqual(currencyValue); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is not an object', () => { + expect(() => + validateCurrencyFieldOrThrow('not an object', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when amountMicros is not a valid numeric value', () => { + const currencyValue = { + amountMicros: 'not a number', + currencyCode: 'USD', + }; + + expect(() => + validateCurrencyFieldOrThrow(currencyValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when currencyCode is not a string', () => { + const currencyValue = { + amountMicros: 1000000, + currencyCode: 123, + }; + + expect(() => + validateCurrencyFieldOrThrow(currencyValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when an invalid subfield is present', () => { + const currencyValue = { + amountMicros: 1000000, + currencyCode: 'USD', + invalidField: 'invalid', + }; + + expect(() => + validateCurrencyFieldOrThrow(currencyValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-and-date-time-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-and-date-time-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..da4a9f05436 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-and-date-time-field-or-throw.util.spec.ts @@ -0,0 +1,97 @@ +import { validateDateAndDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateDateAndDateTimeFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateDateAndDateTimeFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the value when it is a valid ISO date string', () => { + const dateString = '2024-01-15'; + const result = validateDateAndDateTimeFieldOrThrow( + dateString, + 'testField', + ); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid ISO datetime string', () => { + const datetimeString = '2024-01-15T10:30:00Z'; + const result = validateDateAndDateTimeFieldOrThrow( + datetimeString, + 'testField', + ); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a Date object', () => { + const dateObject = new Date('2024-01-15'); + const result = validateDateAndDateTimeFieldOrThrow( + dateObject, + 'testField', + ); + + expect(result).toBe(dateObject); + }); + + it('should return the value when it is a timestamp number', () => { + const timestamp = Date.now(); + const result = validateDateAndDateTimeFieldOrThrow( + timestamp, + 'testField', + ); + + expect(result).toBe(timestamp); + }); + + it('should return the value when it is a Date', () => { + const date = new Date(); + const result = validateDateAndDateTimeFieldOrThrow(date, 'testField'); + + expect(result).toBe(date); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is an invalid date string', () => { + expect(() => + validateDateAndDateTimeFieldOrThrow('invalid-date', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an empty string', () => { + expect(() => + validateDateAndDateTimeFieldOrThrow('', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a boolean', () => { + expect(() => + validateDateAndDateTimeFieldOrThrow(true, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an array', () => { + expect(() => + validateDateAndDateTimeFieldOrThrow([], 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an object', () => { + expect(() => + validateDateAndDateTimeFieldOrThrow({}, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is undefined', () => { + expect(() => + validateDateAndDateTimeFieldOrThrow(undefined, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-emails-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-emails-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..0802b8aafd9 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-emails-field-or-throw.util.spec.ts @@ -0,0 +1,87 @@ +import { validateEmailsFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateEmailsFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateEmailsFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the emails object when all fields are valid', () => { + const emailsValue = { + primaryEmail: 'primary@example.com', + additionalEmails: ['secondary1@example.com', 'secondary2@example.com'], + }; + const result = validateEmailsFieldOrThrow(emailsValue, 'testField'); + + expect(result).toEqual(emailsValue); + }); + + it('should return the emails object when only primaryEmail is provided', () => { + const emailsValue = { + primaryEmail: 'primary@example.com', + }; + const result = validateEmailsFieldOrThrow(emailsValue, 'testField'); + + expect(result).toEqual(emailsValue); + }); + + it('should accept empty additionalEmails array', () => { + const emailsValue = { + primaryEmail: 'primary@example.com', + additionalEmails: [], + }; + const result = validateEmailsFieldOrThrow(emailsValue, 'testField'); + + expect(result).toEqual(emailsValue); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is not an object', () => { + expect(() => + validateEmailsFieldOrThrow('not an object', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is undefined', () => { + expect(() => validateEmailsFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when primaryEmail is not a string', () => { + const emailsValue = { + primaryEmail: 12345, + }; + + expect(() => + validateEmailsFieldOrThrow(emailsValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when additionalEmails is not an array', () => { + const emailsValue = { + additionalEmails: { key: 'not an array' }, + }; + + expect(() => + validateEmailsFieldOrThrow(emailsValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when invalid subfields are present', () => { + const emailsValue = { + primaryEmail: 'primary@example.com', + invalidField1: 'invalid', + invalidField2: 'invalid', + }; + + expect(() => + validateEmailsFieldOrThrow(emailsValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-full-name-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-full-name-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..ae4abd877ea --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-full-name-field-or-throw.util.spec.ts @@ -0,0 +1,70 @@ +import { validateFullNameFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateFullNameFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateFullNameFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return valid full name object with both fields', () => { + const value = { + firstName: 'John', + lastName: 'Doe', + }; + const result = validateFullNameFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + + it('should return valid full name object with only firstName', () => { + const value = { + firstName: 'John', + }; + const result = validateFullNameFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + + it('should return valid full name object with only lastName', () => { + const value = { + lastName: 'Doe', + }; + const result = validateFullNameFieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is not an object', () => { + expect(() => + validateFullNameFieldOrThrow('invalid', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when firstName is not a string', () => { + const value = { + firstName: { invalid: 'object' }, + }; + + expect(() => validateFullNameFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when an invalid subfield is provided', () => { + const value = { + firstName: 'John', + lastName: 'Doe', + invalidSubField: 'invalid', + }; + + expect(() => validateFullNameFieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-links-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-links-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..6e1b6a202d6 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-links-field-or-throw.util.spec.ts @@ -0,0 +1,88 @@ +import { validateLinksFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateLinksFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateLinksFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the links object when all fields are valid', () => { + const linksValue = { + primaryLinkUrl: 'https://example.com', + primaryLinkLabel: 'Example Website', + secondaryLinks: [{ url: 'https://secondary.com', label: 'Secondary' }], + }; + const result = validateLinksFieldOrThrow(linksValue, 'testField'); + + expect(result).toEqual(linksValue); + }); + + it('should return the links object when only primaryLinkUrl is provided', () => { + const linksValue = { + primaryLinkUrl: 'https://example.com', + }; + const result = validateLinksFieldOrThrow(linksValue, 'testField'); + + expect(result).toEqual(linksValue); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is not an object', () => { + expect(() => + validateLinksFieldOrThrow('not an object', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is undefined', () => { + expect(() => validateLinksFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when primaryLinkUrl is not a string', () => { + const linksValue = { + primaryLinkUrl: 12345, + }; + + expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when primaryLinkLabel is not a string', () => { + const linksValue = { + primaryLinkLabel: ['not', 'a', 'string'], + }; + + expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when secondaryLinks is not an object or null', () => { + const linksValue = { + secondaryLinks: 'not an object', + }; + + expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when invalid subfields are present', () => { + const linksValue = { + primaryLinkUrl: 'https://example.com', + invalidField1: 'invalid', + invalidField2: 'invalid', + }; + + expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-multi-select-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-multi-select-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..24ba546be09 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-multi-select-field-or-throw.util.spec.ts @@ -0,0 +1,73 @@ +import { validateMultiSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateMultiSelectFieldOrThrow', () => { + const validOptions = ['option1', 'option2', 'option3']; + + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateMultiSelectFieldOrThrow( + null, + 'testField', + validOptions, + ); + + expect(result).toBeNull(); + }); + + it('should return array when all values are in options', () => { + const value = ['option1', 'option2']; + const result = validateMultiSelectFieldOrThrow( + value, + 'testField', + validOptions, + ); + + expect(result).toEqual(value); + }); + + it('should return string when value is a single string in options', () => { + const value = 'option1'; + const result = validateMultiSelectFieldOrThrow( + value, + 'testField', + validOptions, + ); + + expect(result).toEqual(value); + }); + + it('should return empty array when value is empty array', () => { + const value: string[] = []; + const result = validateMultiSelectFieldOrThrow( + value, + 'testField', + validOptions, + ); + + expect(result).toEqual(value); + }); + }); + + describe('invalid inputs', () => { + it('should throw when options are undefined', () => { + expect(() => + validateMultiSelectFieldOrThrow(['option1'], 'testField', undefined), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when options are not defined', () => { + expect(() => + validateMultiSelectFieldOrThrow(['option1'], 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value contains option not in the options list', () => { + const value = ['option1', 'invalidOption']; + + expect(() => + validateMultiSelectFieldOrThrow(value, 'testField', validOptions), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-number-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-number-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..a063a41b6b7 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-number-field-or-throw.util.spec.ts @@ -0,0 +1,87 @@ +import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util'; + +describe('validateNumberFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateNumberFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the number when value is a positive integer', () => { + const result = validateNumberFieldOrThrow(123, 'testField'); + + expect(result).toBe(123); + }); + + it('should return the number when value is a negative integer', () => { + const result = validateNumberFieldOrThrow(-456, 'testField'); + + expect(result).toBe(-456); + }); + + it('should return the number when value is a positive float', () => { + const result = validateNumberFieldOrThrow(123.45, 'testField'); + + expect(result).toBe(123.45); + }); + + it('should return the number when value is zero', () => { + const result = validateNumberFieldOrThrow(0, 'testField'); + + expect(result).toBe(0); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => validateNumberFieldOrThrow(undefined, 'testField')).toThrow( + 'Invalid number value undefined for field "testField"', + ); + }); + + it('should throw when value is a string with a number', () => { + expect(() => validateNumberFieldOrThrow('123', 'testField')).toThrow( + 'Invalid number value \'123\' for field "testField"', + ); + }); + + it('should throw when value is an empty string', () => { + expect(() => validateNumberFieldOrThrow('', 'testField')).toThrow( + 'Invalid number value \'\' for field "testField"', + ); + }); + + it('should throw when value is a boolean (true)', () => { + expect(() => validateNumberFieldOrThrow(true, 'testField')).toThrow( + 'Invalid number value true for field "testField"', + ); + }); + + it('should throw when value is a boolean (false)', () => { + expect(() => validateNumberFieldOrThrow(false, 'testField')).toThrow( + 'Invalid number value false for field "testField"', + ); + }); + + it('should throw when value is an array', () => { + expect(() => validateNumberFieldOrThrow([1, 2, 3], 'testField')).toThrow( + 'Invalid number value [ 1, 2, 3 ] for field "testField"', + ); + }); + + it('should throw when value is an object', () => { + expect(() => + validateNumberFieldOrThrow({ key: 'value' }, 'testField'), + ).toThrow( + 'Invalid number value { key: \'value\' } for field "testField"', + ); + }); + + it('should throw when value is NaN', () => { + expect(() => validateNumberFieldOrThrow(NaN, 'testField')).toThrow( + 'Invalid number value NaN for field "testField"', + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-numeric-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-numeric-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..e2bd82a5c59 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-numeric-field-or-throw.util.spec.ts @@ -0,0 +1,49 @@ +import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateNumericFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateNumericFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return null when value is an empty string', () => { + const result = validateNumericFieldOrThrow('', 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the number when value is a float', () => { + const result = validateNumericFieldOrThrow(3.14159, 'testField'); + + expect(result).toBe(3.14159); + }); + }); + describe('invalid inputs', () => { + it('should throw when value is NaN', () => { + expect(() => validateNumericFieldOrThrow(NaN, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a non-numeric string', () => { + expect(() => + validateNumericFieldOrThrow('not a number', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is undefined', () => { + expect(() => validateNumericFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an array', () => { + expect(() => validateNumericFieldOrThrow([1, 2, 3], 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-phones-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-phones-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..745f5d0c762 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-phones-field-or-throw.util.spec.ts @@ -0,0 +1,88 @@ +import { validatePhonesFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validatePhonesFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validatePhonesFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the phones object when all fields are valid', () => { + const phonesValue = { + primaryPhoneNumber: '+1234567890', + primaryPhoneCountryCode: 'US', + primaryPhoneCallingCode: '+1', + additionalPhones: null, + }; + const result = validatePhonesFieldOrThrow(phonesValue, 'testField'); + + expect(result).toEqual(phonesValue); + }); + + it('should return the phones object when only primaryPhoneNumber is provided', () => { + const phonesValue = { + primaryPhoneNumber: '+1234567890', + }; + const result = validatePhonesFieldOrThrow(phonesValue, 'testField'); + + expect(result).toEqual(phonesValue); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is not an object', () => { + expect(() => + validatePhonesFieldOrThrow('not an object', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is undefined', () => { + expect(() => validatePhonesFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when primaryPhoneNumber is not a string', () => { + const phonesValue = { + primaryPhoneNumber: 123456, + }; + + expect(() => + validatePhonesFieldOrThrow(phonesValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when primaryPhoneCountryCode is not a string', () => { + const phonesValue = { + primaryPhoneCountryCode: 123, + }; + + expect(() => + validatePhonesFieldOrThrow(phonesValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when primaryPhoneCallingCode is not a string', () => { + const phonesValue = { + primaryPhoneCallingCode: 1, + }; + + expect(() => + validatePhonesFieldOrThrow(phonesValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when an invalid subfield is present', () => { + const phonesValue = { + primaryPhoneNumber: '+1234567890', + invalidField: 'invalid', + }; + + expect(() => + validatePhonesFieldOrThrow(phonesValue, 'testField'), + ).toThrow('Invalid subfield invalidField for phones field "testField"'); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rating-and-select-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rating-and-select-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..cab559bb3fa --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rating-and-select-field-or-throw.util.spec.ts @@ -0,0 +1,51 @@ +import { validateRatingAndSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util'; + +describe('validateRatingAndSelectFieldOrThrow', () => { + const validOptions = ['option1', 'option2', 'option3']; + + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateRatingAndSelectFieldOrThrow( + null, + 'testField', + validOptions, + ); + + expect(result).toBeNull(); + }); + + it('should return the string when value is a valid option', () => { + const result = validateRatingAndSelectFieldOrThrow( + 'option1', + 'testField', + validOptions, + ); + + expect(result).toBe('option1'); + }); + }); + + describe('invalid inputs', () => { + it('should throw when options is undefined', () => { + expect(() => + validateRatingAndSelectFieldOrThrow('option1', 'testField', undefined), + ).toThrow('Invalid options for field "testField"'); + }); + + it('should throw when value is not in the options list', () => { + expect(() => + validateRatingAndSelectFieldOrThrow( + 'invalidOption', + 'testField', + validOptions, + ), + ).toThrow('Invalid value \'invalidOption\' for field "testField"'); + }); + + it('should throw when value is a number', () => { + expect(() => + validateRatingAndSelectFieldOrThrow(123, 'testField', validOptions), + ).toThrow('Invalid string value 123 for text field "testField"'); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-raw-json-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-raw-json-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..8ace0f960ef --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-raw-json-field-or-throw.util.spec.ts @@ -0,0 +1,60 @@ +import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateRawJsonFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateRawJsonFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return empty object when value is an empty object', () => { + const result = validateRawJsonFieldOrThrow({}, 'testField'); + + expect(result).toEqual({}); + }); + + it('should return the value when it is a valid JSON object', () => { + const jsonObject = { key: 'value', nested: { prop: 123 } }; + const result = validateRawJsonFieldOrThrow(jsonObject, 'testField'); + + expect(result).toEqual(jsonObject); + }); + + it('should return the value when it is a valid JSON array', () => { + const jsonArray = [1, 2, 3, 'test', { key: 'value' }]; + const result = validateRawJsonFieldOrThrow(jsonArray, 'testField'); + + expect(result).toEqual(jsonArray); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => validateRawJsonFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a function', () => { + const functionValue = () => 'test'; + + expect(() => + validateRawJsonFieldOrThrow(functionValue, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a number', () => { + expect(() => validateRawJsonFieldOrThrow(42, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a string', () => { + expect(() => + validateRawJsonFieldOrThrow('string value', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-v2-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-v2-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..008aed8c336 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-rich-text-v2-field-or-throw.util.spec.ts @@ -0,0 +1,53 @@ +import { validateRichTextV2FieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateRichTextV2FieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateRichTextV2FieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return null when value is an empty object', () => { + const result = validateRichTextV2FieldOrThrow({}, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the value when it has valid subfields', () => { + const value = { + blocknote: 'some blocknote content', + markdown: '# Heading\nContent', + }; + const result = validateRichTextV2FieldOrThrow(value, 'testField'); + + expect(result).toEqual(value); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => + validateRichTextV2FieldOrThrow(undefined, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a string', () => { + expect(() => + validateRichTextV2FieldOrThrow('not an object', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value has invalid subfields', () => { + const value = { invalidField: 'value' }; + + expect(() => validateRichTextV2FieldOrThrow(value, 'testField')).toThrow( + CommonQueryRunnerException, + ); + expect(() => validateRichTextV2FieldOrThrow(value, 'testField')).toThrow( + /Should have only blocknote, markdown subfields/, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..f5f4ffef8fb --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts @@ -0,0 +1,69 @@ +import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util'; + +describe('validateTextFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateTextFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return empty string when value is an empty string', () => { + const result = validateTextFieldOrThrow('', 'testField'); + + expect(result).toEqual(''); + }); + + it('should return the string when value is a regular string', () => { + const result = validateTextFieldOrThrow('hello world', 'testField'); + + expect(result).toBe('hello world'); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => validateTextFieldOrThrow(undefined, 'testField')).toThrow( + 'Invalid string value undefined for text field "testField"', + ); + }); + + it('should throw when value is a number', () => { + expect(() => validateTextFieldOrThrow(123, 'testField')).toThrow( + 'Invalid string value 123 for text field "testField"', + ); + }); + + it('should throw when value is a float number', () => { + expect(() => validateTextFieldOrThrow(123.45, 'testField')).toThrow( + 'Invalid string value 123.45 for text field "testField"', + ); + }); + + it('should throw when value is a boolean (true)', () => { + expect(() => validateTextFieldOrThrow(true, 'testField')).toThrow( + 'Invalid string value true for text field "testField"', + ); + }); + + it('should throw when value is a boolean (false)', () => { + expect(() => validateTextFieldOrThrow(false, 'testField')).toThrow( + 'Invalid string value false for text field "testField"', + ); + }); + + it('should throw when value is an array', () => { + expect(() => validateTextFieldOrThrow([1, 2, 3], 'testField')).toThrow( + 'Invalid string value [ 1, 2, 3 ] for text field "testField"', + ); + }); + + it('should throw when value is an object', () => { + expect(() => + validateTextFieldOrThrow({ key: 'value' }, 'testField'), + ).toThrow( + 'Invalid string value { key: \'value\' } for text field "testField"', + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-uuid-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-uuid-field-or-throw.util.spec.ts new file mode 100644 index 00000000000..aff37b6296e --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-uuid-field-or-throw.util.spec.ts @@ -0,0 +1,39 @@ +import { validateUUIDFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateUUIDFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateUUIDFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the value when it is a valid UUID v4', () => { + const validUuid = '550e8400-e29b-41d4-a716-446655440000'; + const result = validateUUIDFieldOrThrow(validUuid, 'testField'); + + expect(result).toBe(validUuid); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is undefined', () => { + expect(() => validateUUIDFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an empty string', () => { + expect(() => validateUUIDFieldOrThrow('', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an invalid UUID format', () => { + expect(() => + validateUUIDFieldOrThrow('invalid-uuid', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts new file mode 100644 index 00000000000..b4d63aab5df --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util.ts @@ -0,0 +1,43 @@ +import { isNull } from '@sniptt/guards'; +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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateActorFieldOrThrow = ( + value: unknown, + fieldName: string, +): { source: FieldActorSource; context: Record } | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'source': + validateRatingAndSelectFieldOrThrow( + subFieldValue, + `${fieldName}.${subField}`, + Object.keys(FieldActorSource), + ); + break; + case 'context': + validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for actor field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as { + source: FieldActorSource; + context: Record; + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts new file mode 100644 index 00000000000..bc1fbcb3a1f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util.ts @@ -0,0 +1,72 @@ +import { isNull } from '@sniptt/guards'; + +import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateAddressFieldOrThrow = ( + value: unknown, + fieldName: string, +): { + addressStreet1?: string | null; + addressStreet2?: string | null; + addressCity?: string | null; + addressState?: string | null; + addressPostcode?: string | null; + addressCountry?: string | null; + addressLat?: number | null; + addressLng?: number | null; +} | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'addressStreet1': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressStreet2': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressCity': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressState': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressPostcode': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressCountry': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressLat': + validateNumericFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'addressLng': + validateNumericFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for address field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as { + addressStreet1?: string | null; + addressStreet2?: string | null; + addressCity?: string | null; + addressState?: string | null; + addressPostcode?: string | null; + addressCountry?: string | null; + addressLat?: number | null; + addressLng?: number | null; + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts new file mode 100644 index 00000000000..d89b59e1d73 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util.ts @@ -0,0 +1,26 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateArrayFieldOrThrow = ( + value: unknown, + fieldName: string, +): string | string[] | null => { + if (isNull(value)) return null; + + if (typeof value === 'string') return value; + + if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) { + throw new CommonQueryRunnerException( + `Invalid value ${inspect(value)} for field "${fieldName} - Array values need to be string"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts new file mode 100644 index 00000000000..2d091fe2e0b --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts @@ -0,0 +1,21 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateBooleanFieldOrThrow = ( + value: unknown, + fieldName: string, +): boolean | null => { + if (typeof value !== 'boolean' && !isNull(value)) + throw new CommonQueryRunnerException( + `Invalid boolean value ${inspect(value)} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts new file mode 100644 index 00000000000..a5f518ba71f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util.ts @@ -0,0 +1,43 @@ +import { isNull } from '@sniptt/guards'; + +import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateCurrencyFieldOrThrow = ( + value: unknown, + fieldName: string, +): { + amountMicros?: number | string | null; + currencyCode?: string | null; +} | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'amountMicros': + validateNumericFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'currencyCode': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for currency field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as { + amountMicros?: number | string | null; + currencyCode?: string | null; + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts new file mode 100644 index 00000000000..a4a39c3733f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util.ts @@ -0,0 +1,26 @@ +import { inspect } from 'util'; + +import { isDate, isNull, isNumber, isString } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateDateAndDateTimeFieldOrThrow = ( + value: unknown, + fieldName: string, +) => { + if (isNull(value)) return null; + + if (isString(value) || isNumber(value) || isDate(value)) { + const date = new Date(value); + + if (!isNaN(date.getTime())) return value; + } + + throw new CommonQueryRunnerException( + `Invalid value ${inspect(value)} for date or date-time field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts new file mode 100644 index 00000000000..32b9e5597a2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util.ts @@ -0,0 +1,42 @@ +import { isNull } from '@sniptt/guards'; + +import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateEmailsFieldOrThrow = ( + value: unknown, + fieldName: string, +): { + primaryEmail?: string | null; + additionalEmails?: string[] | null; +} | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'primaryEmail': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'additionalEmails': + validateArrayFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for emails field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as { + primaryEmail?: string | null; + additionalEmails?: string[] | null; + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts new file mode 100644 index 00000000000..cc02d87cd32 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util.ts @@ -0,0 +1,41 @@ +import { isNull } from '@sniptt/guards'; + +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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateFullNameFieldOrThrow = ( + value: unknown, + fieldName: string, +): { + firstName?: string | null; + lastName?: string | null; +} | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'firstName': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'lastName': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for full name field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as { + firstName?: string | null; + lastName?: string | null; + }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts new file mode 100644 index 00000000000..e0d7efd6846 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util.ts @@ -0,0 +1,39 @@ +import { isNull } from '@sniptt/guards'; + +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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { type LinksFieldGraphQLInput } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util'; + +export const validateLinksFieldOrThrow = ( + value: unknown, + fieldName: string, +): LinksFieldGraphQLInput | null => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'primaryLinkUrl': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'primaryLinkLabel': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'secondaryLinks': + validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for links field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as LinksFieldGraphQLInput; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts new file mode 100644 index 00000000000..6c263ecf80a --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util.ts @@ -0,0 +1,41 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; + +import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateMultiSelectFieldOrThrow = ( + value: unknown, + fieldName: string, + options?: string[], +): string | string[] | null => { + const preValidatedValue = validateArrayFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + if (!isDefined(options)) { + throw new CommonQueryRunnerException( + `Invalid options for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + if ( + (Array.isArray(preValidatedValue) + ? preValidatedValue + : [preValidatedValue] + ).some((item) => !options.includes(item)) + ) { + throw new CommonQueryRunnerException( + `Invalid value ${inspect(value)} for multi select field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + return value as string | string[]; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts new file mode 100644 index 00000000000..a56e78d0f77 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts @@ -0,0 +1,25 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateNumberFieldOrThrow = ( + value: unknown, + fieldName: string, +): number | null => { + if ( + (typeof value !== 'number' && !isNull(value)) || + (typeof value === 'number' && + (isNaN(value) || value === Infinity || value === -Infinity)) + ) + throw new CommonQueryRunnerException( + `Invalid number value ${inspect(value)} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util.ts new file mode 100644 index 00000000000..f4abfea14e2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util.ts @@ -0,0 +1,18 @@ +import { isNull } from '@sniptt/guards'; + +import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util'; + +//Need to handle stringified numbers because of BigFloatScalarType custom gql type + +export const validateNumericFieldOrThrow = ( + value: unknown, + fieldName: string, +): number | string | null => { + if (value === '' || isNull(value)) return null; + + const numberValue = Number(value); + + validateNumberFieldOrThrow(numberValue, fieldName); + + return value as number | string; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts new file mode 100644 index 00000000000..2a3017739a2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util.ts @@ -0,0 +1,43 @@ +import { isNull } from '@sniptt/guards'; + +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 { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { type PhonesFieldGraphQLInput } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; + +export const validatePhonesFieldOrThrow = ( + value: unknown, + fieldName: string, +): PhonesFieldGraphQLInput => { + const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName); + + if (isNull(preValidatedValue)) return null; + + for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) { + switch (subField) { + case 'primaryPhoneNumber': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'primaryPhoneCountryCode': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'primaryPhoneCallingCode': + validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + case 'additionalPhones': + validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`); + break; + + default: + throw new CommonQueryRunnerException( + `Invalid subfield ${subField} for phones field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + } + + return value as PhonesFieldGraphQLInput; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts new file mode 100644 index 00000000000..f47b134f08f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts @@ -0,0 +1,34 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; + +import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateRatingAndSelectFieldOrThrow = ( + value: unknown, + fieldName: string, + options?: string[], +): string | null => { + const preValidatedValue = validateTextFieldOrThrow(value, fieldName); + + if (!isDefined(options)) { + throw new CommonQueryRunnerException( + `Invalid options for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + if (!isNull(preValidatedValue) && !options.includes(preValidatedValue)) { + throw new CommonQueryRunnerException( + `Invalid value ${inspect(value)} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + return preValidatedValue; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts new file mode 100644 index 00000000000..04dd0af1085 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util.ts @@ -0,0 +1,24 @@ +import { inspect } from 'util'; + +import { isNull, isObject } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateRawJsonFieldOrThrow = ( + value: unknown, + fieldName: string, +): object | null => { + if (isNull(value)) return null; + + if (!isObject(value)) { + throw new CommonQueryRunnerException( + `Invalid object value ${inspect(value)} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts new file mode 100644 index 00000000000..425aad4a5ca --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util.ts @@ -0,0 +1,53 @@ +import { inspect } from 'util'; + +import { isNull, isObject } from '@sniptt/guards'; +import { + compositeTypeDefinitions, + FieldMetadataType, +} from 'twenty-shared/types'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateRichTextV2FieldOrThrow = ( + value: unknown, + fieldName: string, +): { + blocknote: string | null | undefined; + markdown: string | null | undefined; +} | null => { + if (isNull(value)) return null; + + try { + const parsedValue = JSON.parse(JSON.stringify(value)); + + if (!isObject(parsedValue)) throw new Error('Should be an object'); + + if (Object.keys(parsedValue).length === 0) return null; + + const subfields = Object.keys(parsedValue); + const richTextV2Subfields = compositeTypeDefinitions + .get(FieldMetadataType.RICH_TEXT_V2) + ?.properties.filter( + (prop) => prop.hidden !== true && prop.hidden !== 'input', + ) + .map((prop) => prop.name); + + if (!subfields.every((subfield) => richTextV2Subfields?.includes(subfield))) + throw new Error( + `Should have only ${richTextV2Subfields?.join(', ')} subfields`, + ); + + return value as { + blocknote: string | null | undefined; + markdown: string | null | undefined; + }; + } catch (error) { + throw new CommonQueryRunnerException( + `Invalid rich text v2 value ${inspect(value)} for field "${fieldName}" - ${error.message}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts new file mode 100644 index 00000000000..f39f8ec6df3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts @@ -0,0 +1,21 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateTextFieldOrThrow = ( + value: unknown, + fieldName: string, +): string | null => { + if (typeof value !== 'string' && !isNull(value)) + throw new CommonQueryRunnerException( + `Invalid string value ${inspect(value)} for text field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts new file mode 100644 index 00000000000..00e00942159 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts @@ -0,0 +1,22 @@ +import { inspect } from 'util'; + +import { isNull } from '@sniptt/guards'; +import { isValidUuid } from 'twenty-shared/utils'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateUUIDFieldOrThrow = ( + value: unknown, + fieldName: string, +): string | null => { + if (!isValidUuid(value as string) && !isNull(value)) + throw new CommonQueryRunnerException( + `Invalid UUID value ${inspect(value)} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + + return value as string; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts new file mode 100644 index 00000000000..2af321989f4 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@nestjs/common'; + +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; +import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; +import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; + +@Injectable() +export class QueryRunnerArgsFactory { + constructor( + private readonly recordPositionService: RecordPositionService, + private readonly recordInputTransformerService: RecordInputTransformerService, + ) {} + + public overrideFilterByFieldMetadata< + T extends ObjectRecordFilter | undefined, + >( + filter: T, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ): T { + if (!isDefined(filter)) { + return filter; + } + + const overrideFilter = (filterObject: ObjectRecordFilter) => { + return Object.entries(filterObject).reduce((acc, [key, value]) => { + if (key === 'and' || key === 'or') { + // @ts-expect-error legacy noImplicitAny + acc[key] = value.map((nestedFilter: ObjectRecordFilter) => + overrideFilter(nestedFilter), + ); + } else if (key === 'not') { + // @ts-expect-error legacy noImplicitAny + acc[key] = overrideFilter(value); + } else { + // @ts-expect-error legacy noImplicitAny + acc[key] = this.transformFilterValueByType( + key, + value, + objectMetadataItemWithFieldMaps, + ); + } + + return acc; + }, {}); + }; + + return overrideFilter(filter) as T; + } + + private transformFilterValueByType( + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ) { + const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key]; + const fieldMetadata = + objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + + if (!fieldMetadata) { + return value; + } + + // Special handling for filter values, which have a specific structure + switch (fieldMetadata.type) { + case FieldMetadataType.NUMBER: { + if (value?.is === 'NULL') { + return value; + } else { + return Object.fromEntries( + Object.entries(value).map(([filterKey, filterValue]) => [ + filterKey, + Number(filterValue), + ]), + ); + } + } + default: + return value; + } + } + + async overrideValueByFieldMetadata( + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any, + fieldMetadataMapByName: FieldMetadataMap, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ) { + const fieldMetadata = fieldMetadataMapByName[key]; + + if (!fieldMetadata) { + return value; + } + + return this.recordInputTransformerService.process({ + recordInput: { [key]: value }, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + }); + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 85a8990a320..8358c1299ae 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -6,7 +6,8 @@ import { Omit } from 'zod/v4/core/util.cjs'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; -import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler'; +import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor'; +import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory'; import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, @@ -25,8 +26,6 @@ import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; -import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; -import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; @@ -62,7 +61,7 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory; @Inject() - protected readonly queryResultGettersFactory: QueryResultGettersFactory; + protected readonly dataArgProcessor: DataArgProcessor; @Inject() protected readonly twentyORMGlobalManager: TwentyORMGlobalManager; @Inject() @@ -76,8 +75,6 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly apiKeyRoleService: ApiKeyRoleService; @Inject() - protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler; - @Inject() protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; @Inject() protected readonly commonResultGettersService: CommonResultGettersService; @@ -191,18 +188,19 @@ export abstract class CommonBaseQueryRunnerService< ); const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + + const computedArgs = await this.computeArgs(args, queryRunnerContext); + const hookedArgs = (await this.workspaceQueryHookService.executePreQueryHooks( authContext, objectMetadataItemWithFieldMaps.nameSingular, operationName, - args as WorkspacePreQueryHookPayload, + computedArgs as WorkspacePreQueryHookPayload, )) as CommonInput; - const computedArgs = await this.computeArgs(hookedArgs, queryRunnerContext); - return { - ...computedArgs, + ...hookedArgs, selectedFieldsResult, }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index 86d875427d7..4e5d40757c1 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -140,7 +140,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer return { ...args, - data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ + data: await this.dataArgProcessor.process({ partialRecordInputs: args.data, authContext, objectMetadataItemWithFieldMaps, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts index 3fa4a53f9e5..635fad0b52c 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts @@ -53,15 +53,15 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ ): Promise> { const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const coercedData = await this.dataArgProcessor.process({ + partialRecordInputs: [args.data], + authContext, + objectMetadataItemWithFieldMaps, + }); + return { ...args, - data: ( - await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ - partialRecordInputs: [args.data], - authContext, - objectMetadataItemWithFieldMaps, - }) - )[0], + data: coercedData[0], }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts index 3d8553de213..b16c488dc89 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts @@ -171,7 +171,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne ), ) ?? [], ), - data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ + data: await this.dataArgProcessor.process({ partialRecordInputs: args.data, authContext, objectMetadataItemWithFieldMaps, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts index 8caae1efef6..9ddbd566f32 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -29,10 +29,6 @@ import { CommonQueryNames, MergeManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util'; @@ -174,9 +170,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ ); if (!priorityRecord) { - throw new GraphqlQueryRunnerException( + throw new CommonQueryRunnerException( 'Priority record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, + CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts index 28b3fd24a52..60df7d4f670 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts @@ -106,7 +106,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer objectMetadataItemWithFieldMaps, ) || {}, data: ( - await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ + await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, objectMetadataItemWithFieldMaps, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts index cfe1b79a6d1..c899bd641be 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts @@ -65,7 +65,7 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ return { ...args, data: ( - await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ + await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, objectMetadataItemWithFieldMaps, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts index 2e8335404a1..41de004a6e6 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts @@ -7,6 +7,7 @@ export enum CommonQueryRunnerExceptionCode { INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT', ARGS_CONFLICT = 'ARGS_CONFLICT', + INVALID_ARGS_DATA = 'INVALID_ARGS_DATA', INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', INVALID_ARGS_LAST = 'INVALID_ARGS_LAST', UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT', diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts index 426719a906f..562ad7ccd98 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts @@ -20,6 +20,7 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = ( case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST: case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST: case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA: case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT: case CommonQueryRunnerExceptionCode.INVALID_CURSOR: case CommonQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED: diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts index 3c9e52ffd75..07a9c569f9d 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts @@ -19,6 +19,7 @@ export const commonQueryRunnerToRestApiExceptionHandler = ( case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST: case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST: case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA: case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT: case CommonQueryRunnerExceptionCode.INVALID_CURSOR: case CommonQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED: diff --git a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts index be29c40913f..fa0916b327d 100644 --- a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts +++ b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers'; +import { CommonArgsProcessors } from 'src/engine/api/common/common-args-processors/common-args-processors'; import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners'; import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service'; import { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service'; @@ -14,6 +14,8 @@ import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; +import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; +import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; @@ -39,13 +41,15 @@ import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-wo ViewFilterGroupModule, ThrottlerModule, MetricsModule, + RecordPositionModule, + RecordTransformerModule, GlobalWorkspaceDataSourceModule, FeatureFlagModule, ], providers: [ ProcessNestedRelationsHelper, ProcessNestedRelationsV2Helper, - ...CommonArgsHandlers, + ...CommonArgsProcessors, ProcessAggregateHelper, ...CommonQueryRunners, CommonResultGettersService, diff --git a/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts b/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts index ac11854af9c..f30795d4f62 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts @@ -7,6 +7,5 @@ export interface CommonSelectedFields { export type CommonSelectedFieldsResult = { select: CommonSelectedFields; relations: CommonSelectedFields; - //TODO = Refacto-common - to update when rest api will handle aggregates aggregate: Record; }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts deleted file mode 100644 index 465e8137654..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; -import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; - -describe('QueryRunnerArgsFactory', () => { - const recordPositionService = { - overridePositionOnRecords: jest - .fn() - .mockImplementation( - ({ partialRecordInputs }: { partialRecordInputs: any[] }) => { - return Promise.resolve( - partialRecordInputs.map((record: any) => ({ - ...record, - position: - record.position === 'last' || !record.position - ? 2 - : record.position, - })), - ); - }, - ), - }; - const workspaceId = 'workspaceId'; - const options = { - authContext: { workspace: { id: workspaceId } }, - objectMetadataItemWithFieldMaps: { - isCustom: true, - nameSingular: 'testNumber', - fieldsById: { - 'position-id': { - type: FieldMetadataType.POSITION, - isCustom: true, - name: 'position', - }, - 'testNumber-id': { - type: FieldMetadataType.NUMBER, - isCustom: true, - name: 'testNumber', - }, - 'otherField-id': { - type: FieldMetadataType.TEXT, - isCustom: true, - name: 'otherField', - }, - } as unknown as FieldMetadataMap, - fieldIdByName: { - position: 'position-id', - testNumber: 'testNumber-id', - otherField: 'otherField-id', - }, - fieldIdByJoinColumnName: {}, - }, - } as unknown as WorkspaceQueryRunnerOptions; - - let factory: QueryRunnerArgsFactory; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - QueryRunnerArgsFactory, - RecordInputTransformerService, - { - provide: RecordPositionService, - useValue: recordPositionService, - }, - ], - }).compile(); - - factory = module.get(QueryRunnerArgsFactory); - }); - - it('should be defined', () => { - expect(factory).toBeDefined(); - }); - - describe('create', () => { - it('should simply return the args when data is an empty array', async () => { - const args = { - data: [], - }; - const result = await factory.create( - args, - options, - ResolverArgsType.CREATE_MANY, - ); - - expect(result).toEqual(args); - }); - - it('createMany type should override data position and number', async () => { - const args = { - id: 'uuid', - data: [{ position: 'last', testNumber: 1 }], - }; - - const result = await factory.create( - args, - options, - ResolverArgsType.CREATE_MANY, - ); - - const expectedArgs = { - partialRecordInputs: [{ position: 'last', testNumber: 1 }], - objectMetadata: { - isCustom: true, - nameSingular: 'testNumber', - fieldIdByName: { - position: 'position-id', - testNumber: 'testNumber-id', - otherField: 'otherField-id', - }, - }, - workspaceId, - shouldBackfillPositionIfUndefined: true, - }; - - expect( - recordPositionService.overridePositionOnRecords, - ).toHaveBeenCalledWith(expectedArgs); - expect(result).toEqual({ - id: 'uuid', - data: [{ position: 2, testNumber: 1 }], - }); - }); - - it('createMany type should override position if not present', async () => { - const args = { - id: 'uuid', - data: [{ testNumber: 1 }], - }; - - const result = await factory.create( - args, - options, - ResolverArgsType.CREATE_MANY, - ); - - const expectedArgs = { - partialRecordInputs: [{ testNumber: 1 }], - objectMetadata: { - isCustom: true, - nameSingular: 'testNumber', - fieldIdByName: { - position: 'position-id', - testNumber: 'testNumber-id', - otherField: 'otherField-id', - }, - }, - workspaceId, - shouldBackfillPositionIfUndefined: true, - }; - - expect( - recordPositionService.overridePositionOnRecords, - ).toHaveBeenCalledWith(expectedArgs); - expect(result).toEqual({ - id: 'uuid', - data: [{ position: 2, testNumber: 1 }], - }); - }); - - it('findMany type should override data position and number', async () => { - const args = { - id: 'uuid', - filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, - }; - - const result = await factory.create( - args, - options, - ResolverArgsType.FIND_MANY, - ); - - expect(result).toEqual({ - id: 'uuid', - filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, - }); - }); - - it('findOne type should override number in filter', async () => { - const args = { - id: 'uuid', - filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, - }; - - const result = await factory.create( - args, - options, - ResolverArgsType.FIND_ONE, - ); - - expect(result).toEqual({ - id: 'uuid', - filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, - }); - }); - - it('findDuplicates type should override number in data and id', async () => { - const args = { - ids: [123], - data: [{ testNumber: 1, otherField: 'test' }], - }; - - const result = await factory.create( - args, - options, - ResolverArgsType.FIND_DUPLICATES, - ); - - expect(result).toEqual({ - ids: [123], - data: [{ testNumber: 1, position: 2, otherField: 'test' }], - }); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts deleted file mode 100644 index 8fe5dd226c5..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { QueryRunnerArgsFactory } from './query-runner-args.factory'; - -import { QueryResultGettersFactory } from './query-result-getters/query-result-getters.factory'; - -export const workspaceQueryRunnerFactories = [ - QueryRunnerArgsFactory, - QueryResultGettersFactory, -]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts deleted file mode 100644 index bbd555ecb67..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; -import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; -import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; -import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; - -import { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-connection.guard'; -import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-nested-record-array.guard'; -import { isQueryResultFieldValueARecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard'; -import { isQueryResultFieldValueARecord } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard'; -import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler'; -import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler'; -import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; -import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -// TODO: find a way to prevent conflict between handlers executing logic on object relations -// And this factory that is also executing logic on object relations -// Right now the factory will override any change made on relations by the handlers -@Injectable() -export class QueryResultGettersFactory { - private readonly logger = new Logger(QueryResultGettersFactory.name); - private handlers: Map; - - constructor(private readonly fileService: FileService) { - this.initializeHandlers(); - } - - private initializeHandlers() { - this.handlers = new Map([ - ['attachment', new AttachmentQueryResultGetterHandler(this.fileService)], - ['person', new PersonQueryResultGetterHandler(this.fileService)], - [ - 'workspaceMember', - new WorkspaceMemberQueryResultGetterHandler(this.fileService), - ], - ['note', new ActivityQueryResultGetterHandler(this.fileService)], - ['task', new ActivityQueryResultGetterHandler(this.fileService)], - ]); - } - - private async processConnection( - connection: IConnection, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, - workspaceId: string, - ): Promise> { - return { - ...connection, - edges: await Promise.all( - connection.edges.map(async (edge: IEdge) => ({ - ...edge, - node: await this.processRecord( - edge.node, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ), - })), - ), - }; - } - - private async processNestedRecordArray( - result: { records: ObjectRecord[] }, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, - workspaceId: string, - ) { - return { - ...result, - records: await Promise.all( - result.records.map( - async (record: ObjectRecord) => - await this.processRecord( - record, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ), - ), - ), - }; - } - - private async processRecordArray( - recordArray: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, - workspaceId: string, - ) { - return await Promise.all( - recordArray.map( - async (record: ObjectRecord) => - await this.processRecord( - record, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ), - ), - ); - } - - private async processRecord( - record: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, - workspaceId: string, - ): Promise { - const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId]; - - if (!isDefined(objectMetadataMapItem)) { - throw new Error('Object metadata map item is not defined'); - } - - const handler = this.getHandler(objectMetadataMapItem.nameSingular); - - const relationFields = Object.keys(record) - .map( - (recordFieldName) => - objectMetadataMapItem.fieldsById[ - objectMetadataMapItem.fieldIdByName[recordFieldName] - ], - ) - .filter(isDefined) - .filter((fieldMetadata) => - isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION), - ); - - const relationFieldsProcessedMap = {} as Record< - string, - QueryResultFieldValue - >; - - for (const relationField of relationFields) { - if (!isDefined(relationField.relationTargetObjectMetadataId)) { - throw new Error('Relation target object metadata id is not defined'); - } - - relationFieldsProcessedMap[relationField.name] = - await this.processQueryResultField( - record[relationField.name], - relationField.relationTargetObjectMetadataId, - objectMetadataMaps, - workspaceId, - ); - } - - const objectRecordProcessedWithoutRelationFields = await handler.handle( - record, - workspaceId, - ); - - const processedRecord = { - ...objectRecordProcessedWithoutRelationFields, - ...relationFieldsProcessedMap, - }; - - return processedRecord; - } - - private async processQueryResultField( - queryResultField: QueryResultFieldValue, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, - workspaceId: string, - ) { - if (isQueryResultFieldValueAConnection(queryResultField)) { - return await this.processConnection( - queryResultField, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ); - } else if (isQueryResultFieldValueANestedRecordArray(queryResultField)) { - return await this.processNestedRecordArray( - queryResultField, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ); - } else if (isQueryResultFieldValueARecordArray(queryResultField)) { - return await this.processRecordArray( - queryResultField, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ); - } else if (isQueryResultFieldValueARecord(queryResultField)) { - return await this.processRecord( - queryResultField, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ); - } else { - this.logger.warn( - `Query result field is not a record, connection, nested record array or record array. - This is an undetected case in query result getter that should be implemented !!`, - ); - - return queryResultField; - } - } - - async create( - result: QueryResultFieldValue, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, - workspaceId: string, - objectMetadataMaps: ObjectMetadataMaps, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { - return await this.processQueryResultField( - result, - objectMetadataItem.id, - objectMetadataMaps, - workspaceId, - ); - } - - private getHandler(objectType: string): QueryResultGetterHandlerInterface { - return ( - this.handlers.get(objectType) || { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handle: (result: any) => result, - } - ); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts deleted file mode 100644 index 303827eba8f..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types'; -import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; - -import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { - type CreateManyResolverArgs, - type CreateOneResolverArgs, - type FindDuplicatesResolverArgs, - type FindManyResolverArgs, - type FindOneResolverArgs, - GroupByResolverArgs, - type MergeManyResolverArgs, - type ResolverArgs, - ResolverArgsType, - type UpdateManyResolverArgs, - type UpdateOneResolverArgs, - type WorkspaceResolverBuilderMethodNames, -} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -@Injectable() -export class QueryRunnerArgsFactory { - constructor( - private readonly recordPositionService: RecordPositionService, - private readonly recordInputTransformerService: RecordInputTransformerService, - ) {} - - async create( - args: ResolverArgs, - options: WorkspaceQueryRunnerOptions, - resolverArgsType: WorkspaceResolverBuilderMethodNames, - ) { - const fieldMetadataMapByNameByName = - options.objectMetadataItemWithFieldMaps.fieldsById; - - const { objectMetadataItemWithFieldMaps, authContext } = options; - - switch (resolverArgsType) { - case ResolverArgsType.CREATE_ONE: - return { - ...args, - data: ( - await this.overrideDataByFieldMetadata({ - partialRecordInputs: [(args as CreateOneResolverArgs).data], - authContext, - objectMetadataItemWithFieldMaps, - }) - )[0], - } satisfies CreateOneResolverArgs; - case ResolverArgsType.CREATE_MANY: - return { - ...args, - data: await this.overrideDataByFieldMetadata({ - partialRecordInputs: (args as CreateManyResolverArgs).data, - authContext, - objectMetadataItemWithFieldMaps, - }), - } satisfies CreateManyResolverArgs; - case ResolverArgsType.UPDATE_ONE: - return { - ...args, - id: (args as UpdateOneResolverArgs).id, - data: ( - await this.overrideDataByFieldMetadata({ - partialRecordInputs: [(args as UpdateOneResolverArgs).data], - authContext, - objectMetadataItemWithFieldMaps, - shouldBackfillPositionIfUndefined: false, - }) - )[0], - } satisfies UpdateOneResolverArgs; - case ResolverArgsType.UPDATE_MANY: - return { - ...args, - filter: this.overrideFilterByFieldMetadata( - (args as UpdateManyResolverArgs).filter, - options.objectMetadataItemWithFieldMaps, - ), - data: ( - await this.overrideDataByFieldMetadata({ - partialRecordInputs: [(args as UpdateManyResolverArgs).data], - authContext, - objectMetadataItemWithFieldMaps, - shouldBackfillPositionIfUndefined: false, - }) - )[0], - } satisfies UpdateManyResolverArgs; - case ResolverArgsType.FIND_ONE: - return { - ...args, - filter: this.overrideFilterByFieldMetadata( - (args as FindOneResolverArgs).filter, - options.objectMetadataItemWithFieldMaps, - ), - }; - case ResolverArgsType.FIND_MANY: - return { - ...args, - filter: this.overrideFilterByFieldMetadata( - (args as FindManyResolverArgs).filter, - options.objectMetadataItemWithFieldMaps, - ), - }; - case ResolverArgsType.FIND_DUPLICATES: - return { - ...args, - ids: (await Promise.all( - (args as FindDuplicatesResolverArgs).ids?.map((id) => - this.overrideValueByFieldMetadata( - 'id', - id, - fieldMetadataMapByNameByName, - options.objectMetadataItemWithFieldMaps, - ), - ) ?? [], - )) as string[], - data: await this.overrideDataByFieldMetadata({ - partialRecordInputs: (args as FindDuplicatesResolverArgs).data, - authContext, - objectMetadataItemWithFieldMaps, - shouldBackfillPositionIfUndefined: false, - }), - } satisfies FindDuplicatesResolverArgs; - case ResolverArgsType.MERGE_MANY: - return { - ...args, - ids: (await Promise.all( - (args as MergeManyResolverArgs).ids?.map((id) => - this.overrideValueByFieldMetadata( - 'id', - id, - fieldMetadataMapByNameByName, - options.objectMetadataItemWithFieldMaps, - ), - ) ?? [], - )) as string[], - conflictPriorityIndex: (args as MergeManyResolverArgs) - .conflictPriorityIndex, - dryRun: (args as MergeManyResolverArgs).dryRun, - } satisfies MergeManyResolverArgs; - case ResolverArgsType.GROUP_BY: - return { - ...args, - filter: this.overrideFilterByFieldMetadata( - (args as GroupByResolverArgs).filter, - options.objectMetadataItemWithFieldMaps, - ), - }; - default: - return args; - } - } - - async overrideDataByFieldMetadata({ - partialRecordInputs, - authContext, - objectMetadataItemWithFieldMaps, - shouldBackfillPositionIfUndefined = true, - }: { - partialRecordInputs: Partial[] | undefined; - authContext: AuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - shouldBackfillPositionIfUndefined?: boolean; - }): Promise[]> { - if (!isDefined(partialRecordInputs)) { - return []; - } - - const allOverriddenRecords: Partial[] = []; - - const workspace = authContext.workspace; - - assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - - const overriddenPositionRecords = - await this.recordPositionService.overridePositionOnRecords({ - partialRecordInputs, - workspaceId: workspace.id, - objectMetadata: { - isCustom: objectMetadataItemWithFieldMaps.isCustom, - nameSingular: objectMetadataItemWithFieldMaps.nameSingular, - fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName, - }, - shouldBackfillPositionIfUndefined, - }); - - for (const record of overriddenPositionRecords) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const createArgByArgKey: [string, any][] = await Promise.all( - Object.entries(record).map(async ([key, value]) => { - const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; - - if (!fieldMetadata) { - return [key, value]; - } - - switch (fieldMetadata.type) { - case FieldMetadataType.NUMBER: - case FieldMetadataType.RICH_TEXT: - case FieldMetadataType.PHONES: - case FieldMetadataType.RICH_TEXT_V2: - case FieldMetadataType.LINKS: - case FieldMetadataType.EMAILS: { - const transformedRecord = - await this.recordInputTransformerService.process({ - recordInput: { [key]: value }, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - }); - - return [key, transformedRecord[key]]; - } - default: - return [key, value]; - } - }), - ); - - allOverriddenRecords.push(Object.fromEntries(createArgByArgKey)); - } - - return allOverriddenRecords; - } - - public overrideFilterByFieldMetadata< - T extends ObjectRecordFilter | undefined, - >( - filter: T, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ): T { - if (!isDefined(filter)) { - return filter; - } - - const overrideFilter = (filterObject: ObjectRecordFilter) => { - return Object.entries(filterObject).reduce((acc, [key, value]) => { - if (key === 'and' || key === 'or') { - // @ts-expect-error legacy noImplicitAny - acc[key] = value.map((nestedFilter: ObjectRecordFilter) => - overrideFilter(nestedFilter), - ); - } else if (key === 'not') { - // @ts-expect-error legacy noImplicitAny - acc[key] = overrideFilter(value); - } else { - // @ts-expect-error legacy noImplicitAny - acc[key] = this.transformFilterValueByType( - key, - value, - objectMetadataItemWithFieldMaps, - ); - } - - return acc; - }, {}); - }; - - return overrideFilter(filter) as T; - } - - private transformFilterValueByType( - key: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ) { - const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; - - if (!fieldMetadata) { - return value; - } - - // Special handling for filter values, which have a specific structure - switch (fieldMetadata.type) { - case FieldMetadataType.NUMBER: { - if (value?.is === 'NULL') { - return value; - } else { - return Object.fromEntries( - Object.entries(value).map(([filterKey, filterValue]) => [ - filterKey, - Number(filterValue), - ]), - ); - } - } - default: - return value; - } - } - - async overrideValueByFieldMetadata( - key: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any, - fieldMetadataMapByName: FieldMetadataMap, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ) { - const fieldMetadata = fieldMetadataMapByName[key]; - - if (!fieldMetadata) { - return value; - } - - return this.recordInputTransformerService.process({ - recordInput: { [key]: value }, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - }); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts index 013e8441c4a..e88d5ce7d41 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts @@ -22,7 +22,6 @@ import { twentyORMGraphqlApiExceptionHandler } from 'src/engine/twenty-orm/utils interface QueryFailedErrorWithCode extends QueryFailedError { code: string; } -//TODO : Refacto-common - Should be handle first in common api layer export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( error: QueryFailedErrorWithCode, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 4cc34c2be07..def86199a21 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module'; -import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener'; import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; @@ -29,11 +28,6 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen RecordPositionModule, SubscriptionsModule, ], - providers: [ - ...workspaceQueryRunnerFactories, - EntityEventsToDbListener, - TelemetryListener, - ], - exports: [...workspaceQueryRunnerFactories], + providers: [EntityEventsToDbListener, TelemetryListener], }) export class WorkspaceQueryRunnerModule {} diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts index 55538a9f467..e25031f2a75 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts @@ -20,7 +20,6 @@ import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role. import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { PermissionsException, @@ -55,8 +54,6 @@ export interface FormatResult { } export abstract class RestApiBaseHandler { - @Inject() - protected readonly recordInputTransformerService: RecordInputTransformerService; @Inject() protected readonly twentyORMManager: TwentyORMManager; @Inject() diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts index e75eb68afce..0b8843e434f 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts @@ -1,4 +1,3 @@ -//TODO : Refacto-common - remove this comment - This parser is a copy of the filter input factory without objectMetadata dependency. Validation will be done in common layer import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util'; import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util'; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts index 33405241028..d1a0520649f 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts @@ -1,8 +1,6 @@ -//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer - import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { parseOrderByRestRequestCommon } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util'; +import { parseOrderBy } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util'; import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; export const parseOrderByRestRequest = ( @@ -10,5 +8,5 @@ export const parseOrderByRestRequest = ( ): ObjectRecordOrderBy => { const orderByQuery = request.query.order_by; - return parseOrderByRestRequestCommon(orderByQuery); + return parseOrderBy(orderByQuery); }; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util.ts index f6030b7747b..6fd089b49ac 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util.ts @@ -1,5 +1,3 @@ -//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer - import { OrderByDirection } from 'twenty-shared/types'; import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -14,7 +12,7 @@ import { const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst; -export const parseOrderByRestRequestCommon = ( +export const parseOrderBy = ( orderByQuery: string | string[] | ParsedQs | ParsedQs[] | undefined, ): ObjectRecordOrderBy => { if (typeof orderByQuery !== 'string') { diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-for-records-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-for-records-rest-request.util.ts index 4047f9324c7..69302bffa95 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-for-records-rest-request.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-for-records-rest-request.util.ts @@ -1,6 +1,6 @@ import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { parseOrderByRestRequestCommon } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util'; +import { parseOrderBy } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util'; import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; export const parseOrderByForRecordsWithGroupByRestRequest = ( @@ -8,5 +8,5 @@ export const parseOrderByForRecordsWithGroupByRestRequest = ( ): ObjectRecordOrderBy | undefined => { const orderByForRecordsWithGroupByQuery = request.query.order_by_for_records; - return parseOrderByRestRequestCommon(orderByForRecordsWithGroupByQuery); + return parseOrderBy(orderByForRecordsWithGroupByQuery); }; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index 8f31d86e1f7..125330843db 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -32,9 +32,9 @@ describe('computeSchemaComponents', () => { "lastName": "Osinski", }, "fieldLinks": { - "additionalLinks": [], "primaryLinkLabel": "", "primaryLinkUrl": "https://narrow-help.net/", + "secondaryLinks": [], }, "fieldMultiSelect": [ "OPTION_1", @@ -46,9 +46,7 @@ describe('computeSchemaComponents', () => { "primaryPhoneCountryCode": "FR", "primaryPhoneNumber": "06 10 20 30 40", }, - "fieldSelect": [ - "OPTION_1", - ], + "fieldSelect": "OPTION_1", }, "properties": { "fieldActor": { @@ -535,9 +533,9 @@ describe('computeSchemaComponents', () => { "lastName": "Jones", }, "fieldLinks": { - "additionalLinks": [], "primaryLinkLabel": "", "primaryLinkUrl": "https://unlawful-blowgun.biz", + "secondaryLinks": [], }, "fieldMultiSelect": [ "OPTION_1", @@ -549,9 +547,7 @@ describe('computeSchemaComponents', () => { "primaryPhoneCountryCode": "FR", "primaryPhoneNumber": "06 10 20 30 40", }, - "fieldSelect": [ - "OPTION_1", - ], + "fieldSelect": "OPTION_1", }, "properties": { "fieldActor": { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts index 25d0f9f73df..9f285702168 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts @@ -58,7 +58,7 @@ export const generateRandomFieldValue = ({ return { primaryLinkLabel: '', primaryLinkUrl: faker.internet.url(), - additionalLinks: [], + secondaryLinks: [], }; } @@ -82,10 +82,10 @@ export const generateRandomFieldValue = ({ case FieldMetadataType.SELECT: { if (!isDefined(field.options) || !isDefined(field.options[0].value)) { - return []; + return null; } - return [field.options[0].value]; + return field.options[0].value; } case FieldMetadataType.MULTI_SELECT: { diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts index 310b9274c4c..eb867ae12eb 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts @@ -2,15 +2,16 @@ import { Injectable } from '@nestjs/common'; import { FieldMetadataType, + ObjectRecord, compositeTypeDefinitions, - type RichTextV2Metadata, - richTextV2ValueSchema, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +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'; import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; @Injectable() export class RecordInputTransformerService { @@ -18,15 +19,9 @@ export class RecordInputTransformerService { recordInput, objectMetadataMapItem, }: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - recordInput: Record; + recordInput: Partial; objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise> { - if (!recordInput) { - return recordInput; - } - + }): Promise> { let transformedEntries = {}; for (const [key, value] of Object.entries(recordInput)) { @@ -72,11 +67,11 @@ export class RecordInputTransformerService { 'Rich text is not supported, please use RICH_TEXT_V2 instead', ); case FieldMetadataType.RICH_TEXT_V2: - return this.transformRichTextV2Value(value); + return await transformRichTextV2Value(value); case FieldMetadataType.LINKS: return transformLinksValue(value); case FieldMetadataType.EMAILS: - return this.transformEmailsValue(value); + return transformEmailsValue(value); case FieldMetadataType.PHONES: return transformPhonesValue({ input: value }); default: @@ -84,73 +79,6 @@ export class RecordInputTransformerService { } } - private async transformRichTextV2Value( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - richTextValue: any, - ): Promise { - const parsedValue = richTextV2ValueSchema.parse(richTextValue); - - const { ServerBlockNoteEditor } = await import('@blocknote/server-util'); - - const serverBlockNoteEditor = ServerBlockNoteEditor.create(); - - // Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks) - // Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947 - let convertedMarkdown: string | null = null; - - try { - convertedMarkdown = isDefined(parsedValue.blocknote) - ? await serverBlockNoteEditor.blocksToMarkdownLossy( - JSON.parse(parsedValue.blocknote), - ) - : null; - } catch { - convertedMarkdown = parsedValue.blocknote || null; - } - - const convertedBlocknote = parsedValue.markdown - ? JSON.stringify( - await serverBlockNoteEditor.tryParseMarkdownToBlocks( - parsedValue.markdown, - ), - ) - : null; - - return { - markdown: parsedValue.markdown || convertedMarkdown, - blocknote: parsedValue.blocknote || convertedBlocknote, - }; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private transformEmailsValue(value: any): any { - if (!value) { - return value; - } - - let additionalEmails = value?.additionalEmails; - const primaryEmail = value?.primaryEmail - ? value.primaryEmail.toLowerCase() - : ''; - - if (additionalEmails) { - try { - const emailArray = JSON.parse(additionalEmails) as string[]; - - additionalEmails = JSON.stringify( - emailArray.map((email) => email.toLowerCase()), - ); - } catch { - /* empty */ - } - } - - return { - primaryEmail, - additionalEmails, - }; - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private stringifySubFields(fieldMetadataType: FieldMetadataType, value: any) { const compositeType = compositeTypeDefinitions.get(fieldMetadataType); diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-emails-value.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-emails-value.util.ts new file mode 100644 index 00000000000..1c74bef302f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-emails-value.util.ts @@ -0,0 +1,34 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const transformEmailsValue = (value: any): any => { + if (!value) { + return value; + } + + let additionalEmails = value?.additionalEmails; + const primaryEmail = value?.primaryEmail + ? value.primaryEmail.toLowerCase() + : ''; + + if (additionalEmails) { + try { + const emailArray = ( + isNonEmptyString(additionalEmails) + ? JSON.parse(additionalEmails) + : additionalEmails + ) as string[]; + + additionalEmails = JSON.stringify( + emailArray.map((email) => email.toLowerCase()), + ); + } catch { + /* empty */ + } + } + + return { + primaryEmail, + additionalEmails, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts index 669cfd102c2..1ddef8f80c2 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts @@ -32,7 +32,7 @@ export const transformLinksValue = ( const secondaryLinksArray = isNonEmptyString(secondaryLinksRaw) ? parseJson(secondaryLinksRaw) - : null; + : secondaryLinksRaw; const { primaryLinkLabel, primaryLinkUrl, secondaryLinks } = removeEmptyLinks( { diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts new file mode 100644 index 00000000000..44825e0bcc5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts @@ -0,0 +1,46 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { + type RichTextV2Metadata, + richTextV2ValueSchema, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const transformRichTextV2Value = async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + richTextValue: any, +): Promise => { + const parsedValue = isNonEmptyString(richTextValue) + ? richTextV2ValueSchema.parse(richTextValue) + : richTextValue; + + const { ServerBlockNoteEditor } = await import('@blocknote/server-util'); + + const serverBlockNoteEditor = ServerBlockNoteEditor.create(); + + // Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks) + // Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947 + let convertedMarkdown: string | null = null; + + try { + convertedMarkdown = isDefined(parsedValue.blocknote) + ? await serverBlockNoteEditor.blocksToMarkdownLossy( + JSON.parse(parsedValue.blocknote), + ) + : null; + } catch { + convertedMarkdown = parsedValue.blocknote || null; + } + + const convertedBlocknote = parsedValue.markdown + ? JSON.stringify( + await serverBlockNoteEditor.tryParseMarkdownToBlocks( + parsedValue.markdown, + ), + ) + : null; + + return { + markdown: parsedValue.markdown || convertedMarkdown, + blocknote: parsedValue.blocknote || convertedBlocknote, + }; +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/address-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/address-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..c3d6399b05b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/address-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - ADDRESS Gql create input - failure ADDRESS - should fail with : {"addressField":"not-an-address"} 1`] = `"Expected type "AddressCreateInput" to be an object."`; + +exports[`Create input validation - ADDRESS Rest create input - failure ADDRESS - should fail with : {"addressField":"not-an-address"} 1`] = `"["Invalid object value 'not-an-address' for field \\"addressField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/array-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/array-field-create-input-validation.integration-spec.ts.snap index 379dc379881..b989262df5b 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/array-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/array-field-create-input-validation.integration-spec.ts.snap @@ -4,6 +4,6 @@ exports[`Create input validation - ARRAY Gql create input - failure ARRAY - shou exports[`Create input validation - ARRAY Gql create input - failure ARRAY - should fail with : {"arrayField":true} 1`] = `"String cannot represent a non string value: true"`; -exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":1} 1`] = `"["malformed array literal: \\"1\\""]"`; +exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":1} 1`] = `"["Invalid value 1 for field \\"arrayField - Array values need to be string\\""]"`; -exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":true} 1`] = `"["malformed array literal: \\"true\\""]"`; +exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":true} 1`] = `"["Invalid value true for field \\"arrayField - Array values need to be string\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/currency-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/currency-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..2285c3ae820 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/currency-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - CURRENCY Gql create input - failure CURRENCY - should fail with : {"currencyField":"not-a-currency"} 1`] = `"Expected type "CurrencyCreateInput" to be an object."`; + +exports[`Create input validation - CURRENCY Rest create input - failure CURRENCY - should fail with : {"currencyField":"not-a-currency"} 1`] = `"["Invalid object value 'not-a-currency' for field \\"currencyField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-field-create-input-validation.integration-spec.ts.snap index 5f86a833135..855656c35c9 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-field-create-input-validation.integration-spec.ts.snap @@ -1,21 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"invalid input syntax for type date: "malformed-date""`; +exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"Invalid value 'malformed-date' for date or date-time field "dateField""`; -exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"invalid input syntax for type date: "{}""`; +exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"Invalid value [] for date or date-time field "dateField""`; -exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"invalid input syntax for type date: "{}""`; +exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"Invalid value {} for date or date-time field "dateField""`; exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":1} 1`] = `"invalid input syntax for type date: "1""`; -exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":true} 1`] = `"invalid input syntax for type date: "true""`; +exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":true} 1`] = `"Invalid value true for date or date-time field "dateField""`; -exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"["invalid input syntax for type date: \\"malformed-date\\""]"`; +exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"["Invalid value 'malformed-date' for date or date-time field \\"dateField\\""]"`; -exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"["invalid input syntax for type date: \\"{}\\""]"`; +exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"["Invalid value [] for date or date-time field \\"dateField\\""]"`; -exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"["invalid input syntax for type date: \\"{}\\""]"`; +exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"["Invalid value {} for date or date-time field \\"dateField\\""]"`; exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":1} 1`] = `"["invalid input syntax for type date: \\"1\\""]"`; -exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":true} 1`] = `"["invalid input syntax for type date: \\"true\\""]"`; +exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":true} 1`] = `"["Invalid value true for date or date-time field \\"dateField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap index 413ee14d76a..ef4e139e455 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/date-time-field-create-input-validation.integration-spec.ts.snap @@ -1,21 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"invalid input syntax for type timestamp with time zone: "malformed-date""`; +exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"Invalid value 'malformed-date' for date or date-time field "dateTimeField""`; -exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"invalid input syntax for type timestamp with time zone: "{}""`; +exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"Invalid value [] for date or date-time field "dateTimeField""`; -exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"invalid input syntax for type timestamp with time zone: "{}""`; +exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"Invalid value {} for date or date-time field "dateTimeField""`; exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"invalid input syntax for type timestamp with time zone: "1""`; -exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"invalid input syntax for type timestamp with time zone: "true""`; +exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"Invalid value true for date or date-time field "dateTimeField""`; -exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"malformed-date\\""]"`; +exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"["Invalid value 'malformed-date' for date or date-time field \\"dateTimeField\\""]"`; -exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"{}\\""]"`; +exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"["Invalid value [] for date or date-time field \\"dateTimeField\\""]"`; -exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"{}\\""]"`; +exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"["Invalid value {} for date or date-time field \\"dateTimeField\\""]"`; exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"1\\""]"`; -exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"true\\""]"`; +exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"["Invalid value true for date or date-time field \\"dateTimeField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/emails-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/emails-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..9dc03eff06b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/emails-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":"not-an-email"} 1`] = `"Expected type "EmailsCreateInput" to be an object."`; + +exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":"not-an-email"} 1`] = `"["Invalid object value 'not-an-email' for field \\"emailsField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/full-name-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/full-name-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..b99eafee136 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/full-name-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - FULL_NAME Gql create input - failure FULL_NAME - should fail with : {"fullNameField":"not-a-full-name"} 1`] = `"Expected type "FullNameCreateInput" to be an object."`; + +exports[`Create input validation - FULL_NAME Rest create input - failure FULL_NAME - should fail with : {"fullNameField":"not-a-full-name"} 1`] = `"["Invalid object value 'not-a-full-name' for field \\"fullNameField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/links-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/links-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..21fb5ebe68f --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/links-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - LINKS Gql create input - failure LINKS - should fail with : {"linksField":"not-a-link"} 1`] = `"Expected type "LinksCreateInput" to be an object."`; + +exports[`Create input validation - LINKS Rest create input - failure LINKS - should fail with : {"linksField":"not-a-link"} 1`] = `"["Invalid object value 'not-a-link' for field \\"linksField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/multi-select-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/multi-select-field-create-input-validation.integration-spec.ts.snap index 538d8a75129..d58631baa17 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/multi-select-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/multi-select-field-create-input-validation.integration-spec.ts.snap @@ -2,12 +2,16 @@ exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":"not-a-select-option"} 1`] = `"Value "not-a-select-option" does not exist in "ApiInputValidationTestObjectMultiSelectFieldEnum" enum."`; +exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":{}} 1`] = `"Enum "ApiInputValidationTestObjectMultiSelectFieldEnum" cannot represent non-string value: {}."`; + exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":1} 1`] = `"Enum "ApiInputValidationTestObjectMultiSelectFieldEnum" cannot represent non-string value: 1."`; exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":true} 1`] = `"Enum "ApiInputValidationTestObjectMultiSelectFieldEnum" cannot represent non-string value: true."`; -exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":"not-a-select-option"} 1`] = `"["malformed array literal: \\"not-a-select-option\\""]"`; +exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":"not-a-select-option"} 1`] = `"["Invalid value 'not-a-select-option' for multi select field \\"multiSelectField\\""]"`; -exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":1} 1`] = `"["malformed array literal: \\"1\\""]"`; +exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":{}} 1`] = `"["Invalid value {} for field \\"multiSelectField - Array values need to be string\\""]"`; -exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":true} 1`] = `"["malformed array literal: \\"true\\""]"`; +exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":1} 1`] = `"["Invalid value 1 for field \\"multiSelectField - Array values need to be string\\""]"`; + +exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":true} 1`] = `"["Invalid value true for field \\"multiSelectField - Array values need to be string\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/phones-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/phones-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..542a8d29737 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/phones-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - PHONES Gql create input - failure PHONES - should fail with : {"phonesField":"not-a-phone"} 1`] = `"Expected type "PhonesCreateInput" to be an object."`; + +exports[`Create input validation - PHONES Rest create input - failure PHONES - should fail with : {"phonesField":"not-a-phone"} 1`] = `"["Invalid object value 'not-a-phone' for field \\"phonesField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rating-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rating-field-create-input-validation.integration-spec.ts.snap index 95420d0bbb4..a676ced67d1 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rating-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rating-field-create-input-validation.integration-spec.ts.snap @@ -10,12 +10,12 @@ exports[`Create input validation - RATING Gql create input - failure RATING - sh exports[`Create input validation - RATING Gql create input - failure RATING - should fail with : {"ratingField":true} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: true."`; -exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":"not-a-rating"} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"not-a-rating\\""]"`; +exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":"not-a-rating"} 1`] = `"["Invalid value 'not-a-rating' for field \\"ratingField\\""]"`; -exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":[]} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"\\""]"`; +exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":[]} 1`] = `"["Invalid string value [] for text field \\"ratingField\\""]"`; -exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":{}} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"[object Object]\\""]"`; +exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":{}} 1`] = `"["Invalid string value {} for text field \\"ratingField\\""]"`; -exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":1} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"1\\""]"`; +exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":1} 1`] = `"["Invalid string value 1 for text field \\"ratingField\\""]"`; -exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":true} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"true\\""]"`; +exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":true} 1`] = `"["Invalid string value true for text field \\"ratingField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/raw-json-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/raw-json-field-create-input-validation.integration-spec.ts.snap index ce3e029b046..925ad17fccd 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/raw-json-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/raw-json-field-create-input-validation.integration-spec.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Create input validation - RAW_JSON Gql create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"Unexpected token 'o', "not-a-json" is not valid JSON"`; +exports[`Create input validation - RAW_JSON Gql create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"Invalid object value 'not-a-json' for field "rawJsonField""`; -exports[`Create input validation - RAW_JSON Rest create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"["Unexpected token 'o', \\"not-a-json\\" is not valid JSON"]"`; \ No newline at end of file +exports[`Create input validation - RAW_JSON Rest create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"["Invalid object value 'not-a-json' for field \\"rawJsonField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/relation-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/relation-field-create-input-validation.integration-spec.ts.snap index 78aab899a4a..bf9cdb77bc0 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/relation-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/relation-field-create-input-validation.integration-spec.ts.snap @@ -1,17 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"invalid input syntax for type uuid: "non-uuid""`; +exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"Invalid UUID value 'non-uuid' for field "manyToOneRelationFieldId""`; -exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"invalid input syntax for type uuid: "1""`; +exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":[]} 1`] = `"ID cannot represent value: []"`; + +exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":{}} 1`] = `"ID cannot represent value: {}"`; + +exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"Invalid UUID value '1' for field "manyToOneRelationFieldId""`; exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":true} 1`] = `"ID cannot represent value: true"`; exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"oneToManyRelationFieldId":"not-existing-field"} 1`] = `"Field "oneToManyRelationFieldId" is not defined by type "ApiInputValidationTestObjectCreateInput". Did you mean "manyToOneRelationFieldId" or "manyToOneRelationField"?"`; -exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"["invalid input syntax for type uuid: \\"non-uuid\\""]"`; +exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"oneToOneRelationField":"not-existing-field"} 1`] = `"Field "oneToOneRelationField" is not defined by type "ApiInputValidationTestObjectCreateInput". Did you mean "manyToOneRelationField" or "manyToOneRelationFieldId"?"`; -exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"["invalid input syntax for type uuid: \\"1\\""]"`; +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"["Invalid UUID value 'non-uuid' for field \\"manyToOneRelationFieldId\\""]"`; -exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":true} 1`] = `"["invalid input syntax for type uuid: \\"true\\""]"`; +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":[]} 1`] = `"["Invalid UUID value [] for field \\"manyToOneRelationFieldId\\""]"`; -exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"oneToManyRelationFieldId":"not-existing-field"} 1`] = `"["Field metadata for field \\"oneToManyRelationFieldId\\" is missing in object metadata apiInputValidationTestObject"]"`; +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":{}} 1`] = `"["Invalid UUID value {} for field \\"manyToOneRelationFieldId\\""]"`; + +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"["Invalid UUID value 1 for field \\"manyToOneRelationFieldId\\""]"`; + +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":true} 1`] = `"["Invalid UUID value true for field \\"manyToOneRelationFieldId\\""]"`; + +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"oneToManyRelationFieldId":"not-existing-field"} 1`] = `"["Object apiInputValidationTestObject doesn't have any \\"oneToManyRelationFieldId\\" field."]"`; + +exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"oneToOneRelationField":"not-existing-field"} 1`] = `"["Object apiInputValidationTestObject doesn't have any \\"oneToOneRelationField\\" field."]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap index 52b700a0087..3765c3e2455 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-field-create-input-validation.integration-spec.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"Rich text is not supported, please use RICH_TEXT_V2 instead"`; +exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"richTextField RICH_TEXT-typed field does not support write operations"`; -exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"["Rich text is not supported, please use RICH_TEXT_V2 instead"]"`; +exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"["richTextField RICH_TEXT-typed field does not support write operations"]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-v2-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-v2-field-create-input-validation.integration-spec.ts.snap new file mode 100644 index 00000000000..da48013133e --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/rich-text-v2-field-create-input-validation.integration-spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create input validation - RICH_TEXT_V2 Gql create input - failure RICH_TEXT_V2 - should fail with : {"richTextV2Field":"not-a-rich-text"} 1`] = `"Expected type "RichTextV2CreateInput" to be an object."`; + +exports[`Create input validation - RICH_TEXT_V2 Rest create input - failure RICH_TEXT_V2 - should fail with : {"richTextV2Field":"not-a-rich-text"} 1`] = `"["Invalid rich text v2 value 'not-a-rich-text' for field \\"richTextV2Field\\" - Should be an object"]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/select-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/select-field-create-input-validation.integration-spec.ts.snap index 4a8f02a32e8..6e34a47d9ee 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/select-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/select-field-create-input-validation.integration-spec.ts.snap @@ -10,12 +10,12 @@ exports[`Create input validation - SELECT Gql create input - failure SELECT - sh exports[`Create input validation - SELECT Gql create input - failure SELECT - should fail with : {"selectField":true} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: true."`; -exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":"not-a-select-option"} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_selectField_enum\\": \\"not-a-select-option\\""]"`; +exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":"not-a-select-option"} 1`] = `"["Invalid value 'not-a-select-option' for field \\"selectField\\""]"`; -exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":[]} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_selectField_enum\\": \\"\\""]"`; +exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":[]} 1`] = `"["Invalid string value [] for text field \\"selectField\\""]"`; -exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":{}} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_selectField_enum\\": \\"[object Object]\\""]"`; +exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":{}} 1`] = `"["Invalid string value {} for text field \\"selectField\\""]"`; -exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":1} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_selectField_enum\\": \\"1\\""]"`; +exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":1} 1`] = `"["Invalid string value 1 for text field \\"selectField\\""]"`; -exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":true} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_selectField_enum\\": \\"true\\""]"`; +exports[`Create input validation - SELECT Rest create input - failure SELECT - should fail with : {"selectField":true} 1`] = `"["Invalid string value true for text field \\"selectField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/text-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/text-field-create-input-validation.integration-spec.ts.snap index 53b86773a6e..c1b79e36bec 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/text-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/text-field-create-input-validation.integration-spec.ts.snap @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Create input validation - TEXT Gql create input - failure TEXT - should fail with : {"textField":[]} 1`] = `"String cannot represent a non string value: []"`; + +exports[`Create input validation - TEXT Gql create input - failure TEXT - should fail with : {"textField":{}} 1`] = `"String cannot represent a non string value: {}"`; + +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\\""]"`; + +exports[`Create input validation - TEXT Rest create input - failure TEXT - should fail with : {"textField":{}} 1`] = `"["Invalid string value {} for text field \\"textField\\""]"`; + +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\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/uuid-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/uuid-field-create-input-validation.integration-spec.ts.snap index 44e0a7d7223..67be00eb88a 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/uuid-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/uuid-field-create-input-validation.integration-spec.ts.snap @@ -10,12 +10,12 @@ exports[`Create input validation - UUID Gql create input - failure UUID - should exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":true} 1`] = `"UUID must be a string"`; -exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":"non-uuid"} 1`] = `"["invalid input syntax for type uuid: \\"non-uuid\\""]"`; +exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":"non-uuid"} 1`] = `"["Invalid UUID value 'non-uuid' for field \\"uuidField\\""]"`; -exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":[]} 1`] = `"["invalid input syntax for type uuid: \\"{}\\""]"`; +exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":[]} 1`] = `"["Invalid UUID value [] for field \\"uuidField\\""]"`; -exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":{}} 1`] = `"["invalid input syntax for type uuid: \\"{}\\""]"`; +exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":{}} 1`] = `"["Invalid UUID value {} for field \\"uuidField\\""]"`; -exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":1} 1`] = `"["invalid input syntax for type uuid: \\"1\\""]"`; +exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":1} 1`] = `"["Invalid UUID value 1 for field \\"uuidField\\""]"`; -exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":true} 1`] = `"["invalid input syntax for type uuid: \\"true\\""]"`; +exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":true} 1`] = `"["Invalid UUID value true for field \\"uuidField\\""]"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/address-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/address-field-create-input-validation.integration-spec.ts index 7b78152c55c..397c89b9bf2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/address-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/address-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -10,6 +13,9 @@ const FIELD_METADATA_TYPE = FieldMetadataType.ADDRESS; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; + describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; let objectMetadataSingularName: string; @@ -68,4 +74,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts index e6f644a2f7d..488d437e564 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant.ts @@ -1,15 +1,8 @@ import { type FieldMetadataTypesToTestForCreateInputValidation } from 'test/integration/graphql/suites/inputs-validation/types/field-metadata-type-to-test'; import { FieldMetadataType } from 'twenty-shared/types'; -import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; - export const failingCreateInputByFieldMetadataType: { - [K in Exclude< - FieldMetadataTypesToTestForCreateInputValidation, - | CompositeFieldMetadataType - | FieldMetadataType.NUMBER - | FieldMetadataType.BOOLEAN - >]: { + [K in FieldMetadataTypesToTestForCreateInputValidation]: { input: any; }[]; } = { @@ -19,57 +12,49 @@ export const failingCreateInputByFieldMetadataType: { textField: null, }, }, - // { - // input: { - // textField: {}, - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // textField: [], - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // textField: true, - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // textField: 1, - // }, - // //TODO - rest api to fix, should throw - // }, + { + input: { + textField: {}, + }, + }, + { + input: { + textField: [], + }, + }, + { + input: { + textField: true, + }, + }, + { + input: { + textField: 1, + }, + }, + ], + [FieldMetadataType.NUMBER]: [ + { + input: { + numberField: {}, + }, + }, + { + input: { + numberField: [], + }, + }, + { + input: { + numberField: true, + }, + }, + { + input: { + numberField: 'string', + }, + }, ], - // [FieldMetadataType.NUMBER]: [ - // { - // input: { - // numberField: {}, - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // numberField: [], - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // numberField: true, - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // numberField: 'string', - // }, - // // TODO - rest api to fix, should throw - // }, - // ], [FieldMetadataType.UUID]: [ { input: { @@ -125,18 +110,16 @@ export const failingCreateInputByFieldMetadataType: { }, ], [FieldMetadataType.RELATION]: [ - // { - // input: { - // manyToOneRelationFieldId: {}, - // }, - // //TODO - rest api to fix, should throw - // }, - // { - // input: { - // manyToOneRelationFieldId: [], - // }, - // //TODO - rest api to fix, should throw - // }, + { + input: { + manyToOneRelationFieldId: {}, + }, + }, + { + input: { + manyToOneRelationFieldId: [], + }, + }, { input: { manyToOneRelationFieldId: true, @@ -152,12 +135,11 @@ export const failingCreateInputByFieldMetadataType: { manyToOneRelationFieldId: 'non-uuid', }, }, - // { - // input: { - // manyToOneRelationField: 'not-existing-field', - // }, - // //TODO - rest api to fix, should throw - // }, + { + input: { + oneToOneRelationField: 'not-existing-field', + }, + }, { input: { oneToManyRelationFieldId: 'not-existing-field', @@ -170,26 +152,8 @@ export const failingCreateInputByFieldMetadataType: { rawJsonField: 'not-a-json', }, }, - // //TODO - to fix, should throw - // { - // input: { - // rawJsonField: true, - // }, - // }, - // //TODO - to fix, should throw - // { - // input: { - // rawJsonField: 1, - // }, - // }, ], [FieldMetadataType.ARRAY]: [ - // //TODO - to fix, should throw - // { - // input: { - // arrayField: 'not-an-array', - // }, - // }, { input: { arrayField: true, @@ -234,12 +198,11 @@ export const failingCreateInputByFieldMetadataType: { multiSelectField: 'not-a-select-option', }, }, - // { - // input: { - // multiSelectField: {}, - // }, - // //TODO - rest api to fix, should throw - // }, + { + input: { + multiSelectField: {}, + }, + }, { input: { multiSelectField: true, @@ -305,39 +268,33 @@ export const failingCreateInputByFieldMetadataType: { }, }, ], - // [FieldMetadataType.BOOLEAN]: [ - // // { - // // input: { - // // booleanField: null, - // // }, - // // }, - // // { - // // input: { - // // booleanField: {}, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // // { - // // input: { - // // booleanField: [], - // // }, - // // gqlErrorMessage: 'cannot represent a non string value', - // // //TODO - rest api to fix, should throw - // // restErrorMessage: '', - // // }, - // // { - // // input: { - // // booleanField: 'string', - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // // { - // // input: { - // // booleanField: 1, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], + [FieldMetadataType.BOOLEAN]: [ + { + input: { + booleanField: null, + }, + }, + { + input: { + booleanField: {}, + }, + }, + { + input: { + booleanField: [], + }, + }, + { + input: { + booleanField: 'string', + }, + }, + { + input: { + booleanField: 1, + }, + }, + ], [FieldMetadataType.RICH_TEXT]: [ { input: { @@ -345,74 +302,53 @@ export const failingCreateInputByFieldMetadataType: { }, }, ], - // [FieldMetadataType.ADDRESS]: [ - // // { - // // input: { - // // addressField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // // { - // // input: { - // // addressField: 'not-an-address', - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.CURRENCY]: [ - // // { - // // input: { - // // currencyField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.EMAILS]: [ - // // { - // // input: { - // // emailsField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.PHONES]: [ - // // { - // // input: { - // // phonesField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.FULL_NAME]: [ - // // { - // // input: { - // // fullNameField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.LINKS]: [ - // // { - // // input: { - // // linksField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.RICH_TEXT_V2]: [ - // // { - // // input: { - // // richTextV2Field: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], - // [FieldMetadataType.ACTOR]: [ - // // { - // // input: { - // // actorField: null, - // // }, - // // //TODO - rest api to fix, should throw - // // }, - // ], + [FieldMetadataType.ADDRESS]: [ + { + input: { + addressField: 'not-an-address', + }, + }, + ], + [FieldMetadataType.CURRENCY]: [ + { + input: { + currencyField: 'not-a-currency', + }, + }, + ], + [FieldMetadataType.EMAILS]: [ + { + input: { + emailsField: 'not-an-email', + }, + }, + ], + [FieldMetadataType.PHONES]: [ + { + input: { + phonesField: 'not-a-phone', + }, + }, + ], + [FieldMetadataType.FULL_NAME]: [ + { + input: { + fullNameField: 'not-a-full-name', + }, + }, + ], + [FieldMetadataType.LINKS]: [ + { + input: { + linksField: 'not-a-link', + }, + }, + ], + [FieldMetadataType.RICH_TEXT_V2]: [ + { + input: { + richTextV2Field: 'not-a-rich-text', + }, + }, + ], }; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts index 7c8d94f5d14..1675bf946f2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts @@ -159,6 +159,16 @@ export const successfulCreateInputByFieldMetadataType: { ); }, }, + { + input: { + arrayField: 'item1', + }, + validateInput: (record: Record) => { + return ( + record.arrayField.length === 1 && record.arrayField.includes('item1') + ); + }, + }, { input: { arrayField: [], diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/currency-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/currency-field-create-input-validation.integration-spec.ts index 4463811d80f..19206f79e9a 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/currency-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/currency-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -10,6 +13,9 @@ const FIELD_METADATA_TYPE = FieldMetadataType.CURRENCY; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; + describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; let objectMetadataSingularName: string; @@ -68,4 +74,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/emails-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/emails-field-create-input-validation.integration-spec.ts index bb9c680b576..da2c7fd7871 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/emails-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/emails-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -10,6 +13,9 @@ const FIELD_METADATA_TYPE = FieldMetadataType.EMAILS; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; + describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; let objectMetadataSingularName: string; @@ -68,4 +74,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/full-name-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/full-name-field-create-input-validation.integration-spec.ts index 94b423e406a..8c736067201 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/full-name-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/full-name-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -10,6 +13,9 @@ const FIELD_METADATA_TYPE = FieldMetadataType.FULL_NAME; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; + describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; let objectMetadataSingularName: string; @@ -68,4 +74,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/links-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/links-field-create-input-validation.integration-spec.ts index cc17c07d2d1..f0db6d3722d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/links-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/links-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -10,6 +13,9 @@ const FIELD_METADATA_TYPE = FieldMetadataType.LINKS; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; + describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; let objectMetadataSingularName: string; @@ -68,4 +74,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/phones-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/phones-field-create-input-validation.integration-spec.ts index 9d21b5b9d11..d89d991d03d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/phones-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/phones-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -9,6 +12,8 @@ const FIELD_METADATA_TYPE = FieldMetadataType.PHONES; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; @@ -68,4 +73,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/rich-text-v2-field-create-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/rich-text-v2-field-create-input-validation.integration-spec.ts index 400300efc27..6248325c02c 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/rich-text-v2-field-create-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/rich-text-v2-field-create-input-validation.integration-spec.ts @@ -1,5 +1,8 @@ +import { failingCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/failing-create-input-by-field-metadata-type.constant'; import { successfulCreateInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant'; +import { expectGqlCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-error.util'; import { expectGqlCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-gql-create-input-validation-success.util'; +import { expectRestCreateInputValidationError } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-error.util'; import { expectRestCreateInputValidationSuccess } from 'test/integration/graphql/suites/inputs-validation/create-validation/utils/expect-rest-create-input-validation-success.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -10,6 +13,9 @@ const FIELD_METADATA_TYPE = FieldMetadataType.RICH_TEXT_V2; const successfulTestCases = successfulCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingCreateInputByFieldMetadataType[FIELD_METADATA_TYPE]; + describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { let objectMetadataId: string; let objectMetadataSingularName: string; @@ -68,4 +74,38 @@ describe(`Create input validation - ${FIELD_METADATA_TYPE}`, () => { }, ); }); + + describe('Gql create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectGqlCreateInputValidationError( + objectMetadataSingularName, + input, + ); + }, + ); + }); + + describe('Rest create input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedInput: JSON.stringify(testCase.input), + })), + )( + `${FIELD_METADATA_TYPE} - should fail with : $stringifiedInput`, + async ({ input }) => { + await expectRestCreateInputValidationError( + objectMetadataPluralName, + input, + ); + }, + ); + }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts index a5e95ae3342..fa04e0d5efd 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts @@ -208,7 +208,7 @@ describe('Core REST API Create One endpoint', () => { .expect(400) .expect((res) => { expect(res.body.messages[0]).toMatch( - /invalid input value for enum workspace_[a-z0-9]+\.opportunity_stage_enum: "INVALID_ENUM_VALUE"/, + 'Invalid value \'INVALID_ENUM_VALUE\' for field "stage"', ); expect(res.body.error).toBe('BadRequestException'); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts index d1896b1a35e..bb8736ccee6 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-many.integration-spec.ts @@ -53,6 +53,7 @@ describe('Core REST API Find Many endpoint', () => { companyId: TEST_COMPANY_1_ID, }, }); + index++; } }); @@ -61,7 +62,9 @@ describe('Core REST API Find Many endpoint', () => { const response = await makeRestAPIRequest({ method: 'get', path: '/people', - }).expect(200); + }); + + expect(response.status).toBe(200); const people = response.body.data.people; const pageInfo = response.body.pageInfo;