From 08a3d983cb83e592d6c3e1390ccfe02939e1b106 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:13:18 +0100 Subject: [PATCH] Date & DateTime validation fixes / improvements (#18009) Fixes https://github.com/twentyhq/twenty/issues/17138 - Backend should have strict date/dateTime format validation - FE in import csv is more permissive --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- ...ildRecordFromImportedStructuredRow.test.ts | 4 +- .../buildRecordFromImportedStructuredRow.ts | 11 +- .../data-arg.processor.spec.ts.snap | 209 ++++++++ ...-inputs-by-field-metadata-type.constant.ts | 202 ++++++++ ...-metadata-config-by-field-name.constant.ts | 172 +++++++ ...-inputs-by-field-metadata-type.constant.ts | 448 ++++++++++++++++++ .../__tests__/data-arg.processor.spec.ts | 195 ++++++++ .../data-arg-processor/data-arg.processor.ts | 6 +- ...-and-date-time-field-or-throw.util.spec.ts | 97 ---- .../validate-date-field-or-throw.util.spec.ts | 191 ++++++++ ...date-date-time-field-or-throw.util.spec.ts | 189 ++++++++ ...-date-and-date-time-field-or-throw.util.ts | 31 -- .../validate-date-field-or-throw.util.ts | 69 +++ .../validate-date-time-field-or-throw.util.ts | 72 +++ ...-input-validation.integration-spec.ts.snap | 20 +- ...-input-validation.integration-spec.ts.snap | 20 +- ...-input-validation.integration-spec.ts.snap | 12 - ...-input-validation.integration-spec.ts.snap | 12 - ...-input-validation.integration-spec.ts.snap | 16 - ...-input-validation.integration-spec.ts.snap | 16 - ...-input-validation.integration-spec.ts.snap | 12 - ...-input-validation.integration-spec.ts.snap | 12 - ...-input-validation.integration-spec.ts.snap | 16 - ...e-input-by-field-metadata-type.constant.ts | 182 +------ ...e-input-by-field-metadata-type.constant.ts | 116 +---- 25 files changed, 1781 insertions(+), 549 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg.processor.spec.ts.snap create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/failing-inputs-by-field-metadata-type.constant.ts create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg.processor.spec.ts delete mode 100644 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 create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-field-or-throw.util.spec.ts create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-time-field-or-throw.util.spec.ts delete mode 100644 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 create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-field-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-time-field-or-throw.util.ts diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts index 7c3472c653..bd54a445bb 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts @@ -428,8 +428,8 @@ describe('buildRecordFromImportedStructuredRow', () => { blocknote: 'Rich content in blocknote format', markdown: 'Content in markdown format', }, - dateField: '2023-12-25', - dateTimeField: '2023-12-25T10:30:00Z', + dateField: '2023-12-25T00:00:00.000Z', + dateTimeField: '2023-12-25T10:30:00.000Z', ratingField: '4', }); }); diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts index c9979c8bee..a8e4243bbc 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts @@ -349,13 +349,22 @@ export const buildRecordFromImportedStructuredRow = ({ break; } case FieldMetadataType.UUID: + if ( + isDefined(importedFieldValue) && + isNonEmptyString(importedFieldValue) + ) { + recordToBuild[field.name] = importedFieldValue; + } + break; case FieldMetadataType.DATE: case FieldMetadataType.DATE_TIME: if ( isDefined(importedFieldValue) && isNonEmptyString(importedFieldValue) ) { - recordToBuild[field.name] = importedFieldValue; + recordToBuild[field.name] = new Date( + importedFieldValue, + ).toISOString(); } break; case FieldMetadataType.SELECT: diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg.processor.spec.ts.snap b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg.processor.spec.ts.snap new file mode 100644 index 0000000000..8f86124142 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg.processor.spec.ts.snap @@ -0,0 +1,209 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`DataArgProcessor failing inputs validation ADDRESS should throw for invalid input #1: "not-an-address" 1`] = `"Invalid object value 'not-an-address' for field "addressField""`; + +exports[`DataArgProcessor failing inputs validation ADDRESS should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "addressField""`; + +exports[`DataArgProcessor failing inputs validation ADDRESS should throw for invalid input #3: true 1`] = `"Invalid object value true for field "addressField""`; + +exports[`DataArgProcessor failing inputs validation ARRAY should throw for invalid input #1: true 1`] = `"Invalid value true for field "arrayField - Array values need to be string""`; + +exports[`DataArgProcessor failing inputs validation ARRAY should throw for invalid input #2: 1 1`] = `"Invalid value 1 for field "arrayField - Array values need to be string""`; + +exports[`DataArgProcessor failing inputs validation BOOLEAN should throw for invalid input #1: {} 1`] = `"Invalid boolean value {} for field "booleanField""`; + +exports[`DataArgProcessor failing inputs validation BOOLEAN should throw for invalid input #2: [] 1`] = `"Invalid boolean value [] for field "booleanField""`; + +exports[`DataArgProcessor failing inputs validation BOOLEAN should throw for invalid input #3: "string" 1`] = `"Invalid boolean value 'string' for field "booleanField""`; + +exports[`DataArgProcessor failing inputs validation BOOLEAN should throw for invalid input #4: 1 1`] = `"Invalid boolean value 1 for field "booleanField""`; + +exports[`DataArgProcessor failing inputs validation CURRENCY should throw for invalid input #1: "not-a-currency" 1`] = `"Invalid object value 'not-a-currency' for field "currencyField""`; + +exports[`DataArgProcessor failing inputs validation CURRENCY should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "currencyField""`; + +exports[`DataArgProcessor failing inputs validation CURRENCY should throw for invalid input #3: true 1`] = `"Invalid object value true for field "currencyField""`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #1: "malformed-date" 1`] = `"Invalid value 'malformed-date' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #2: {} 1`] = `"Invalid value {} for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #3: [] 1`] = `"Invalid value [] for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #4: true 1`] = `"Invalid value true for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #5: 1 1`] = `"Invalid value 1 for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #6: "2024" 1`] = `"Invalid value '2024' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #7: "2024-01" 1`] = `"Invalid value '2024-01' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #8: "2024-13-01" 1`] = `"Invalid value '2024-13-01' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE should throw for invalid input #9: "2024-02-31" 1`] = `"Invalid value '2024-02-31' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #1: "malformed-date" 1`] = `"Invalid value 'malformed-date' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #2: {} 1`] = `"Invalid value {} for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #3: [] 1`] = `"Invalid value [] for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #4: true 1`] = `"Invalid value true for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #5: 1 1`] = `"Invalid value 1 for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #6: "2024" 1`] = `"Invalid value '2024' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #7: "2024-01" 1`] = `"Invalid value '2024-01' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #8: "2024-13-01T10:30:00Z" 1`] = `"Invalid value '2024-13-01T10:30:00Z' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation DATE_TIME should throw for invalid input #9: "2024-01-15T25:30:00Z" 1`] = `"Invalid value '2024-01-15T25:30:00Z' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #1: "not-an-email" 1`] = `"Invalid object value 'not-an-email' for field "emailsField""`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #2: {"primaryEmail":"not-an-email"} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.primaryEmail""`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #3: {"additionalEmails":"not-an-email"} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #4: {"additionalEmails":["not-an-email"]} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #5: {"primaryEmail":"email@email.com","additionalEmails":["not-an-email"]} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #6: {"primaryEmail":"not-an-email","additionalEmails":["additional@email.com"]} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.primaryEmail""`; + +exports[`DataArgProcessor failing inputs validation EMAILS should throw for invalid input #7: {"additionalEmails":["not-an-email","additional@email.com"]} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`; + +exports[`DataArgProcessor failing inputs validation FILES should throw for invalid input #1: "not-an-addFiles-property" 1`] = `"Invalid value "'not-an-addFiles-property'" for FILES field "filesField" - It should be an array of objects with "fileId" and "label" properties."`; + +exports[`DataArgProcessor failing inputs validation FILES should throw for invalid input #2: {"addFiles":[{"invalidField":"test"}]} 1`] = `"Invalid value "{ addFiles: [ { invalidField: 'test' } ] }" for FILES field "filesField" - : Invalid input: expected array, received object"`; + +exports[`DataArgProcessor failing inputs validation FILES should throw for invalid input #3: {"addFiles":[{"fileId":"not-a-uuid","label":"Doc.pdf"}]} 1`] = `"Invalid value "{ addFiles: [ { fileId: 'not-a-uuid', label: 'Doc.pdf' } ] }" for FILES field "filesField" - : Invalid input: expected array, received object"`; + +exports[`DataArgProcessor failing inputs validation FILES should throw for invalid input #4: [{"addFiles":[{"fileId":"550e8400-e29b-41d4-a716-446655440000","label":12345}]}] 1`] = `"Invalid value "[ { addFiles: [ [Object] ] } ]" for FILES field "filesField" - 0.fileId: Invalid input: expected string, received undefined, 0.label: Invalid input: expected string, received undefined, 0: Unrecognized key: "addFiles""`; + +exports[`DataArgProcessor failing inputs validation FILES should throw for invalid input #5: {"addFiles":[{"fileId":"550e8400-e29b-41d4-a716-446655440000","label":"Document.pdf","extension":"not-allowed-in-input"}]} 1`] = ` +"Invalid value "{ + addFiles: [ + { + fileId: '550e8400-e29b-41d4-a716-446655440000', + label: 'Document.pdf', + extension: 'not-allowed-in-input' + } + ] +}" for FILES field "filesField" - : Invalid input: expected array, received object" +`; + +exports[`DataArgProcessor failing inputs validation FULL_NAME should throw for invalid input #1: "not-a-full-name" 1`] = `"Invalid object value 'not-a-full-name' for field "fullNameField""`; + +exports[`DataArgProcessor failing inputs validation FULL_NAME should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "fullNameField""`; + +exports[`DataArgProcessor failing inputs validation FULL_NAME should throw for invalid input #3: true 1`] = `"Invalid object value true for field "fullNameField""`; + +exports[`DataArgProcessor failing inputs validation LINKS should throw for invalid input #1: "not-a-link" 1`] = `"Invalid object value 'not-a-link' for field "linksField""`; + +exports[`DataArgProcessor failing inputs validation LINKS should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "linksField""`; + +exports[`DataArgProcessor failing inputs validation LINKS should throw for invalid input #3: true 1`] = `"Invalid object value true for field "linksField""`; + +exports[`DataArgProcessor failing inputs validation MORPH_RELATION should throw for invalid input #1: "not-a-morph-relation" 1`] = `"Invalid UUID value 'not-a-morph-relation' for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessor failing inputs validation MORPH_RELATION should throw for invalid input #2: {} 1`] = `"Invalid UUID value {} for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessor failing inputs validation MORPH_RELATION should throw for invalid input #3: [] 1`] = `"Invalid UUID value [] for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessor failing inputs validation MORPH_RELATION should throw for invalid input #4: true 1`] = `"Invalid UUID value true for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessor failing inputs validation MORPH_RELATION should throw for invalid input #5: 1 1`] = `"Invalid UUID value 1 for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessor failing inputs validation MULTI_SELECT should throw for invalid input #1: "not-a-select-option" 1`] = `"Invalid value 'not-a-select-option' for multi select field "multiSelectField""`; + +exports[`DataArgProcessor failing inputs validation MULTI_SELECT should throw for invalid input #2: {} 1`] = `"Invalid value {} for field "multiSelectField - Array values need to be string""`; + +exports[`DataArgProcessor failing inputs validation MULTI_SELECT should throw for invalid input #3: true 1`] = `"Invalid value true for field "multiSelectField - Array values need to be string""`; + +exports[`DataArgProcessor failing inputs validation MULTI_SELECT should throw for invalid input #4: 1 1`] = `"Invalid value 1 for field "multiSelectField - Array values need to be string""`; + +exports[`DataArgProcessor failing inputs validation NUMBER should throw for invalid input #1: {} 1`] = `"Invalid number value {} for field "numberField""`; + +exports[`DataArgProcessor failing inputs validation NUMBER should throw for invalid input #2: [] 1`] = `"Invalid number value [] for field "numberField""`; + +exports[`DataArgProcessor failing inputs validation NUMBER should throw for invalid input #3: true 1`] = `"Invalid number value true for field "numberField""`; + +exports[`DataArgProcessor failing inputs validation NUMBER should throw for invalid input #4: "string" 1`] = `"Invalid number value 'string' for field "numberField""`; + +exports[`DataArgProcessor failing inputs validation NUMERIC should throw for invalid input #1: {} 1`] = `"Invalid number value NaN for field "numericField""`; + +exports[`DataArgProcessor failing inputs validation NUMERIC should throw for invalid input #2: "not-a-number" 1`] = `"Invalid number value NaN for field "numericField""`; + +exports[`DataArgProcessor failing inputs validation PHONES should throw for invalid input #1: "not-a-phone" 1`] = `"Invalid object value 'not-a-phone' for field "phonesField""`; + +exports[`DataArgProcessor failing inputs validation PHONES should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "phonesField""`; + +exports[`DataArgProcessor failing inputs validation PHONES should throw for invalid input #3: true 1`] = `"Invalid object value true for field "phonesField""`; + +exports[`DataArgProcessor failing inputs validation POSITION should throw for invalid input #1: "not-a-position" 1`] = `"Invalid position value 'not-a-position' for field "position""`; + +exports[`DataArgProcessor failing inputs validation POSITION should throw for invalid input #2: null 1`] = `"Invalid position value NaN for field "position""`; + +exports[`DataArgProcessor failing inputs validation POSITION should throw for invalid input #3: {} 1`] = `"Invalid position value {} for field "position""`; + +exports[`DataArgProcessor failing inputs validation POSITION should throw for invalid input #4: [] 1`] = `"Invalid position value [] for field "position""`; + +exports[`DataArgProcessor failing inputs validation RATING should throw for invalid input #1: "not-a-rating" 1`] = `"Invalid value 'not-a-rating' for field "ratingField""`; + +exports[`DataArgProcessor failing inputs validation RATING should throw for invalid input #2: {} 1`] = `"Invalid string value {} for text field "ratingField""`; + +exports[`DataArgProcessor failing inputs validation RATING should throw for invalid input #3: [] 1`] = `"Invalid string value [] for text field "ratingField""`; + +exports[`DataArgProcessor failing inputs validation RATING should throw for invalid input #4: true 1`] = `"Invalid string value true for text field "ratingField""`; + +exports[`DataArgProcessor failing inputs validation RATING should throw for invalid input #5: 1 1`] = `"Invalid string value 1 for text field "ratingField""`; + +exports[`DataArgProcessor failing inputs validation RAW_JSON should throw for invalid input #1: "not-a-json" 1`] = `"Invalid object value 'not-a-json' for field "rawJsonField""`; + +exports[`DataArgProcessor failing inputs validation RELATION should throw for invalid input #1: {} 1`] = `"Invalid UUID value {} for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessor failing inputs validation RELATION should throw for invalid input #2: [] 1`] = `"Invalid UUID value [] for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessor failing inputs validation RELATION should throw for invalid input #3: true 1`] = `"Invalid UUID value true for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessor failing inputs validation RELATION should throw for invalid input #4: 1 1`] = `"Invalid UUID value 1 for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessor failing inputs validation RELATION should throw for invalid input #5: "non-uuid" 1`] = `"Invalid UUID value 'non-uuid' for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessor failing inputs validation RICH_TEXT should throw for invalid input #1: "test" 1`] = `"richTextField RICH_TEXT-typed field does not support write operations"`; + +exports[`DataArgProcessor failing inputs validation RICH_TEXT_V2 should throw for invalid input #1: "not-a-rich-text" 1`] = `"Invalid rich text v2 value 'not-a-rich-text' for field "richTextV2Field" - Should be an object"`; + +exports[`DataArgProcessor failing inputs validation RICH_TEXT_V2 should throw for invalid input #2: 1 1`] = `"Invalid rich text v2 value 1 for field "richTextV2Field" - Should be an object"`; + +exports[`DataArgProcessor failing inputs validation RICH_TEXT_V2 should throw for invalid input #3: true 1`] = `"Invalid rich text v2 value true for field "richTextV2Field" - Should be an object"`; + +exports[`DataArgProcessor failing inputs validation SELECT should throw for invalid input #1: "not-a-select-option" 1`] = `"Invalid value 'not-a-select-option' for field "selectField""`; + +exports[`DataArgProcessor failing inputs validation SELECT should throw for invalid input #2: {} 1`] = `"Invalid string value {} for text field "selectField""`; + +exports[`DataArgProcessor failing inputs validation SELECT should throw for invalid input #3: [] 1`] = `"Invalid string value [] for text field "selectField""`; + +exports[`DataArgProcessor failing inputs validation SELECT should throw for invalid input #4: true 1`] = `"Invalid string value true for text field "selectField""`; + +exports[`DataArgProcessor failing inputs validation SELECT should throw for invalid input #5: 1 1`] = `"Invalid string value 1 for text field "selectField""`; + +exports[`DataArgProcessor failing inputs validation TEXT should throw for invalid input #1: {} 1`] = `"Invalid string value {} for text field "textField""`; + +exports[`DataArgProcessor failing inputs validation TEXT should throw for invalid input #2: [] 1`] = `"Invalid string value [] for text field "textField""`; + +exports[`DataArgProcessor failing inputs validation TEXT should throw for invalid input #3: true 1`] = `"Invalid string value true for text field "textField""`; + +exports[`DataArgProcessor failing inputs validation TEXT should throw for invalid input #4: 1 1`] = `"Invalid string value 1 for text field "textField""`; + +exports[`DataArgProcessor failing inputs validation UUID should throw for invalid input #1: {} 1`] = `"Invalid UUID value {} for field "uuidField""`; + +exports[`DataArgProcessor failing inputs validation UUID should throw for invalid input #2: [] 1`] = `"Invalid UUID value [] for field "uuidField""`; + +exports[`DataArgProcessor failing inputs validation UUID should throw for invalid input #3: true 1`] = `"Invalid UUID value true for field "uuidField""`; + +exports[`DataArgProcessor failing inputs validation UUID should throw for invalid input #4: 1 1`] = `"Invalid UUID value 1 for field "uuidField""`; + +exports[`DataArgProcessor failing inputs validation UUID should throw for invalid input #5: "non-uuid" 1`] = `"Invalid UUID value 'non-uuid' for field "uuidField""`; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/failing-inputs-by-field-metadata-type.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/failing-inputs-by-field-metadata-type.constant.ts new file mode 100644 index 0000000000..25cc79a267 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/failing-inputs-by-field-metadata-type.constant.ts @@ -0,0 +1,202 @@ +import { joinColumnNameForManyToOneMorphRelationField1 } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +export const failingInputsByFieldMetadataType: { + [K in FieldMetadataType]?: { + input: Record; + }[]; +} = { + [FieldMetadataType.TEXT]: [ + { input: { textField: {} } }, + { input: { textField: [] } }, + { input: { textField: true } }, + { input: { textField: 1 } }, + ], + [FieldMetadataType.NUMBER]: [ + { input: { numberField: {} } }, + { input: { numberField: [] } }, + { input: { numberField: true } }, + { input: { numberField: 'string' } }, + ], + [FieldMetadataType.UUID]: [ + { input: { uuidField: {} } }, + { input: { uuidField: [] } }, + { input: { uuidField: true } }, + { input: { uuidField: 1 } }, + { input: { uuidField: 'non-uuid' } }, + ], + [FieldMetadataType.SELECT]: [ + { input: { selectField: 'not-a-select-option' } }, + { input: { selectField: {} } }, + { input: { selectField: [] } }, + { input: { selectField: true } }, + { input: { selectField: 1 } }, + ], + [FieldMetadataType.RELATION]: [ + { input: { manyToOneRelationFieldId: {} } }, + { input: { manyToOneRelationFieldId: [] } }, + { input: { manyToOneRelationFieldId: true } }, + { input: { manyToOneRelationFieldId: 1 } }, + { input: { manyToOneRelationFieldId: 'non-uuid' } }, + ], + [FieldMetadataType.RAW_JSON]: [{ input: { rawJsonField: 'not-a-json' } }], + [FieldMetadataType.ARRAY]: [ + { input: { arrayField: true } }, + { input: { arrayField: 1 } }, + ], + [FieldMetadataType.MORPH_RELATION]: [ + { + input: { + [joinColumnNameForManyToOneMorphRelationField1]: 'not-a-morph-relation', + }, + }, + { input: { [joinColumnNameForManyToOneMorphRelationField1]: {} } }, + { input: { [joinColumnNameForManyToOneMorphRelationField1]: [] } }, + { input: { [joinColumnNameForManyToOneMorphRelationField1]: true } }, + { input: { [joinColumnNameForManyToOneMorphRelationField1]: 1 } }, + ], + [FieldMetadataType.RATING]: [ + { input: { ratingField: 'not-a-rating' } }, + { input: { ratingField: {} } }, + { input: { ratingField: [] } }, + { input: { ratingField: true } }, + { input: { ratingField: 1 } }, + ], + [FieldMetadataType.MULTI_SELECT]: [ + { input: { multiSelectField: 'not-a-select-option' } }, + { input: { multiSelectField: {} } }, + { input: { multiSelectField: true } }, + { input: { multiSelectField: 1 } }, + ], + [FieldMetadataType.DATE]: [ + { input: { dateField: 'malformed-date' } }, + { input: { dateField: {} } }, + { input: { dateField: [] } }, + { input: { dateField: true } }, + { input: { dateField: 1 } }, + { input: { dateField: '2024' } }, + { input: { dateField: '2024-01' } }, + { input: { dateField: '2024-13-01' } }, + { input: { dateField: '2024-02-31' } }, + ], + [FieldMetadataType.DATE_TIME]: [ + { input: { dateTimeField: 'malformed-date' } }, + { input: { dateTimeField: {} } }, + { input: { dateTimeField: [] } }, + { input: { dateTimeField: true } }, + { input: { dateTimeField: 1 } }, + { input: { dateTimeField: '2024' } }, + { input: { dateTimeField: '2024-01' } }, + { input: { dateTimeField: '2024-13-01T10:30:00Z' } }, + { input: { dateTimeField: '2024-01-15T25:30:00Z' } }, + ], + [FieldMetadataType.BOOLEAN]: [ + { input: { booleanField: {} } }, + { input: { booleanField: [] } }, + { input: { booleanField: 'string' } }, + { input: { booleanField: 1 } }, + ], + [FieldMetadataType.RICH_TEXT]: [{ input: { richTextField: 'test' } }], + [FieldMetadataType.ADDRESS]: [ + { input: { addressField: 'not-an-address' } }, + { input: { addressField: 1 } }, + { input: { addressField: true } }, + ], + [FieldMetadataType.CURRENCY]: [ + { input: { currencyField: 'not-a-currency' } }, + { input: { currencyField: 1 } }, + { input: { currencyField: true } }, + ], + [FieldMetadataType.EMAILS]: [ + { input: { emailsField: 'not-an-email' } }, + { input: { emailsField: { primaryEmail: 'not-an-email' } } }, + { input: { emailsField: { additionalEmails: 'not-an-email' } } }, + { input: { emailsField: { additionalEmails: ['not-an-email'] } } }, + { + input: { + emailsField: { + primaryEmail: 'email@email.com', + additionalEmails: ['not-an-email'], + }, + }, + }, + { + input: { + emailsField: { + primaryEmail: 'not-an-email', + additionalEmails: ['additional@email.com'], + }, + }, + }, + { + input: { + emailsField: { + additionalEmails: ['not-an-email', 'additional@email.com'], + }, + }, + }, + ], + [FieldMetadataType.PHONES]: [ + { input: { phonesField: 'not-a-phone' } }, + { input: { phonesField: 1 } }, + { input: { phonesField: true } }, + ], + [FieldMetadataType.FULL_NAME]: [ + { input: { fullNameField: 'not-a-full-name' } }, + { input: { fullNameField: 1 } }, + { input: { fullNameField: true } }, + ], + [FieldMetadataType.LINKS]: [ + { input: { linksField: 'not-a-link' } }, + { input: { linksField: 1 } }, + { input: { linksField: true } }, + ], + [FieldMetadataType.RICH_TEXT_V2]: [ + { input: { richTextV2Field: 'not-a-rich-text' } }, + { input: { richTextV2Field: 1 } }, + { input: { richTextV2Field: true } }, + ], + [FieldMetadataType.POSITION]: [ + { input: { position: 'not-a-position' } }, + { input: { position: NaN } }, + { input: { position: {} } }, + { input: { position: [] } }, + ], + [FieldMetadataType.FILES]: [ + { input: { filesField: 'not-an-addFiles-property' } }, + { input: { filesField: { addFiles: [{ invalidField: 'test' }] } } }, + { + input: { + filesField: { addFiles: [{ fileId: 'not-a-uuid', label: 'Doc.pdf' }] }, + }, + }, + { + input: { + filesField: [ + { + addFiles: [ + { fileId: '550e8400-e29b-41d4-a716-446655440000', label: 12345 }, + ], + }, + ], + }, + }, + { + input: { + filesField: { + addFiles: [ + { + fileId: '550e8400-e29b-41d4-a716-446655440000', + label: 'Document.pdf', + extension: 'not-allowed-in-input', + }, + ], + }, + }, + }, + ], + [FieldMetadataType.NUMERIC]: [ + { input: { numericField: {} } }, + { input: { numericField: 'not-a-number' } }, + ], +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts new file mode 100644 index 0000000000..a07d592f90 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant.ts @@ -0,0 +1,172 @@ +import { joinColumnNameForManyToOneMorphRelationField1 } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; +import { + type FieldMetadataDefaultOption, + type FieldMetadataOptions, + type FieldMetadataSettings, + FieldMetadataType, + RelationType, +} from 'twenty-shared/types'; + +export type FieldMetadataConfig = { + name: string; + type: FieldMetadataType; + isNullable: boolean; + options?: FieldMetadataOptions; + settings?: FieldMetadataSettings; + defaultValue?: unknown; +}; + +export const fieldMetadataConfigByFieldName: Record< + string, + FieldMetadataConfig +> = { + textField: { + name: 'textField', + type: FieldMetadataType.TEXT, + isNullable: true, + }, + numberField: { + name: 'numberField', + type: FieldMetadataType.NUMBER, + isNullable: true, + }, + numericField: { + name: 'numericField', + type: FieldMetadataType.NUMERIC, + isNullable: true, + }, + uuidField: { + name: 'uuidField', + type: FieldMetadataType.UUID, + isNullable: true, + }, + selectField: { + name: 'selectField', + type: FieldMetadataType.SELECT, + isNullable: true, + options: [ + { value: 'OPTION_1' }, + { value: 'OPTION_2' }, + ] as FieldMetadataDefaultOption[], + }, + manyToOneRelationFieldId: { + name: 'manyToOneRelationFieldId', + type: FieldMetadataType.RELATION, + isNullable: true, + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'manyToOneRelationFieldId', + }, + }, + rawJsonField: { + name: 'rawJsonField', + type: FieldMetadataType.RAW_JSON, + isNullable: true, + }, + arrayField: { + name: 'arrayField', + type: FieldMetadataType.ARRAY, + isNullable: true, + }, + ratingField: { + name: 'ratingField', + type: FieldMetadataType.RATING, + isNullable: true, + options: [ + { value: 'RATING_1' }, + { value: 'RATING_2' }, + { value: 'RATING_3' }, + { value: 'RATING_4' }, + { value: 'RATING_5' }, + ] as FieldMetadataDefaultOption[], + }, + multiSelectField: { + name: 'multiSelectField', + type: FieldMetadataType.MULTI_SELECT, + isNullable: true, + options: [ + { value: 'OPTION_1' }, + { value: 'OPTION_2' }, + ] as FieldMetadataDefaultOption[], + }, + dateField: { + name: 'dateField', + type: FieldMetadataType.DATE, + isNullable: true, + }, + dateTimeField: { + name: 'dateTimeField', + type: FieldMetadataType.DATE_TIME, + isNullable: true, + }, + booleanField: { + name: 'booleanField', + type: FieldMetadataType.BOOLEAN, + isNullable: true, + }, + addressField: { + name: 'addressField', + type: FieldMetadataType.ADDRESS, + isNullable: true, + }, + currencyField: { + name: 'currencyField', + type: FieldMetadataType.CURRENCY, + isNullable: true, + }, + emailsField: { + name: 'emailsField', + type: FieldMetadataType.EMAILS, + isNullable: true, + }, + phonesField: { + name: 'phonesField', + type: FieldMetadataType.PHONES, + isNullable: true, + }, + fullNameField: { + name: 'fullNameField', + type: FieldMetadataType.FULL_NAME, + isNullable: true, + }, + linksField: { + name: 'linksField', + type: FieldMetadataType.LINKS, + isNullable: true, + }, + richTextV2Field: { + name: 'richTextV2Field', + type: FieldMetadataType.RICH_TEXT_V2, + isNullable: true, + }, + richTextField: { + name: 'richTextField', + type: FieldMetadataType.RICH_TEXT, + isNullable: true, + }, + position: { + name: 'position', + type: FieldMetadataType.POSITION, + isNullable: true, + }, + filesField: { + name: 'filesField', + type: FieldMetadataType.FILES, + isNullable: true, + settings: {} as FieldMetadataSettings, + }, + actorField: { + name: 'actorField', + type: FieldMetadataType.ACTOR, + isNullable: true, + }, + [joinColumnNameForManyToOneMorphRelationField1]: { + name: joinColumnNameForManyToOneMorphRelationField1, + type: FieldMetadataType.MORPH_RELATION, + isNullable: true, + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: joinColumnNameForManyToOneMorphRelationField1, + }, + }, +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts new file mode 100644 index 0000000000..89bc9a9f71 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/successful-inputs-by-field-metadata-type.constant.ts @@ -0,0 +1,448 @@ +import { joinColumnNameForManyToOneMorphRelationField1 } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const TEST_UUID = '20202020-b21e-4ec2-873b-de4264d89021'; + +export const successfulInputsByFieldMetadataType: { + [K in FieldMetadataType]?: { + input: Record; + expectedOutput: Record; + }[]; +} = { + [FieldMetadataType.TEXT]: [ + { input: { textField: 'test' }, expectedOutput: { textField: 'test' } }, + { input: { textField: '' }, expectedOutput: { textField: null } }, + { input: { textField: null }, expectedOutput: { textField: null } }, + ], + [FieldMetadataType.NUMBER]: [ + { input: { numberField: 1 }, expectedOutput: { numberField: 1 } }, + { input: { numberField: null }, expectedOutput: { numberField: null } }, + { input: { numberField: 0 }, expectedOutput: { numberField: 0 } }, + { input: { numberField: -1.1 }, expectedOutput: { numberField: -1.1 } }, + ], + [FieldMetadataType.UUID]: [ + { + input: { uuidField: TEST_UUID }, + expectedOutput: { uuidField: TEST_UUID }, + }, + { input: { uuidField: null }, expectedOutput: { uuidField: null } }, + ], + [FieldMetadataType.SELECT]: [ + { + input: { selectField: 'OPTION_1' }, + expectedOutput: { selectField: 'OPTION_1' }, + }, + { input: { selectField: null }, expectedOutput: { selectField: null } }, + ], + [FieldMetadataType.RELATION]: [ + { + input: { manyToOneRelationFieldId: TEST_UUID }, + expectedOutput: { manyToOneRelationFieldId: TEST_UUID }, + }, + { + input: { manyToOneRelationFieldId: null }, + expectedOutput: { manyToOneRelationFieldId: null }, + }, + ], + [FieldMetadataType.MORPH_RELATION]: [ + { + input: { [joinColumnNameForManyToOneMorphRelationField1]: TEST_UUID }, + expectedOutput: { + [joinColumnNameForManyToOneMorphRelationField1]: TEST_UUID, + }, + }, + { + input: { [joinColumnNameForManyToOneMorphRelationField1]: null }, + expectedOutput: { [joinColumnNameForManyToOneMorphRelationField1]: null }, + }, + ], + [FieldMetadataType.RAW_JSON]: [ + { + input: { rawJsonField: { key: 'value' } }, + expectedOutput: { rawJsonField: { key: 'value' } }, + }, + { input: { rawJsonField: {} }, expectedOutput: { rawJsonField: null } }, + { input: { rawJsonField: null }, expectedOutput: { rawJsonField: null } }, + { + input: { rawJsonField: '{"key": "value"}' }, + expectedOutput: { rawJsonField: '{"key": "value"}' }, + }, + ], + [FieldMetadataType.ARRAY]: [ + { + input: { arrayField: ['item1', 'item2'] }, + expectedOutput: { arrayField: ['item1', 'item2'] }, + }, + { + input: { arrayField: 'item1' }, + expectedOutput: { arrayField: ['item1'] }, + }, + { input: { arrayField: [] }, expectedOutput: { arrayField: null } }, + { input: { arrayField: null }, expectedOutput: { arrayField: null } }, + ], + [FieldMetadataType.RATING]: [ + { + input: { ratingField: 'RATING_2' }, + expectedOutput: { ratingField: 'RATING_2' }, + }, + { input: { ratingField: null }, expectedOutput: { ratingField: null } }, + ], + [FieldMetadataType.MULTI_SELECT]: [ + { + input: { multiSelectField: ['OPTION_1'] }, + expectedOutput: { multiSelectField: ['OPTION_1'] }, + }, + { + input: { multiSelectField: [] }, + expectedOutput: { multiSelectField: null }, + }, + { + input: { multiSelectField: null }, + expectedOutput: { multiSelectField: null }, + }, + ], + [FieldMetadataType.DATE]: [ + { input: { dateField: null }, expectedOutput: { dateField: null } }, + { + input: { dateField: '2025-01-13' }, + expectedOutput: { dateField: '2025-01-13' }, + }, + { + input: { dateField: '20250113' }, + expectedOutput: { dateField: '20250113' }, + }, + { + input: { dateField: '2025.01.13' }, + expectedOutput: { dateField: '2025.01.13' }, + }, + { + input: { dateField: '2025/01/13' }, + expectedOutput: { dateField: '2025/01/13' }, + }, + { + input: { dateField: '01-13-2025' }, + expectedOutput: { dateField: '01-13-2025' }, + }, + { + input: { dateField: '01/13/2025' }, + expectedOutput: { dateField: '01/13/2025' }, + }, + { + input: { dateField: '01.13.2025' }, + expectedOutput: { dateField: '01.13.2025' }, + }, + { + input: { dateField: 'January 13, 2025' }, + expectedOutput: { dateField: 'January 13, 2025' }, + }, + { + input: { dateField: 'Jan 13, 2025' }, + expectedOutput: { dateField: 'Jan 13, 2025' }, + }, + { + input: { dateField: '13 January 2025' }, + expectedOutput: { dateField: '13 January 2025' }, + }, + { + input: { dateField: '13 Jan 2025' }, + expectedOutput: { dateField: '13 Jan 2025' }, + }, + { + input: { dateField: '13-Jan-2025' }, + expectedOutput: { dateField: '13-Jan-2025' }, + }, + { + input: { dateField: '2025-Jan-13' }, + expectedOutput: { dateField: '2025-Jan-13' }, + }, + { + input: { dateField: '2025-01-13T10:30:00.000Z' }, + expectedOutput: { dateField: '2025-01-13T10:30:00.000Z' }, + }, + { + input: { dateField: '2025-01-13T10:30:00Z' }, + expectedOutput: { dateField: '2025-01-13T10:30:00Z' }, + }, + { + input: { dateField: '2025-01-13T10:30:00.000' }, + expectedOutput: { dateField: '2025-01-13T10:30:00.000' }, + }, + { + input: { dateField: '2025-01-13T10:30:00' }, + expectedOutput: { dateField: '2025-01-13T10:30:00' }, + }, + { + input: { dateField: '2025-01-13 10:30:00' }, + expectedOutput: { dateField: '2025-01-13 10:30:00' }, + }, + { + input: { dateField: '2025-01-13 10:30:00.000' }, + expectedOutput: { dateField: '2025-01-13 10:30:00.000' }, + }, + ], + [FieldMetadataType.DATE_TIME]: [ + { input: { dateTimeField: null }, expectedOutput: { dateTimeField: null } }, + { + input: { dateTimeField: '2025-01-13T10:30:00.000Z' }, + expectedOutput: { dateTimeField: '2025-01-13T10:30:00.000Z' }, + }, + { + input: { dateTimeField: '2025-01-13T10:30:00Z' }, + expectedOutput: { dateTimeField: '2025-01-13T10:30:00Z' }, + }, + { + input: { dateTimeField: '2025-01-13T10:30:00.000+02:00' }, + expectedOutput: { dateTimeField: '2025-01-13T10:30:00.000+02:00' }, + }, + { + input: { dateTimeField: '2025-01-13T10:30:00+02:00' }, + expectedOutput: { dateTimeField: '2025-01-13T10:30:00+02:00' }, + }, + { + input: { dateTimeField: '2025-01-13T10:30:00.000' }, + expectedOutput: { dateTimeField: '2025-01-13T10:30:00.000' }, + }, + { + input: { dateTimeField: '2025-01-13T10:30:00' }, + expectedOutput: { dateTimeField: '2025-01-13T10:30:00' }, + }, + { + input: { dateTimeField: '2025-01-13 10:30:00.000' }, + expectedOutput: { dateTimeField: '2025-01-13 10:30:00.000' }, + }, + { + input: { dateTimeField: '2025-01-13 10:30:00' }, + expectedOutput: { dateTimeField: '2025-01-13 10:30:00' }, + }, + { + input: { dateTimeField: '2025-01-13 10:30' }, + expectedOutput: { dateTimeField: '2025-01-13 10:30' }, + }, + { + input: { dateTimeField: '2025-01-13' }, + expectedOutput: { dateTimeField: '2025-01-13' }, + }, + { + input: { dateTimeField: '20250113' }, + expectedOutput: { dateTimeField: '20250113' }, + }, + { + input: { dateTimeField: '2025.01.13' }, + expectedOutput: { dateTimeField: '2025.01.13' }, + }, + { + input: { dateTimeField: '2025/01/13' }, + expectedOutput: { dateTimeField: '2025/01/13' }, + }, + { + input: { dateTimeField: '01-13-2025' }, + expectedOutput: { dateTimeField: '01-13-2025' }, + }, + { + input: { dateTimeField: '01/13/2025' }, + expectedOutput: { dateTimeField: '01/13/2025' }, + }, + { + input: { dateTimeField: '01.13.2025' }, + expectedOutput: { dateTimeField: '01.13.2025' }, + }, + { + input: { dateTimeField: 'January 13, 2025' }, + expectedOutput: { dateTimeField: 'January 13, 2025' }, + }, + { + input: { dateTimeField: 'Jan 13, 2025' }, + expectedOutput: { dateTimeField: 'Jan 13, 2025' }, + }, + { + input: { dateTimeField: '13 January 2025' }, + expectedOutput: { dateTimeField: '13 January 2025' }, + }, + { + input: { dateTimeField: '13 Jan 2025' }, + expectedOutput: { dateTimeField: '13 Jan 2025' }, + }, + { + input: { dateTimeField: '13-Jan-2025' }, + expectedOutput: { dateTimeField: '13-Jan-2025' }, + }, + { + input: { dateTimeField: '2025-Jan-13' }, + expectedOutput: { dateTimeField: '2025-Jan-13' }, + }, + ], + [FieldMetadataType.BOOLEAN]: [ + { input: { booleanField: true }, expectedOutput: { booleanField: true } }, + { input: { booleanField: false }, expectedOutput: { booleanField: false } }, + { input: { booleanField: null }, expectedOutput: { booleanField: null } }, + ], + [FieldMetadataType.ADDRESS]: [ + { + input: { + addressField: { + addressPostcode: 'postcode', + addressStreet1: 'street 1', + addressStreet2: 'street 2', + addressCity: 'city', + addressState: 'state', + addressCountry: 'country', + }, + }, + expectedOutput: { + addressField: { + addressPostcode: 'postcode', + addressStreet1: 'street 1', + addressStreet2: 'street 2', + addressCity: 'city', + addressState: 'state', + addressCountry: 'country', + addressLat: undefined, + addressLng: undefined, + }, + }, + }, + { input: { addressField: null }, expectedOutput: { addressField: null } }, + ], + [FieldMetadataType.CURRENCY]: [ + { + input: { currencyField: { amountMicros: 1000000, currencyCode: 'USD' } }, + expectedOutput: { + currencyField: { amountMicros: 1000000, currencyCode: 'USD' }, + }, + }, + { input: { currencyField: null }, expectedOutput: { currencyField: null } }, + ], + [FieldMetadataType.EMAILS]: [ + { + input: { + emailsField: { + primaryEmail: 'test@test.com', + additionalEmails: ['test2@test.com'], + }, + }, + expectedOutput: { + emailsField: { + primaryEmail: 'test@test.com', + additionalEmails: '["test2@test.com"]', + }, + }, + }, + { input: { emailsField: null }, expectedOutput: { emailsField: null } }, + ], + [FieldMetadataType.PHONES]: [ + { + input: { + phonesField: { + primaryPhoneNumber: '1234567890', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', + additionalPhones: [ + { number: '1234567890', callingCode: '+33', countryCode: 'FR' }, + ], + }, + }, + expectedOutput: { + phonesField: { + primaryPhoneNumber: '1234567890', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', + additionalPhones: + '[{"countryCode":"FR","callingCode":"+33","number":"1234567890"}]', + }, + }, + }, + { input: { phonesField: null }, expectedOutput: { phonesField: null } }, + ], + [FieldMetadataType.FULL_NAME]: [ + { + input: { fullNameField: { firstName: 'John', lastName: 'Doe' } }, + expectedOutput: { + fullNameField: { firstName: 'John', lastName: 'Doe' }, + }, + }, + { input: { fullNameField: null }, expectedOutput: { fullNameField: null } }, + ], + [FieldMetadataType.LINKS]: [ + { + input: { + linksField: { + primaryLinkUrl: 'https://twenty.com', + primaryLinkLabel: 'Twenty', + secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }], + }, + }, + expectedOutput: { + linksField: { + primaryLinkUrl: 'https://twenty.com', + primaryLinkLabel: 'Twenty', + secondaryLinks: '[{"url":"twenty.com","label":"Twenty"}]', + }, + }, + }, + { input: { linksField: null }, expectedOutput: { linksField: null } }, + ], + [FieldMetadataType.RICH_TEXT_V2]: [ + { + input: { richTextV2Field: { blocknote: 'test', markdown: 'test' } }, + expectedOutput: { + richTextV2Field: { blocknote: 'test', markdown: 'test' }, + }, + }, + { + input: { richTextV2Field: null }, + expectedOutput: { richTextV2Field: null }, + }, + ], + [FieldMetadataType.POSITION]: [ + { input: { position: 1000 }, expectedOutput: { position: 1000 } }, + { input: { position: 0 }, expectedOutput: { position: 0 } }, + { input: { position: -100 }, expectedOutput: { position: -100 } }, + ], + [FieldMetadataType.FILES]: [ + { + input: { + filesField: [ + { fileId: '550e8400-e29b-41d4-a716-446655440000', label: 'Doc.pdf' }, + ], + }, + expectedOutput: { + filesField: [ + { fileId: '550e8400-e29b-41d4-a716-446655440000', label: 'Doc.pdf' }, + ], + }, + }, + { input: { filesField: [] }, expectedOutput: { filesField: null } }, + { input: { filesField: null }, expectedOutput: { filesField: null } }, + ], + [FieldMetadataType.NUMERIC]: [ + { + input: { numericField: '123.45' }, + expectedOutput: { numericField: 123.45 }, + }, + { + input: { numericField: 123.45 }, + expectedOutput: { numericField: 123.45 }, + }, + { input: { numericField: null }, expectedOutput: { numericField: null } }, + ], + [FieldMetadataType.ACTOR]: [ + { + input: { + actorField: { + source: 'MANUAL', + name: 'John Doe', + workspaceMemberId: TEST_UUID, + }, + }, + expectedOutput: { + actorField: { + source: 'MANUAL', + name: 'John Doe', + workspaceMemberId: TEST_UUID, + context: undefined, + }, + }, + }, + { input: { actorField: null }, expectedOutput: { actorField: null } }, + ], +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg.processor.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg.processor.spec.ts new file mode 100644 index 0000000000..fba0e5c76c --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg.processor.spec.ts @@ -0,0 +1,195 @@ +import { Test, type TestingModule } from '@nestjs/testing'; + +import { FieldMetadataType } from 'twenty-shared/types'; + +import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor'; +import { type 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 { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +import { failingInputsByFieldMetadataType } from './constants/failing-inputs-by-field-metadata-type.constant'; +import { fieldMetadataConfigByFieldName } from './constants/field-metadata-config-by-field-name.constant'; +import { successfulInputsByFieldMetadataType } from './constants/successful-inputs-by-field-metadata-type.constant'; + +// Mock the rich text v2 transformation to avoid BlockNote module issues +jest.mock( + 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util', + () => ({ + transformRichTextV2Value: jest.fn().mockImplementation((value) => value), + }), +); + +describe('DataArgProcessor', () => { + let dataArgProcessor: DataArgProcessor; + let recordPositionService: jest.Mocked; + + const mockWorkspaceId = '20202020-1234-1234-1234-123456789012'; + + const createMockAuthContext = (): AuthContext => + ({ + workspace: { + id: mockWorkspaceId, + }, + }) as AuthContext; + + const createFlatFieldMetadataMaps = ( + fieldNames: string[], + ): FlatEntityMaps => { + const byUniversalIdentifier: Record = {}; + const universalIdentifierById: Record = {}; + + for (const fieldName of fieldNames) { + const config = fieldMetadataConfigByFieldName[fieldName]; + + if (!config) { + throw new Error(`No config found for field: ${fieldName}`); + } + + const fieldId = `${fieldName}-id`; + const universalId = `${fieldName}-universal-id`; + + byUniversalIdentifier[universalId] = { + id: fieldId, + name: fieldName, + type: config.type ?? FieldMetadataType.TEXT, + isNullable: config.isNullable ?? true, + objectMetadataId: 'object-id', + universalIdentifier: universalId, + options: config.options, + settings: config.settings, + defaultValue: config.defaultValue, + } as FlatFieldMetadata; + + universalIdentifierById[fieldId] = universalId; + } + + return { + byUniversalIdentifier, + universalIdentifierById, + universalIdentifiersByApplicationId: {}, + }; + }; + + const createFlatObjectMetadata = (fieldNames: string[]): FlatObjectMetadata => + ({ + id: 'object-id', + nameSingular: 'testObject', + namePlural: 'testObjects', + isCustom: false, + fieldIds: fieldNames.map((name) => `${name}-id`), + universalIdentifier: 'test-object-universal-id', + labelIdentifierFieldMetadataUniversalIdentifier: null, + imageIdentifierFieldMetadataUniversalIdentifier: null, + }) as FlatObjectMetadata; + + beforeEach(async () => { + recordPositionService = { + overridePositionOnRecords: jest + .fn() + .mockImplementation(({ partialRecordInputs }) => partialRecordInputs), + buildRecordPosition: jest.fn(), + findByPosition: jest.fn(), + updatePosition: jest.fn(), + } as unknown as jest.Mocked; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DataArgProcessor, + { + provide: RecordPositionService, + useValue: recordPositionService, + }, + ], + }).compile(); + + dataArgProcessor = module.get(DataArgProcessor); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(dataArgProcessor).toBeDefined(); + }); + + describe('failing inputs validation', () => { + const fieldMetadataTypesToTest = Object.keys( + failingInputsByFieldMetadataType, + ) as FieldMetadataType[]; + + for (const fieldMetadataType of fieldMetadataTypesToTest) { + const testCases = failingInputsByFieldMetadataType[fieldMetadataType]; + + if (!testCases) { + continue; + } + + describe(`${fieldMetadataType}`, () => { + for (const [index, testCase] of testCases.entries()) { + const fieldName = Object.keys(testCase.input)[0]; + const fieldValue = testCase.input[fieldName]; + + it(`should throw for invalid input #${index + 1}: ${JSON.stringify(fieldValue)}`, async () => { + const fieldNames = [fieldName]; + + const flatFieldMetadataMaps = + createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + await expect( + dataArgProcessor.process({ + partialRecordInputs: [testCase.input], + authContext: createMockAuthContext(), + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + } + }); + } + }); + + describe('successful inputs validation', () => { + const fieldMetadataTypesToTest = Object.keys( + successfulInputsByFieldMetadataType, + ) as FieldMetadataType[]; + + for (const fieldMetadataType of fieldMetadataTypesToTest) { + const testCases = successfulInputsByFieldMetadataType[fieldMetadataType]; + + if (!testCases) { + continue; + } + + describe(`${fieldMetadataType}`, () => { + for (const [index, testCase] of testCases.entries()) { + const fieldName = Object.keys(testCase.input)[0]; + const fieldValue = testCase.input[fieldName]; + + it(`should process valid input #${index + 1}: ${JSON.stringify(fieldValue)}`, async () => { + const fieldNames = [fieldName]; + + const flatFieldMetadataMaps = + createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const result = await dataArgProcessor.process({ + partialRecordInputs: [testCase.input], + authContext: createMockAuthContext(), + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(testCase.expectedOutput); + }); + } + }); + } + }); +}); 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 index 0a8e444660..46c3c9c099 100644 --- 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 @@ -27,7 +27,8 @@ import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-p 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 { validateDateFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-field-or-throw.util'; +import { validateDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-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 { validateFilesFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-files-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'; @@ -184,8 +185,9 @@ export class DataArgProcessor { return transformTextField(validatedValue); } case FieldMetadataType.DATE_TIME: + return validateDateTimeFieldOrThrow(value, key); case FieldMetadataType.DATE: - return validateDateAndDateTimeFieldOrThrow(value, key); + return validateDateFieldOrThrow(value, key); case FieldMetadataType.BOOLEAN: return validateBooleanFieldOrThrow(value, key); case FieldMetadataType.RATING: 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 deleted file mode 100644 index da4a9f0543..0000000000 --- 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 +++ /dev/null @@ -1,97 +0,0 @@ -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-date-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-field-or-throw.util.spec.ts new file mode 100644 index 0000000000..38a39efcb7 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-field-or-throw.util.spec.ts @@ -0,0 +1,191 @@ +import { validateDateFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateDateFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateDateFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the value when it is a valid ISO date string (YYYY-MM-DD)', () => { + const dateString = '2024-01-15'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid ISO compact date string (YYYYMMDD)', () => { + const dateString = '20240115'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date with dots (YYYY.MM.DD)', () => { + const dateString = '2024.01.15'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date with slashes (YYYY/MM/DD)', () => { + const dateString = '2024/01/15'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date in MM-DD-YYYY format', () => { + const dateString = '01-15-2024'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date in MM/DD/YYYY format', () => { + const dateString = '01/15/2024'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date with full month name (MMMM d, yyyy)', () => { + const dateString = 'January 15, 2024'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date with abbreviated month name (MMM d, yyyy)', () => { + const dateString = 'Jan 15, 2024'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date in d MMMM yyyy format', () => { + const dateString = '15 January 2024'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date in d MMM yyyy format', () => { + const dateString = '15 Jan 2024'; + const result = validateDateFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid ISO datetime string (time will be ignored by PostgreSQL)', () => { + const datetimeString = '2024-01-15T10:30:00Z'; + const result = validateDateFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid datetime with milliseconds', () => { + const datetimeString = '2024-01-15T10:30:00.000Z'; + const result = validateDateFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid datetime without timezone', () => { + const datetimeString = '2024-01-15T10:30:00'; + const result = validateDateFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid datetime with space separator', () => { + const datetimeString = '2024-01-15 10:30:00'; + const result = validateDateFieldOrThrow(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 = validateDateFieldOrThrow(dateObject, 'testField'); + + expect(result).toBe(dateObject); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is just a year', () => { + expect(() => validateDateFieldOrThrow('2024', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is year and month only', () => { + expect(() => validateDateFieldOrThrow('2024-01', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an invalid date string', () => { + expect(() => + validateDateFieldOrThrow('invalid-date', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an empty string', () => { + expect(() => validateDateFieldOrThrow('', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a boolean', () => { + expect(() => validateDateFieldOrThrow(true, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an array', () => { + expect(() => validateDateFieldOrThrow([], 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an object', () => { + expect(() => validateDateFieldOrThrow({}, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is undefined', () => { + expect(() => validateDateFieldOrThrow(undefined, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a number (timestamp)', () => { + expect(() => validateDateFieldOrThrow(1234567890, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a random string', () => { + expect(() => + validateDateFieldOrThrow('hello world', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a date with invalid month', () => { + expect(() => validateDateFieldOrThrow('2024-13-01', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a date with invalid day', () => { + expect(() => validateDateFieldOrThrow('2024-02-31', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-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-time-field-or-throw.util.spec.ts new file mode 100644 index 0000000000..5e39c99d7c --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-date-time-field-or-throw.util.spec.ts @@ -0,0 +1,189 @@ +import { validateDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-time-field-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateDateTimeFieldOrThrow', () => { + describe('valid inputs', () => { + it('should return null when value is null', () => { + const result = validateDateTimeFieldOrThrow(null, 'testField'); + + expect(result).toBeNull(); + }); + + it('should return the value when it is a valid ISO datetime string with Z timezone', () => { + const datetimeString = '2024-01-15T10:30:00Z'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid ISO datetime string with milliseconds and Z timezone', () => { + const datetimeString = '2024-01-15T10:30:00.000Z'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid ISO datetime string with timezone offset', () => { + const datetimeString = '2024-01-15T10:30:00+02:00'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid ISO datetime string with milliseconds and timezone offset', () => { + const datetimeString = '2024-01-15T10:30:00.000+02:00'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid ISO datetime string without timezone', () => { + const datetimeString = '2024-01-15T10:30:00'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid ISO datetime string with milliseconds without timezone', () => { + const datetimeString = '2024-01-15T10:30:00.000'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid datetime with space separator', () => { + const datetimeString = '2024-01-15 10:30:00'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid datetime with space separator and milliseconds', () => { + const datetimeString = '2024-01-15 10:30:00.000'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid datetime with space separator without seconds', () => { + const datetimeString = '2024-01-15 10:30'; + const result = validateDateTimeFieldOrThrow(datetimeString, 'testField'); + + expect(result).toBe(datetimeString); + }); + + it('should return the value when it is a valid ISO date string (will be treated as midnight)', () => { + const dateString = '2024-01-15'; + const result = validateDateTimeFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid ISO compact date string', () => { + const dateString = '20240115'; + const result = validateDateTimeFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a valid date with full month name', () => { + const dateString = 'January 15, 2024'; + const result = validateDateTimeFieldOrThrow(dateString, 'testField'); + + expect(result).toBe(dateString); + }); + + it('should return the value when it is a Date object', () => { + const dateObject = new Date('2024-01-15T10:30:00Z'); + const result = validateDateTimeFieldOrThrow(dateObject, 'testField'); + + expect(result).toBe(dateObject); + }); + }); + + describe('invalid inputs', () => { + it('should throw when value is just a year', () => { + expect(() => validateDateTimeFieldOrThrow('2024', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is year and month only', () => { + expect(() => + validateDateTimeFieldOrThrow('2024-01', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an invalid datetime string', () => { + expect(() => + validateDateTimeFieldOrThrow('invalid-datetime', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an empty string', () => { + expect(() => validateDateTimeFieldOrThrow('', 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is a boolean', () => { + expect(() => validateDateTimeFieldOrThrow(true, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an array', () => { + expect(() => validateDateTimeFieldOrThrow([], 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is an object', () => { + expect(() => validateDateTimeFieldOrThrow({}, 'testField')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is undefined', () => { + expect(() => + validateDateTimeFieldOrThrow(undefined, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a number (timestamp)', () => { + expect(() => + validateDateTimeFieldOrThrow(1234567890, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a random string', () => { + expect(() => + validateDateTimeFieldOrThrow('hello world', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a datetime with invalid month', () => { + expect(() => + validateDateTimeFieldOrThrow('2024-13-01T10:30:00Z', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a datetime with invalid day', () => { + expect(() => + validateDateTimeFieldOrThrow('2024-02-31T10:30:00Z', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a datetime with invalid hour', () => { + expect(() => + validateDateTimeFieldOrThrow('2024-01-15T25:30:00Z', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is a datetime with invalid minute', () => { + expect(() => + validateDateTimeFieldOrThrow('2024-01-15T10:60:00Z', 'testField'), + ).toThrow(CommonQueryRunnerException); + }); + }); +}); 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 deleted file mode 100644 index dbc5e9d6ec..0000000000 --- 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 +++ /dev/null @@ -1,31 +0,0 @@ -import { inspect } from 'util'; - -import { msg } from '@lingui/core/macro'; -import { isDate, isNull, isNumber, isString } from '@sniptt/guards'; - -import { - CommonQueryRunnerException, - CommonQueryRunnerExceptionCode, -} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; - -// TODO: should be splitted in both validateDate and validateDateTime because both format are different even if Date parses them indeferrently -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; - } - - const inspectedValue = inspect(value); - - throw new CommonQueryRunnerException( - `Invalid value ${inspectedValue} for date or date-time field "${fieldName}"`, - CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, - { userFriendlyMessage: msg`Invalid value for date: "${inspectedValue}"` }, - ); -}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-field-or-throw.util.ts new file mode 100644 index 0000000000..5aaa136902 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-field-or-throw.util.ts @@ -0,0 +1,69 @@ +import { inspect } from 'util'; + +import { msg } from '@lingui/core/macro'; +import { isDate, isNull, isString } from '@sniptt/guards'; +import { isValid, parse } from 'date-fns'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +const ACCEPTED_DATE_FORMATS = [ + 'yyyy-MM-dd', + 'yyyyMMdd', + 'yyyy.MM.dd', + 'yyyy/MM/dd', + 'MM-dd-yyyy', + 'MM/dd/yyyy', + 'MM.dd.yyyy', + 'MMMM d, yyyy', + 'MMM d, yyyy', + 'd MMMM yyyy', + 'd MMM yyyy', + 'dd-MMM-yyyy', + 'yyyy-MMM-dd', + "yyyy-MM-dd'T'HH:mm:ss.SSSX", + "yyyy-MM-dd'T'HH:mm:ssX", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss", + 'yyyy-MM-dd HH:mm:ss', + 'yyyy-MM-dd HH:mm:ss.SSS', +]; + +const isValidDateFormat = (value: string): boolean => { + for (const format of ACCEPTED_DATE_FORMATS) { + const parsed = parse(value, format, new Date()); + + if (isValid(parsed)) { + return true; + } + } + + return false; +}; + +export const validateDateFieldOrThrow = ( + value: unknown, + fieldName: string, +): unknown => { + if (isNull(value)) return null; + + if (isDate(value) && isValid(value)) { + return value; + } + + if (isString(value) && isValidDateFormat(value)) { + return value; + } + + const inspectedValue = inspect(value); + + throw new CommonQueryRunnerException( + `Invalid value ${inspectedValue} for date field "${fieldName}". Expected format: 'YYYY-MM-DD'`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: msg`Invalid value for date: "${inspectedValue}". Expected format: 'YYYY-MM-DD'`, + }, + ); +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-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-time-field-or-throw.util.ts new file mode 100644 index 0000000000..5237e55372 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-time-field-or-throw.util.ts @@ -0,0 +1,72 @@ +import { inspect } from 'util'; + +import { msg } from '@lingui/core/macro'; +import { isDate, isNull, isString } from '@sniptt/guards'; +import { isValid, parse } from 'date-fns'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +const ACCEPTED_DATE_TIME_FORMATS = [ + "yyyy-MM-dd'T'HH:mm:ss.SSSX", + "yyyy-MM-dd'T'HH:mm:ssX", + "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", + "yyyy-MM-dd'T'HH:mm:ssxxx", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss", + 'yyyy-MM-dd HH:mm:ss.SSS', + 'yyyy-MM-dd HH:mm:ss', + 'yyyy-MM-dd HH:mm', + 'yyyy-MM-dd', + 'yyyyMMdd', + 'yyyy.MM.dd', + 'yyyy/MM/dd', + 'MM-dd-yyyy', + 'MM/dd/yyyy', + 'MM.dd.yyyy', + 'MMMM d, yyyy', + 'MMM d, yyyy', + 'd MMMM yyyy', + 'd MMM yyyy', + 'dd-MMM-yyyy', + 'yyyy-MMM-dd', +]; + +const isValidDateTimeFormat = (value: string): boolean => { + for (const format of ACCEPTED_DATE_TIME_FORMATS) { + const parsed = parse(value, format, new Date()); + + if (isValid(parsed)) { + return true; + } + } + + return false; +}; + +export const validateDateTimeFieldOrThrow = ( + value: unknown, + fieldName: string, +): unknown => { + if (isNull(value)) return null; + + if (isDate(value) && isValid(value)) { + return value; + } + + if (isString(value) && isValidDateTimeFormat(value)) { + return value; + } + + const inspectedValue = inspect(value); + + throw new CommonQueryRunnerException( + `Invalid value ${inspectedValue} for date-time field "${fieldName}". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: msg`Invalid value for date-time: "${inspectedValue}". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'`, + }, + ); +}; 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 9ef4f4fff3..ff8d45ff14 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,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -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":"malformed-date"} 1`] = `"Invalid value 'malformed-date' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; -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 value {} for date or date-time field "dateField""`; - -exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":1} 1`] = `"Data validation error."`; - -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 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 value [] for date or date-time field \\"dateField\\""]"`; - -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`] = `"["Data validation error."]"`; - -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\\""]"`; +exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"["Invalid value 'malformed-date' for date field \\"dateField\\". Expected format: 'YYYY-MM-DD'"]"`; 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 af4e350d7e..8e1dc5ffe7 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,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -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":"malformed-date-time"} 1`] = `"Invalid value 'malformed-date-time' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; -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 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 DATE_TIME field "dateTimeField", value: "1", it should be a string, Date instance or plain object, (current type : number)."`; - -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 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 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 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 DATE_TIME field \\"dateTimeField\\", value: \\"1\\", it should be a string, Date instance or plain object, (current type : number)."]"`; - -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\\""]"`; +exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date-time"} 1`] = `"["Invalid value 'malformed-date-time' for date-time field \\"dateTimeField\\". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"]"`; 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 9c382cb009..fca2013fc5 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,16 +2,4 @@ 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`] = `"["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`] = `"["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":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__/number-field-create-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/number-field-create-input-validation.integration-spec.ts.snap index a52e7f0a54..cb00cefa69 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/number-field-create-input-validation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/__snapshots__/number-field-create-input-validation.integration-spec.ts.snap @@ -2,16 +2,4 @@ exports[`Create input validation - NUMBER Gql create input - failure NUMBER - should fail with : {"numberField":"string"} 1`] = `"Float cannot represent non numeric value: "string""`; -exports[`Create input validation - NUMBER Gql create input - failure NUMBER - should fail with : {"numberField":[]} 1`] = `"Float cannot represent non numeric value: []"`; - -exports[`Create input validation - NUMBER Gql create input - failure NUMBER - should fail with : {"numberField":{}} 1`] = `"Float cannot represent non numeric value: {}"`; - -exports[`Create input validation - NUMBER Gql create input - failure NUMBER - should fail with : {"numberField":true} 1`] = `"Float cannot represent non numeric value: true"`; - exports[`Create input validation - NUMBER Rest create input - failure NUMBER - should fail with : {"numberField":"string"} 1`] = `"["Invalid number value 'string' for field \\"numberField\\""]"`; - -exports[`Create input validation - NUMBER Rest create input - failure NUMBER - should fail with : {"numberField":[]} 1`] = `"["Invalid number value [] for field \\"numberField\\""]"`; - -exports[`Create input validation - NUMBER Rest create input - failure NUMBER - should fail with : {"numberField":{}} 1`] = `"["Invalid number value {} for field \\"numberField\\""]"`; - -exports[`Create input validation - NUMBER Rest create input - failure NUMBER - should fail with : {"numberField":true} 1`] = `"["Invalid number value true for field \\"numberField\\""]"`; 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 5d0d3d89be..9a1962d8e0 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 @@ -2,20 +2,4 @@ exports[`Create input validation - RATING Gql create input - failure RATING - should fail with : {"ratingField":"not-a-rating"} 1`] = `"Value "not-a-rating" does not exist in "ApiInputValidationTestObjectRatingFieldEnum" enum."`; -exports[`Create input validation - RATING Gql create input - failure RATING - should fail with : {"ratingField":[]} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: []."`; - -exports[`Create input validation - RATING Gql create input - failure RATING - should fail with : {"ratingField":{}} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: {}."`; - -exports[`Create input validation - RATING Gql create input - failure RATING - should fail with : {"ratingField":1} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: 1."`; - -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 value 'not-a-rating' for field \\"ratingField\\""]"`; - -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 string value {} for text field \\"ratingField\\""]"`; - -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 string value true for text field \\"ratingField\\""]"`; 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 6fad9eb43a..fe99310121 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 @@ -2,28 +2,12 @@ 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`] = `"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 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":"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":[]} 1`] = `"["Invalid UUID value [] for field \\"manyToOneRelationFieldId\\""]"`; - -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__/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 bb7678ae3e..03144467a1 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 @@ -2,20 +2,8 @@ exports[`Create input validation - SELECT Gql create input - failure SELECT - should fail with : {"selectField":"not-a-select-option"} 1`] = `"Value "not-a-select-option" does not exist in "ApiInputValidationTestObjectSelectFieldEnum" enum."`; -exports[`Create input validation - SELECT Gql create input - failure SELECT - should fail with : {"selectField":[]} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: []."`; - -exports[`Create input validation - SELECT Gql create input - failure SELECT - should fail with : {"selectField":{}} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: {}."`; - exports[`Create input validation - SELECT Gql create input - failure SELECT - should fail with : {"selectField":1} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: 1."`; -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 value 'not-a-select-option' for field \\"selectField\\""]"`; -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 string value {} for text field \\"selectField\\""]"`; - 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 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 13fbd8eae9..8ea26e1007 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,17 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -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":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":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 c8ca4fddbe..42302ec38f 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 @@ -2,20 +2,4 @@ exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":"non-uuid"} 1`] = `"Invalid UUID"`; -exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":[]} 1`] = `"UUID must be a string"`; - -exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":{}} 1`] = `"UUID must be a string"`; - -exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":1} 1`] = `"UUID must be a string"`; - -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 UUID value 'non-uuid' for field \\"uuidField\\""]"`; - -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 UUID value {} for field \\"uuidField\\""]"`; - -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 UUID value true for field \\"uuidField\\""]"`; 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 3a9cd972ba..a386479cf7 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 @@ -8,21 +8,6 @@ export const failingCreateInputByFieldMetadataType: { }[]; } = { [FieldMetadataType.TEXT]: [ - { - input: { - textField: {}, - }, - }, - { - input: { - textField: [], - }, - }, - { - input: { - textField: true, - }, - }, { input: { textField: 1, @@ -30,21 +15,6 @@ export const failingCreateInputByFieldMetadataType: { }, ], [FieldMetadataType.NUMBER]: [ - { - input: { - numberField: {}, - }, - }, - { - input: { - numberField: [], - }, - }, - { - input: { - numberField: true, - }, - }, { input: { numberField: 'string', @@ -52,26 +22,6 @@ export const failingCreateInputByFieldMetadataType: { }, ], [FieldMetadataType.UUID]: [ - { - input: { - uuidField: {}, - }, - }, - { - input: { - uuidField: [], - }, - }, - { - input: { - uuidField: true, - }, - }, - { - input: { - uuidField: 1, - }, - }, { input: { uuidField: 'non-uuid', @@ -84,21 +34,6 @@ export const failingCreateInputByFieldMetadataType: { selectField: 'not-a-select-option', }, }, - { - input: { - selectField: {}, - }, - }, - { - input: { - selectField: [], - }, - }, - { - input: { - selectField: true, - }, - }, { input: { selectField: 1, @@ -106,26 +41,6 @@ export const failingCreateInputByFieldMetadataType: { }, ], [FieldMetadataType.RELATION]: [ - { - input: { - manyToOneRelationFieldId: {}, - }, - }, - { - input: { - manyToOneRelationFieldId: [], - }, - }, - { - input: { - manyToOneRelationFieldId: true, - }, - }, - { - input: { - manyToOneRelationFieldId: 1, - }, - }, { input: { manyToOneRelationFieldId: 'non-uuid', @@ -194,26 +109,6 @@ export const failingCreateInputByFieldMetadataType: { ratingField: 'not-a-rating', }, }, - { - input: { - ratingField: {}, - }, - }, - { - input: { - ratingField: [], - }, - }, - { - input: { - ratingField: true, - }, - }, - { - input: { - ratingField: 1, - }, - }, ], [FieldMetadataType.MULTI_SELECT]: [ { @@ -221,21 +116,6 @@ export const failingCreateInputByFieldMetadataType: { multiSelectField: 'not-a-select-option', }, }, - { - input: { - multiSelectField: {}, - }, - }, - { - input: { - multiSelectField: true, - }, - }, - { - input: { - multiSelectField: 1, - }, - }, ], [FieldMetadataType.DATE]: [ { @@ -243,75 +123,15 @@ export const failingCreateInputByFieldMetadataType: { dateField: 'malformed-date', }, }, - { - input: { - dateField: {}, - }, - }, - { - input: { - dateField: [], - }, - }, - { - input: { - dateField: true, - }, - }, - { - input: { - dateField: 1, - }, - }, ], [FieldMetadataType.DATE_TIME]: [ { input: { - dateTimeField: 'malformed-date', - }, - }, - { - input: { - dateTimeField: {}, - }, - }, - { - input: { - dateTimeField: [], - }, - }, - { - input: { - dateTimeField: true, - }, - }, - { - input: { - dateTimeField: 1, + dateTimeField: 'malformed-date-time', }, }, ], [FieldMetadataType.BOOLEAN]: [ - { - input: { - booleanField: null, - }, - }, - { - input: { - booleanField: {}, - }, - }, - { - input: { - booleanField: [], - }, - }, - { - input: { - booleanField: 'string', - }, - }, { input: { booleanField: 1, 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 d8e021c97e..79f93b0590 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 @@ -23,14 +23,6 @@ export const successfulCreateInputByFieldMetadataType: { return record.textField === 'test'; }, }, - { - input: { - textField: '', - }, - validateInput: (record: Record) => { - return record.textField === ''; - }, - }, ], [FieldMetadataType.NUMBER]: [ { @@ -41,30 +33,6 @@ export const successfulCreateInputByFieldMetadataType: { return record.numberField === 1; }, }, - { - input: { - numberField: null, - }, - validateInput: (record: Record) => { - return record.numberField === null; - }, - }, - { - input: { - numberField: 0, - }, - validateInput: (record: Record) => { - return record.numberField === 0; - }, - }, - { - input: { - numberField: -1.1, - }, - validateInput: (record: Record) => { - return record.numberField === -1.1; - }, - }, ], [FieldMetadataType.UUID]: [ { @@ -75,14 +43,6 @@ export const successfulCreateInputByFieldMetadataType: { return record.uuidField === '00000000-0000-4000-8000-000000000000'; }, }, - { - input: { - uuidField: null, - }, - validateInput: (record: Record) => { - return record.uuidField === null; - }, - }, ], [FieldMetadataType.SELECT]: [ { @@ -162,22 +122,6 @@ export const successfulCreateInputByFieldMetadataType: { return record.rawJsonField === null; }, }, - { - input: { - rawJsonField: null, - }, - validateInput: (record: Record) => { - return record.rawJsonField === null; - }, - }, - { - input: { - rawJsonField: '{"key": "value"}', - }, - validateInput: (record: Record) => { - return record.rawJsonField.key === 'value'; - }, - }, ], [FieldMetadataType.ARRAY]: [ { @@ -202,26 +146,6 @@ export const successfulCreateInputByFieldMetadataType: { ); }, }, - { - input: { - arrayField: [], - }, - validateInput: (record: Record) => { - return ( - Array.isArray(record.arrayField) && record.arrayField.length === 0 - ); - }, - }, - { - input: { - arrayField: null, - }, - validateInput: (record: Record) => { - return ( - Array.isArray(record.arrayField) && record.arrayField.length === 0 - ); - }, - }, ], [FieldMetadataType.RATING]: [ { @@ -232,14 +156,6 @@ export const successfulCreateInputByFieldMetadataType: { return record.ratingField === 'RATING_2'; }, }, - { - input: { - ratingField: null, - }, - validateInput: (record: Record) => { - return record.ratingField === null; - }, - }, ], [FieldMetadataType.MULTI_SELECT]: [ { @@ -277,14 +193,6 @@ export const successfulCreateInputByFieldMetadataType: { }, ], [FieldMetadataType.DATE]: [ - { - input: { - dateField: '2025-01-13', - }, - validateInput: (record: Record) => { - return new Date(record.dateField).toDateString() === 'Mon Jan 13 2025'; - }, - }, { input: { dateField: null, @@ -293,30 +201,32 @@ export const successfulCreateInputByFieldMetadataType: { return record.dateField === null; }, }, + { + input: { + dateField: '2025-01-13', + }, + validateInput: (record: Record) => { + return new Date(record.dateField).toDateString() === 'Mon Jan 13 2025'; + }, + }, ], [FieldMetadataType.DATE_TIME]: [ { input: { - dateTimeField: '2025-01-13 00:00:00', + dateTimeField: '2025-01-13T10:30:00.000Z', }, validateInput: (record: Record) => { const date = new Date(record.dateTimeField); return ( date.toDateString() === 'Mon Jan 13 2025' && - date.getMinutes() === 0 && - date.getSeconds() === 0 + date.getUTCHours() === 10 && + date.getUTCMinutes() === 30 && + date.getUTCSeconds() === 0 && + date.getUTCMilliseconds() === 0 ); }, }, - { - input: { - dateTimeField: null, - }, - validateInput: (record: Record) => { - return record.dateTimeField === null; - }, - }, ], [FieldMetadataType.BOOLEAN]: [ {