diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts index 355d598869..33ddd150c3 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/common-args-processors.ts @@ -1,4 +1,9 @@ -import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor'; +import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service'; +import { FilterArgProcessorService } from 'src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service'; import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory'; -export const CommonArgsProcessors = [DataArgProcessor, QueryRunnerArgsFactory]; // TODO: Refacto-common Remove QueryRunnerArgsFactory +export const CommonArgsProcessors = [ + DataArgProcessorService, + FilterArgProcessorService, + QueryRunnerArgsFactory, +]; // TODO: Refacto-common Remove QueryRunnerArgsFactory diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap new file mode 100644 index 0000000000..c8a12c80bd --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg-processor.service.spec.ts.snap @@ -0,0 +1,209 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`DataArgProcessorService failing inputs validation ADDRESS should throw for invalid input #1: "not-an-address" 1`] = `"Invalid object value 'not-an-address' for field "addressField""`; + +exports[`DataArgProcessorService failing inputs validation ADDRESS should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "addressField""`; + +exports[`DataArgProcessorService failing inputs validation ADDRESS should throw for invalid input #3: true 1`] = `"Invalid object value true for field "addressField""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation BOOLEAN should throw for invalid input #1: {} 1`] = `"Invalid boolean value {} for field "booleanField""`; + +exports[`DataArgProcessorService failing inputs validation BOOLEAN should throw for invalid input #2: [] 1`] = `"Invalid boolean value [] for field "booleanField""`; + +exports[`DataArgProcessorService failing inputs validation BOOLEAN should throw for invalid input #3: "string" 1`] = `"Invalid boolean value 'string' for field "booleanField""`; + +exports[`DataArgProcessorService failing inputs validation BOOLEAN should throw for invalid input #4: 1 1`] = `"Invalid boolean value 1 for field "booleanField""`; + +exports[`DataArgProcessorService failing inputs validation CURRENCY should throw for invalid input #1: "not-a-currency" 1`] = `"Invalid object value 'not-a-currency' for field "currencyField""`; + +exports[`DataArgProcessorService failing inputs validation CURRENCY should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "currencyField""`; + +exports[`DataArgProcessorService failing inputs validation CURRENCY should throw for invalid input #3: true 1`] = `"Invalid object value true for field "currencyField""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation DATE should throw for invalid input #2: {} 1`] = `"Invalid value {} for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessorService failing inputs validation DATE should throw for invalid input #3: [] 1`] = `"Invalid value [] for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation EMAILS should throw for invalid input #1: "not-an-email" 1`] = `"Invalid object value 'not-an-email' for field "emailsField""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation FULL_NAME should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "fullNameField""`; + +exports[`DataArgProcessorService failing inputs validation FULL_NAME should throw for invalid input #3: true 1`] = `"Invalid object value true for field "fullNameField""`; + +exports[`DataArgProcessorService failing inputs validation LINKS should throw for invalid input #1: "not-a-link" 1`] = `"Invalid object value 'not-a-link' for field "linksField""`; + +exports[`DataArgProcessorService failing inputs validation LINKS should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "linksField""`; + +exports[`DataArgProcessorService failing inputs validation LINKS should throw for invalid input #3: true 1`] = `"Invalid object value true for field "linksField""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation MORPH_RELATION should throw for invalid input #2: {} 1`] = `"Invalid UUID value {} for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessorService failing inputs validation MORPH_RELATION should throw for invalid input #3: [] 1`] = `"Invalid UUID value [] for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessorService failing inputs validation MORPH_RELATION should throw for invalid input #4: true 1`] = `"Invalid UUID value true for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessorService failing inputs validation MORPH_RELATION should throw for invalid input #5: 1 1`] = `"Invalid UUID value 1 for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation MULTI_SELECT should throw for invalid input #2: {} 1`] = `"Invalid value {} for field "multiSelectField - Array values need to be string""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation NUMBER should throw for invalid input #1: {} 1`] = `"Invalid number value {} for field "numberField""`; + +exports[`DataArgProcessorService failing inputs validation NUMBER should throw for invalid input #2: [] 1`] = `"Invalid number value [] for field "numberField""`; + +exports[`DataArgProcessorService failing inputs validation NUMBER should throw for invalid input #3: true 1`] = `"Invalid number value true for field "numberField""`; + +exports[`DataArgProcessorService failing inputs validation NUMBER should throw for invalid input #4: "string" 1`] = `"Invalid number value 'string' for field "numberField""`; + +exports[`DataArgProcessorService failing inputs validation NUMERIC should throw for invalid input #1: {} 1`] = `"Invalid number value NaN for field "numericField""`; + +exports[`DataArgProcessorService failing inputs validation NUMERIC should throw for invalid input #2: "not-a-number" 1`] = `"Invalid number value NaN for field "numericField""`; + +exports[`DataArgProcessorService failing inputs validation PHONES should throw for invalid input #1: "not-a-phone" 1`] = `"Invalid object value 'not-a-phone' for field "phonesField""`; + +exports[`DataArgProcessorService failing inputs validation PHONES should throw for invalid input #2: 1 1`] = `"Invalid object value 1 for field "phonesField""`; + +exports[`DataArgProcessorService failing inputs validation PHONES should throw for invalid input #3: true 1`] = `"Invalid object value true for field "phonesField""`; + +exports[`DataArgProcessorService failing inputs validation POSITION should throw for invalid input #1: "not-a-position" 1`] = `"Invalid position value 'not-a-position' for field "position""`; + +exports[`DataArgProcessorService failing inputs validation POSITION should throw for invalid input #2: null 1`] = `"Invalid position value NaN for field "position""`; + +exports[`DataArgProcessorService failing inputs validation POSITION should throw for invalid input #3: {} 1`] = `"Invalid position value {} for field "position""`; + +exports[`DataArgProcessorService failing inputs validation POSITION should throw for invalid input #4: [] 1`] = `"Invalid position value [] for field "position""`; + +exports[`DataArgProcessorService failing inputs validation RATING should throw for invalid input #1: "not-a-rating" 1`] = `"Invalid value 'not-a-rating' for field "ratingField""`; + +exports[`DataArgProcessorService failing inputs validation RATING should throw for invalid input #2: {} 1`] = `"Invalid string value {} for text field "ratingField""`; + +exports[`DataArgProcessorService failing inputs validation RATING should throw for invalid input #3: [] 1`] = `"Invalid string value [] for text field "ratingField""`; + +exports[`DataArgProcessorService failing inputs validation RATING should throw for invalid input #4: true 1`] = `"Invalid string value true for text field "ratingField""`; + +exports[`DataArgProcessorService failing inputs validation RATING should throw for invalid input #5: 1 1`] = `"Invalid string value 1 for text field "ratingField""`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #1: {} 1`] = `"Invalid UUID value {} for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #2: [] 1`] = `"Invalid UUID value [] for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #3: true 1`] = `"Invalid UUID value true for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #4: 1 1`] = `"Invalid UUID value 1 for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessorService failing inputs validation RELATION should throw for invalid input #5: "non-uuid" 1`] = `"Invalid UUID value 'non-uuid' for field "manyToOneRelationFieldId""`; + +exports[`DataArgProcessorService failing inputs validation RICH_TEXT should throw for invalid input #1: "test" 1`] = `"richTextField RICH_TEXT-typed field does not support write operations"`; + +exports[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService 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[`DataArgProcessorService failing inputs validation SELECT should throw for invalid input #2: {} 1`] = `"Invalid string value {} for text field "selectField""`; + +exports[`DataArgProcessorService failing inputs validation SELECT should throw for invalid input #3: [] 1`] = `"Invalid string value [] for text field "selectField""`; + +exports[`DataArgProcessorService failing inputs validation SELECT should throw for invalid input #4: true 1`] = `"Invalid string value true for text field "selectField""`; + +exports[`DataArgProcessorService failing inputs validation SELECT should throw for invalid input #5: 1 1`] = `"Invalid string value 1 for text field "selectField""`; + +exports[`DataArgProcessorService failing inputs validation TEXT should throw for invalid input #1: {} 1`] = `"Invalid string value {} for text field "textField""`; + +exports[`DataArgProcessorService failing inputs validation TEXT should throw for invalid input #2: [] 1`] = `"Invalid string value [] for text field "textField""`; + +exports[`DataArgProcessorService failing inputs validation TEXT should throw for invalid input #3: true 1`] = `"Invalid string value true for text field "textField""`; + +exports[`DataArgProcessorService failing inputs validation TEXT should throw for invalid input #4: 1 1`] = `"Invalid string value 1 for text field "textField""`; + +exports[`DataArgProcessorService failing inputs validation UUID should throw for invalid input #1: {} 1`] = `"Invalid UUID value {} for field "uuidField""`; + +exports[`DataArgProcessorService failing inputs validation UUID should throw for invalid input #2: [] 1`] = `"Invalid UUID value [] for field "uuidField""`; + +exports[`DataArgProcessorService failing inputs validation UUID should throw for invalid input #3: true 1`] = `"Invalid UUID value true for field "uuidField""`; + +exports[`DataArgProcessorService failing inputs validation UUID should throw for invalid input #4: 1 1`] = `"Invalid UUID value 1 for field "uuidField""`; + +exports[`DataArgProcessorService 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__/__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 deleted file mode 100644 index 8f86124142..0000000000 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/__snapshots__/data-arg.processor.spec.ts.snap +++ /dev/null @@ -1,209 +0,0 @@ -// 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__/data-arg.processor.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg-processor.service.spec.ts similarity index 92% rename from packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg.processor.spec.ts rename to packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/__tests__/data-arg-processor.service.spec.ts index fba0e5c76c..fe32e63cfb 100644 --- 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.service.spec.ts @@ -2,7 +2,7 @@ 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 { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service'; 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'; @@ -21,8 +21,8 @@ jest.mock( }), ); -describe('DataArgProcessor', () => { - let dataArgProcessor: DataArgProcessor; +describe('DataArgProcessorService', () => { + let dataArgProcessorService: DataArgProcessorService; let recordPositionService: jest.Mocked; const mockWorkspaceId = '20202020-1234-1234-1234-123456789012'; @@ -96,7 +96,7 @@ describe('DataArgProcessor', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - DataArgProcessor, + DataArgProcessorService, { provide: RecordPositionService, useValue: recordPositionService, @@ -104,7 +104,9 @@ describe('DataArgProcessor', () => { ], }).compile(); - dataArgProcessor = module.get(DataArgProcessor); + dataArgProcessorService = module.get( + DataArgProcessorService, + ); }); afterEach(() => { @@ -112,7 +114,7 @@ describe('DataArgProcessor', () => { }); it('should be defined', () => { - expect(dataArgProcessor).toBeDefined(); + expect(dataArgProcessorService).toBeDefined(); }); describe('failing inputs validation', () => { @@ -140,7 +142,7 @@ describe('DataArgProcessor', () => { const flatObjectMetadata = createFlatObjectMetadata(fieldNames); await expect( - dataArgProcessor.process({ + dataArgProcessorService.process({ partialRecordInputs: [testCase.input], authContext: createMockAuthContext(), flatObjectMetadata, @@ -177,7 +179,7 @@ describe('DataArgProcessor', () => { createFlatFieldMetadataMaps(fieldNames); const flatObjectMetadata = createFlatObjectMetadata(fieldNames); - const result = await dataArgProcessor.process({ + const result = await dataArgProcessorService.process({ partialRecordInputs: [testCase.input], authContext: createMockAuthContext(), flatObjectMetadata, 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.service.ts similarity index 99% rename from packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts rename to packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts index 46c3c9c099..0fa6043c66 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.service.ts @@ -62,7 +62,7 @@ import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-module import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() -export class DataArgProcessor { +export class DataArgProcessorService { constructor(private readonly recordPositionService: RecordPositionService) {} async process({ diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts index 973c43704f..176f5b5ac3 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts @@ -1,7 +1,7 @@ import { inspect } from 'util'; import { msg } from '@lingui/core/macro'; -import { isNull } from '@sniptt/guards'; +import { isBoolean, isNull } from '@sniptt/guards'; import { CommonQueryRunnerException, @@ -12,7 +12,7 @@ export const validateBooleanFieldOrThrow = ( value: unknown, fieldName: string, ): boolean | null => { - if (typeof value !== 'boolean' && !isNull(value)) { + if (!isBoolean(value) && !isNull(value)) { const inspectedValue = inspect(value); throw new CommonQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts index 9d532487a3..8677f2b3d6 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util.ts @@ -1,7 +1,7 @@ import { inspect } from 'util'; import { msg } from '@lingui/core/macro'; -import { isNull } from '@sniptt/guards'; +import { isNull, isNumber } from '@sniptt/guards'; import { CommonQueryRunnerException, @@ -13,8 +13,8 @@ export const validateNumberFieldOrThrow = ( fieldName: string, ): number | null => { if ( - (typeof value !== 'number' && !isNull(value)) || - (typeof value === 'number' && + (!isNumber(value) && !isNull(value)) || + (isNumber(value) && (isNaN(value) || value === Infinity || value === -Infinity)) ) { const inspectedValue = inspect(value); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts index ed44efe6b7..35a2ede099 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts @@ -1,7 +1,7 @@ import { inspect } from 'util'; import { msg } from '@lingui/core/macro'; -import { isNull } from '@sniptt/guards'; +import { isNonEmptyString, isNull } from '@sniptt/guards'; import { isValidUuid } from 'twenty-shared/utils'; import { @@ -13,7 +13,10 @@ export const validateUUIDFieldOrThrow = ( value: unknown, fieldName: string, ): string | null => { - if (!isValidUuid(value as string) && !isNull(value)) { + if ( + (!isNonEmptyString(value) && !isNull(value)) || + (isNonEmptyString(value) && !isValidUuid(value)) + ) { const inspectedValue = inspect(value); throw new CommonQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/__snapshots__/filter-arg-processor.service.spec.ts.snap b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/__snapshots__/filter-arg-processor.service.spec.ts.snap new file mode 100644 index 0000000000..e39c5af095 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/__snapshots__/filter-arg-processor.service.spec.ts.snap @@ -0,0 +1,175 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`FilterArgProcessorService failing filter inputs validation ACTOR should throw for invalid filter #1: {"actorField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: ACTOR"`; + +exports[`FilterArgProcessorService failing filter inputs validation ACTOR should throw for invalid filter #2: {"actorField":{"source":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "actorField.source" of type SELECT - Allowed operators: eq, neq, in, containsAny, is, isEmptyArray"`; + +exports[`FilterArgProcessorService failing filter inputs validation ACTOR should throw for invalid filter #3: {"actorField":{"workspaceMemberId":{"eq":"invalid-uuid"}}} 1`] = `"Invalid UUID value 'invalid-uuid' for field "actorField.workspaceMemberId""`; + +exports[`FilterArgProcessorService failing filter inputs validation ADDRESS should throw for invalid filter #1: {"addressField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: ADDRESS"`; + +exports[`FilterArgProcessorService failing filter inputs validation ADDRESS should throw for invalid filter #2: {"addressField":{"addressCity":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "addressField.addressCity" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation ADDRESS should throw for invalid filter #3: {"addressField":{"addressCity":{}}} 1`] = `"Filter for field "addressField.addressCity" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation ARRAY should throw for invalid filter #1: {"arrayField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "arrayField" of type ARRAY - Allowed operators: containsIlike, is, isEmptyArray"`; + +exports[`FilterArgProcessorService failing filter inputs validation ARRAY should throw for invalid filter #2: {"arrayField":{}} 1`] = `"Filter for field "arrayField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation ARRAY should throw for invalid filter #3: {"arrayField":{"containsIlike":"test","is":"NULL"}} 1`] = `"Filter for field "arrayField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation BOOLEAN should throw for invalid filter #1: {"booleanField":{"eq":{}}} 1`] = `"Invalid boolean value {} for field "booleanField""`; + +exports[`FilterArgProcessorService failing filter inputs validation BOOLEAN should throw for invalid filter #2: {"booleanField":{"eq":[]}} 1`] = `"Invalid boolean value [] for field "booleanField""`; + +exports[`FilterArgProcessorService failing filter inputs validation BOOLEAN should throw for invalid filter #3: {"booleanField":{"eq":1}} 1`] = `"Invalid boolean value 1 for field "booleanField""`; + +exports[`FilterArgProcessorService failing filter inputs validation BOOLEAN should throw for invalid filter #4: {"booleanField":{"invalidOperator":true}} 1`] = `"Operator "invalidOperator" is not valid for field "booleanField" of type BOOLEAN - Allowed operators: eq, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation BOOLEAN should throw for invalid filter #5: {"booleanField":{}} 1`] = `"Filter for field "booleanField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation CURRENCY should throw for invalid filter #1: {"currencyField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: CURRENCY"`; + +exports[`FilterArgProcessorService failing filter inputs validation CURRENCY should throw for invalid filter #2: {"currencyField":{"amountMicros":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "currencyField.amountMicros" of type NUMERIC - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation CURRENCY should throw for invalid filter #3: {"currencyField":{"amountMicros":{"eq":"not-a-number"}}} 1`] = `"Invalid number value NaN for field "currencyField.amountMicros""`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #1: {"dateField":{"eq":"malformed-date"}} 1`] = `"Invalid value 'malformed-date' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #2: {"dateField":{"eq":{}}} 1`] = `"Invalid value {} for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #3: {"dateField":{"eq":[]}} 1`] = `"Invalid value [] for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #4: {"dateField":{"eq":true}} 1`] = `"Invalid value true for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #5: {"dateField":{"eq":1}} 1`] = `"Invalid value 1 for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #6: {"dateField":{"invalidOperator":"2025-01-01"}} 1`] = `"Operator "invalidOperator" is not valid for field "dateField" of type DATE - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE should throw for invalid filter #7: {"dateField":{}} 1`] = `"Filter for field "dateField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #1: {"dateTimeField":{"eq":"malformed-date"}} 1`] = `"Invalid value 'malformed-date' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #2: {"dateTimeField":{"eq":{}}} 1`] = `"Invalid value {} for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #3: {"dateTimeField":{"eq":[]}} 1`] = `"Invalid value [] for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #4: {"dateTimeField":{"eq":true}} 1`] = `"Invalid value true for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #5: {"dateTimeField":{"eq":1}} 1`] = `"Invalid value 1 for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #6: {"dateTimeField":{"invalidOperator":"2025-01-01T10:00:00Z"}} 1`] = `"Operator "invalidOperator" is not valid for field "dateTimeField" of type DATE_TIME - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation DATE_TIME should throw for invalid filter #7: {"dateTimeField":{}} 1`] = `"Filter for field "dateTimeField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation EMAILS should throw for invalid filter #1: {"emailsField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: EMAILS"`; + +exports[`FilterArgProcessorService failing filter inputs validation EMAILS should throw for invalid filter #2: {"emailsField":{"primaryEmail":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "emailsField.primaryEmail" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation FILES should throw for invalid filter #1: {"filesField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "filesField" of type FILES - Allowed operators: is, like"`; + +exports[`FilterArgProcessorService failing filter inputs validation FILES should throw for invalid filter #2: {"filesField":{}} 1`] = `"Filter for field "filesField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation FULL_NAME should throw for invalid filter #1: {"fullNameField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: FULL_NAME"`; + +exports[`FilterArgProcessorService failing filter inputs validation FULL_NAME should throw for invalid filter #2: {"fullNameField":{"firstName":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "fullNameField.firstName" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation LINKS should throw for invalid filter #1: {"linksField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: LINKS"`; + +exports[`FilterArgProcessorService failing filter inputs validation LINKS should throw for invalid filter #2: {"linksField":{"primaryLinkUrl":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "linksField.primaryLinkUrl" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation MORPH_RELATION should throw for invalid filter #1: {"manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id":{"eq":"invalid-uuid"}} 1`] = `"Invalid UUID value 'invalid-uuid' for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id""`; + +exports[`FilterArgProcessorService failing filter inputs validation MORPH_RELATION should throw for invalid filter #2: {"manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id" of type MORPH_RELATION - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation MORPH_RELATION should throw for invalid filter #3: {"manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id":{}} 1`] = `"Filter for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation MULTI_SELECT should throw for invalid filter #1: {"multiSelectField":{"invalidOperator":["OPTION_1"]}} 1`] = `"Operator "invalidOperator" is not valid for field "multiSelectField" of type MULTI_SELECT - Allowed operators: containsAny, is, isEmptyArray"`; + +exports[`FilterArgProcessorService failing filter inputs validation MULTI_SELECT should throw for invalid filter #2: {"multiSelectField":{}} 1`] = `"Filter for field "multiSelectField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation MULTI_SELECT should throw for invalid filter #3: {"multiSelectField":{"containsAny":"not-an-array"}} 1`] = `"Filter operator "containsAny" requires an array value for field multiSelectField"`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #1: {"numberField":{"eq":"not-a-number"}} 1`] = `"Invalid number value NaN for field "numberField""`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #2: {"numberField":{"eq":{}}} 1`] = `"Invalid number value {} for field "numberField""`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #3: {"numberField":{"eq":[]}} 1`] = `"Invalid number value [] for field "numberField""`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #4: {"numberField":{"eq":true}} 1`] = `"Invalid number value true for field "numberField""`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #5: {"numberField":{"invalidOperator":1}} 1`] = `"Operator "invalidOperator" is not valid for field "numberField" of type NUMBER - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #6: {"numberField":{"eq":1,"neq":2}} 1`] = `"Filter for field "numberField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMBER should throw for invalid filter #7: {"numberField":{}} 1`] = `"Filter for field "numberField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMERIC should throw for invalid filter #1: {"numericField":{"eq":"not-a-number"}} 1`] = `"Invalid number value NaN for field "numericField""`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMERIC should throw for invalid filter #2: {"numericField":{"eq":{}}} 1`] = `"Invalid number value {} for field "numericField""`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMERIC should throw for invalid filter #3: {"numericField":{"invalidOperator":1}} 1`] = `"Operator "invalidOperator" is not valid for field "numericField" of type NUMERIC - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation NUMERIC should throw for invalid filter #4: {"numericField":{}} 1`] = `"Filter for field "numericField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation PHONES should throw for invalid filter #1: {"phonesField":{"invalidSubField":{"eq":"test"}}} 1`] = `"Sub field "invalidSubField" not found for composite type: PHONES"`; + +exports[`FilterArgProcessorService failing filter inputs validation PHONES should throw for invalid filter #2: {"phonesField":{"primaryPhoneNumber":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "phonesField.primaryPhoneNumber" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation POSITION should throw for invalid filter #1: {"position":{"eq":"not-a-number"}} 1`] = `"Invalid number value NaN for field "position""`; + +exports[`FilterArgProcessorService failing filter inputs validation POSITION should throw for invalid filter #2: {"position":{"eq":{}}} 1`] = `"Invalid number value {} for field "position""`; + +exports[`FilterArgProcessorService failing filter inputs validation POSITION should throw for invalid filter #3: {"position":{"invalidOperator":1}} 1`] = `"Operator "invalidOperator" is not valid for field "position" of type POSITION - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation POSITION should throw for invalid filter #4: {"position":{}} 1`] = `"Filter for field "position" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation RATING should throw for invalid filter #1: {"ratingField":{"invalidOperator":"RATING_1"}} 1`] = `"Operator "invalidOperator" is not valid for field "ratingField" of type RATING - Allowed operators: eq, neq, in, containsAny, is, isEmptyArray"`; + +exports[`FilterArgProcessorService failing filter inputs validation RATING should throw for invalid filter #2: {"ratingField":{}} 1`] = `"Filter for field "ratingField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation RATING should throw for invalid filter #3: {"ratingField":{"eq":"RATING_1","neq":"RATING_2"}} 1`] = `"Filter for field "ratingField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation RAW_JSON should throw for invalid filter #1: {"rawJsonField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "rawJsonField" of type RAW_JSON - Allowed operators: is, like"`; + +exports[`FilterArgProcessorService failing filter inputs validation RAW_JSON should throw for invalid filter #2: {"rawJsonField":{}} 1`] = `"Filter for field "rawJsonField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation RELATION should throw for invalid filter #1: {"manyToOneRelationFieldId":{"eq":"invalid-uuid"}} 1`] = `"Invalid UUID value 'invalid-uuid' for field "manyToOneRelationFieldId""`; + +exports[`FilterArgProcessorService failing filter inputs validation RELATION should throw for invalid filter #2: {"manyToOneRelationFieldId":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "manyToOneRelationFieldId" of type RELATION - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation RELATION should throw for invalid filter #3: {"manyToOneRelationFieldId":{}} 1`] = `"Filter for field "manyToOneRelationFieldId" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT should throw for invalid filter #1: {"richTextField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "richTextField" of type RICH_TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT should throw for invalid filter #2: {"richTextField":{}} 1`] = `"Filter for field "richTextField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT_V2 should throw for invalid filter #1: {"richTextV2Field":{"invalidOperator":"test"}} 1`] = `"Sub field "invalidOperator" not found for composite type: RICH_TEXT_V2"`; + +exports[`FilterArgProcessorService failing filter inputs validation RICH_TEXT_V2 should throw for invalid filter #2: {"richTextV2Field":{"markdown":{"invalidOperator":"test"}}} 1`] = `"Operator "invalidOperator" is not valid for field "richTextV2Field.markdown" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation SELECT should throw for invalid filter #1: {"selectField":{"invalidOperator":"OPTION_1"}} 1`] = `"Operator "invalidOperator" is not valid for field "selectField" of type SELECT - Allowed operators: eq, neq, in, containsAny, is, isEmptyArray"`; + +exports[`FilterArgProcessorService failing filter inputs validation SELECT should throw for invalid filter #2: {"selectField":{}} 1`] = `"Filter for field "selectField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation SELECT should throw for invalid filter #3: {"selectField":{"eq":"OPTION_1","neq":"OPTION_2"}} 1`] = `"Filter for field "selectField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation TEXT should throw for invalid filter #1: {"textField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "textField" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`FilterArgProcessorService failing filter inputs validation TEXT should throw for invalid filter #2: {"textField":{"eq":"test","neq":"test"}} 1`] = `"Filter for field "textField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation TEXT should throw for invalid filter #3: {"textField":{}} 1`] = `"Filter for field "textField" must have exactly one operator"`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #1: {"uuidField":{"eq":"invalid-uuid"}} 1`] = `"Invalid UUID value 'invalid-uuid' for field "uuidField""`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #2: {"uuidField":{"eq":2}} 1`] = `"Invalid UUID value 2 for field "uuidField""`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #3: {"uuidField":{"eq":{}}} 1`] = `"Invalid UUID value {} for field "uuidField""`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #4: {"uuidField":{"eq":[]}} 1`] = `"Invalid UUID value [] for field "uuidField""`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #5: {"uuidField":{"eq":true}} 1`] = `"Invalid UUID value true for field "uuidField""`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #6: {"uuidField":{"invalidOperator":"test"}} 1`] = `"Operator "invalidOperator" is not valid for field "uuidField" of type UUID - Allowed operators: eq, neq, gt, gte, lt, lte, in, is"`; + +exports[`FilterArgProcessorService failing filter inputs validation UUID should throw for invalid filter #7: {"uuidField":{}} 1`] = `"Filter for field "uuidField" must have exactly one operator"`; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/constants/failing-filter-inputs-by-field-metadata-type.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/constants/failing-filter-inputs-by-field-metadata-type.constant.ts new file mode 100644 index 0000000000..a825e794fd --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/constants/failing-filter-inputs-by-field-metadata-type.constant.ts @@ -0,0 +1,168 @@ +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 failingFilterInputsByFieldMetadataType: { + [K in FieldMetadataType]?: { + filter: Record; + }[]; +} = { + [FieldMetadataType.TEXT]: [ + { filter: { textField: { invalidOperator: 'test' } } }, + { filter: { textField: { eq: 'test', neq: 'test' } } }, + { filter: { textField: {} } }, + ], + [FieldMetadataType.RICH_TEXT]: [ + { filter: { richTextField: { invalidOperator: 'test' } } }, + { filter: { richTextField: {} } }, + ], + [FieldMetadataType.NUMBER]: [ + { filter: { numberField: { eq: 'not-a-number' } } }, + { filter: { numberField: { eq: {} } } }, + { filter: { numberField: { eq: [] } } }, + { filter: { numberField: { eq: true } } }, + { filter: { numberField: { invalidOperator: 1 } } }, + { filter: { numberField: { eq: 1, neq: 2 } } }, + { filter: { numberField: {} } }, + ], + [FieldMetadataType.NUMERIC]: [ + { filter: { numericField: { eq: 'not-a-number' } } }, + { filter: { numericField: { eq: {} } } }, + { filter: { numericField: { invalidOperator: 1 } } }, + { filter: { numericField: {} } }, + ], + [FieldMetadataType.UUID]: [ + { filter: { uuidField: { eq: 'invalid-uuid' } } }, + { filter: { uuidField: { eq: 2 } } }, + { filter: { uuidField: { eq: {} } } }, + { filter: { uuidField: { eq: [] } } }, + { filter: { uuidField: { eq: true } } }, + { filter: { uuidField: { invalidOperator: 'test' } } }, + { filter: { uuidField: {} } }, + ], + [FieldMetadataType.BOOLEAN]: [ + { filter: { booleanField: { eq: {} } } }, + { filter: { booleanField: { eq: [] } } }, + { filter: { booleanField: { eq: 1 } } }, + { filter: { booleanField: { invalidOperator: true } } }, + { filter: { booleanField: {} } }, + ], + [FieldMetadataType.DATE]: [ + { filter: { dateField: { eq: 'malformed-date' } } }, + { filter: { dateField: { eq: {} } } }, + { filter: { dateField: { eq: [] } } }, + { filter: { dateField: { eq: true } } }, + { filter: { dateField: { eq: 1 } } }, + { filter: { dateField: { invalidOperator: '2025-01-01' } } }, + { filter: { dateField: {} } }, + ], + [FieldMetadataType.DATE_TIME]: [ + { filter: { dateTimeField: { eq: 'malformed-date' } } }, + { filter: { dateTimeField: { eq: {} } } }, + { filter: { dateTimeField: { eq: [] } } }, + { filter: { dateTimeField: { eq: true } } }, + { filter: { dateTimeField: { eq: 1 } } }, + { filter: { dateTimeField: { invalidOperator: '2025-01-01T10:00:00Z' } } }, + { filter: { dateTimeField: {} } }, + ], + [FieldMetadataType.SELECT]: [ + { filter: { selectField: { invalidOperator: 'OPTION_1' } } }, + { filter: { selectField: {} } }, + { filter: { selectField: { eq: 'OPTION_1', neq: 'OPTION_2' } } }, + ], + [FieldMetadataType.RATING]: [ + { filter: { ratingField: { invalidOperator: 'RATING_1' } } }, + { filter: { ratingField: {} } }, + { filter: { ratingField: { eq: 'RATING_1', neq: 'RATING_2' } } }, + ], + [FieldMetadataType.MULTI_SELECT]: [ + { filter: { multiSelectField: { invalidOperator: ['OPTION_1'] } } }, + { filter: { multiSelectField: {} } }, + { filter: { multiSelectField: { containsAny: 'not-an-array' } } }, + ], + [FieldMetadataType.ARRAY]: [ + { filter: { arrayField: { invalidOperator: 'test' } } }, + { filter: { arrayField: {} } }, + { filter: { arrayField: { containsIlike: 'test', is: 'NULL' } } }, + ], + [FieldMetadataType.RAW_JSON]: [ + { filter: { rawJsonField: { invalidOperator: 'test' } } }, + { filter: { rawJsonField: {} } }, + ], + [FieldMetadataType.RELATION]: [ + { filter: { manyToOneRelationFieldId: { eq: 'invalid-uuid' } } }, + { filter: { manyToOneRelationFieldId: { invalidOperator: 'test' } } }, + { filter: { manyToOneRelationFieldId: {} } }, + ], + [FieldMetadataType.MORPH_RELATION]: [ + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { eq: 'invalid-uuid' }, + }, + }, + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { + invalidOperator: 'test', + }, + }, + }, + { + filter: { [joinColumnNameForManyToOneMorphRelationField1]: {} }, + }, + ], + [FieldMetadataType.POSITION]: [ + { filter: { position: { eq: 'not-a-number' } } }, + { filter: { position: { eq: {} } } }, + { filter: { position: { invalidOperator: 1 } } }, + { filter: { position: {} } }, + ], + [FieldMetadataType.FILES]: [ + { filter: { filesField: { invalidOperator: 'test' } } }, + { filter: { filesField: {} } }, + ], + [FieldMetadataType.RICH_TEXT_V2]: [ + { filter: { richTextV2Field: { invalidOperator: 'test' } } }, + { filter: { richTextV2Field: { markdown: { invalidOperator: 'test' } } } }, + ], + [FieldMetadataType.ADDRESS]: [ + { filter: { addressField: { invalidSubField: { eq: 'test' } } } }, + { filter: { addressField: { addressCity: { invalidOperator: 'test' } } } }, + { filter: { addressField: { addressCity: {} } } }, + ], + [FieldMetadataType.CURRENCY]: [ + { filter: { currencyField: { invalidSubField: { eq: 'test' } } } }, + { + filter: { currencyField: { amountMicros: { invalidOperator: 'test' } } }, + }, + { filter: { currencyField: { amountMicros: { eq: 'not-a-number' } } } }, + ], + [FieldMetadataType.EMAILS]: [ + { filter: { emailsField: { invalidSubField: { eq: 'test' } } } }, + { + filter: { emailsField: { primaryEmail: { invalidOperator: 'test' } } }, + }, + ], + [FieldMetadataType.PHONES]: [ + { filter: { phonesField: { invalidSubField: { eq: 'test' } } } }, + { + filter: { + phonesField: { primaryPhoneNumber: { invalidOperator: 'test' } }, + }, + }, + ], + [FieldMetadataType.FULL_NAME]: [ + { filter: { fullNameField: { invalidSubField: { eq: 'test' } } } }, + { filter: { fullNameField: { firstName: { invalidOperator: 'test' } } } }, + ], + [FieldMetadataType.LINKS]: [ + { filter: { linksField: { invalidSubField: { eq: 'test' } } } }, + { + filter: { linksField: { primaryLinkUrl: { invalidOperator: 'test' } } }, + }, + ], + [FieldMetadataType.ACTOR]: [ + { filter: { actorField: { invalidSubField: { eq: 'test' } } } }, + { filter: { actorField: { source: { invalidOperator: 'test' } } } }, + { filter: { actorField: { workspaceMemberId: { eq: 'invalid-uuid' } } } }, + ], +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/constants/successful-filter-inputs-by-field-metadata-type.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/constants/successful-filter-inputs-by-field-metadata-type.constant.ts new file mode 100644 index 0000000000..840128ce17 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/constants/successful-filter-inputs-by-field-metadata-type.constant.ts @@ -0,0 +1,244 @@ +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 successfulFilterInputsByFieldMetadataType: { + [K in FieldMetadataType]?: { + filter: Record; + expected?: Record; + }[]; +} = { + [FieldMetadataType.TEXT]: [ + { filter: { textField: { eq: 'test' } } }, + { filter: { textField: { neq: 'test' } } }, + { filter: { textField: { like: '%test%' } } }, + { filter: { textField: { ilike: '%test%' } } }, + { filter: { textField: { startsWith: 'test' } } }, + { filter: { textField: { endsWith: 'test' } } }, + { filter: { textField: { in: ['test1', 'test2'] } } }, + { filter: { textField: { is: 'NULL' } } }, + { filter: { textField: { is: 'NOT_NULL' } } }, + { filter: { textField: { eq: null } } }, + ], + [FieldMetadataType.RICH_TEXT]: [ + { filter: { richTextField: { eq: 'test' } } }, + { filter: { richTextField: { like: '%test%' } } }, + { filter: { richTextField: { ilike: '%test%' } } }, + { filter: { richTextField: { is: 'NULL' } } }, + { filter: { richTextField: { is: 'NOT_NULL' } } }, + ], + [FieldMetadataType.NUMBER]: [ + { filter: { numberField: { eq: 1 } } }, + { filter: { numberField: { neq: 1 } } }, + { filter: { numberField: { gt: 0 } } }, + { filter: { numberField: { gte: 0 } } }, + { filter: { numberField: { lt: 10 } } }, + { filter: { numberField: { lte: 10 } } }, + { filter: { numberField: { in: [1, 2, 3] } } }, + { filter: { numberField: { is: 'NULL' } } }, + { filter: { numberField: { is: 'NOT_NULL' } } }, + { filter: { numberField: { eq: null } } }, + { + filter: { numberField: { eq: '1' } }, + expected: { numberField: { eq: 1 } }, + }, + { filter: { numberField: { eq: 0 } } }, + { filter: { numberField: { eq: -1.5 } } }, + ], + [FieldMetadataType.NUMERIC]: [ + { filter: { numericField: { eq: 1 } } }, + { filter: { numericField: { neq: 1 } } }, + { filter: { numericField: { gt: 0 } } }, + { filter: { numericField: { gte: 0 } } }, + { filter: { numericField: { lt: 10 } } }, + { filter: { numericField: { lte: 10 } } }, + { filter: { numericField: { in: [1, 2, 3] } } }, + { filter: { numericField: { is: 'NULL' } } }, + { filter: { numericField: { is: 'NOT_NULL' } } }, + { filter: { numericField: { eq: null } } }, + ], + [FieldMetadataType.UUID]: [ + { filter: { uuidField: { eq: TEST_UUID } } }, + { filter: { uuidField: { neq: TEST_UUID } } }, + { filter: { uuidField: { in: [TEST_UUID] } } }, + { filter: { uuidField: { is: 'NULL' } } }, + { filter: { uuidField: { is: 'NOT_NULL' } } }, + { filter: { uuidField: { eq: null } } }, + ], + [FieldMetadataType.BOOLEAN]: [ + { filter: { booleanField: { eq: true } } }, + { filter: { booleanField: { eq: false } } }, + { filter: { booleanField: { is: 'NULL' } } }, + { filter: { booleanField: { is: 'NOT_NULL' } } }, + { filter: { booleanField: { eq: null } } }, + { + filter: { booleanField: { eq: 'true' } }, + expected: { booleanField: { eq: true } }, + }, + { + filter: { booleanField: { eq: 'false' } }, + expected: { booleanField: { eq: false } }, + }, + ], + [FieldMetadataType.DATE]: [ + { filter: { dateField: { eq: '2025-01-13' } } }, + { filter: { dateField: { neq: '2025-01-13' } } }, + { filter: { dateField: { gt: '2025-01-01' } } }, + { filter: { dateField: { gte: '2025-01-01' } } }, + { filter: { dateField: { lt: '2025-12-31' } } }, + { filter: { dateField: { lte: '2025-12-31' } } }, + { filter: { dateField: { in: ['2025-01-01', '2025-01-02'] } } }, + { filter: { dateField: { is: 'NULL' } } }, + { filter: { dateField: { is: 'NOT_NULL' } } }, + { filter: { dateField: { eq: null } } }, + ], + [FieldMetadataType.DATE_TIME]: [ + { filter: { dateTimeField: { eq: '2025-01-13T10:30:00Z' } } }, + { filter: { dateTimeField: { neq: '2025-01-13T10:30:00Z' } } }, + { filter: { dateTimeField: { gt: '2025-01-01T00:00:00Z' } } }, + { filter: { dateTimeField: { gte: '2025-01-01T00:00:00Z' } } }, + { filter: { dateTimeField: { lt: '2025-12-31T23:59:59Z' } } }, + { filter: { dateTimeField: { lte: '2025-12-31T23:59:59Z' } } }, + { + filter: { + dateTimeField: { + in: ['2025-01-01T00:00:00Z', '2025-01-02T00:00:00Z'], + }, + }, + }, + { filter: { dateTimeField: { is: 'NULL' } } }, + { filter: { dateTimeField: { is: 'NOT_NULL' } } }, + { filter: { dateTimeField: { eq: null } } }, + ], + [FieldMetadataType.SELECT]: [ + { filter: { selectField: { eq: 'OPTION_1' } } }, + { filter: { selectField: { neq: 'OPTION_1' } } }, + { filter: { selectField: { in: ['OPTION_1', 'OPTION_2'] } } }, + { filter: { selectField: { is: 'NULL' } } }, + { filter: { selectField: { is: 'NOT_NULL' } } }, + ], + [FieldMetadataType.RATING]: [ + { filter: { ratingField: { eq: 'RATING_1' } } }, + { filter: { ratingField: { neq: 'RATING_1' } } }, + { filter: { ratingField: { in: ['RATING_1', 'RATING_2'] } } }, + { filter: { ratingField: { is: 'NULL' } } }, + { filter: { ratingField: { is: 'NOT_NULL' } } }, + ], + [FieldMetadataType.MULTI_SELECT]: [ + { filter: { multiSelectField: { containsAny: ['OPTION_1'] } } }, + { filter: { multiSelectField: { is: 'NULL' } } }, + { filter: { multiSelectField: { is: 'NOT_NULL' } } }, + { filter: { multiSelectField: { isEmptyArray: true } } }, + ], + [FieldMetadataType.ARRAY]: [ + { filter: { arrayField: { containsIlike: '%test%' } } }, + { filter: { arrayField: { is: 'NULL' } } }, + { filter: { arrayField: { is: 'NOT_NULL' } } }, + { filter: { arrayField: { isEmptyArray: true } } }, + ], + [FieldMetadataType.RAW_JSON]: [ + { filter: { rawJsonField: { is: 'NULL' } } }, + { filter: { rawJsonField: { is: 'NOT_NULL' } } }, + { filter: { rawJsonField: { like: '%test%' } } }, + ], + [FieldMetadataType.RELATION]: [ + { filter: { manyToOneRelationFieldId: { eq: TEST_UUID } } }, + { filter: { manyToOneRelationFieldId: { neq: TEST_UUID } } }, + { filter: { manyToOneRelationFieldId: { in: [TEST_UUID] } } }, + { filter: { manyToOneRelationFieldId: { is: 'NULL' } } }, + { filter: { manyToOneRelationFieldId: { is: 'NOT_NULL' } } }, + { filter: { manyToOneRelationFieldId: { eq: null } } }, + ], + [FieldMetadataType.MORPH_RELATION]: [ + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { eq: TEST_UUID }, + }, + }, + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { neq: TEST_UUID }, + }, + }, + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { in: [TEST_UUID] }, + }, + }, + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { is: 'NULL' }, + }, + }, + { + filter: { + [joinColumnNameForManyToOneMorphRelationField1]: { is: 'NOT_NULL' }, + }, + }, + ], + [FieldMetadataType.POSITION]: [ + { filter: { position: { eq: 1 } } }, + { filter: { position: { neq: 1 } } }, + { filter: { position: { gt: 0 } } }, + { filter: { position: { gte: 0 } } }, + { filter: { position: { lt: 10 } } }, + { filter: { position: { lte: 10 } } }, + { filter: { position: { in: [1, 2, 3] } } }, + { filter: { position: { is: 'NULL' } } }, + { filter: { position: { is: 'NOT_NULL' } } }, + { + filter: { position: { eq: '1' } }, + expected: { position: { eq: 1 } }, + }, + ], + [FieldMetadataType.FILES]: [ + { filter: { filesField: { is: 'NULL' } } }, + { filter: { filesField: { is: 'NOT_NULL' } } }, + { filter: { filesField: { like: '%test%' } } }, + ], + [FieldMetadataType.RICH_TEXT_V2]: [ + { filter: { richTextV2Field: { markdown: { ilike: '%test%' } } } }, + ], + [FieldMetadataType.ADDRESS]: [ + { filter: { addressField: { addressCity: { eq: 'Paris' } } } }, + { filter: { addressField: { addressCity: { like: '%Paris%' } } } }, + { filter: { addressField: { addressLat: { gt: 48.0 } } } }, + { filter: { addressField: { addressStreet1: { is: 'NULL' } } } }, + ], + [FieldMetadataType.CURRENCY]: [ + { filter: { currencyField: { amountMicros: { eq: 1000000 } } } }, + { filter: { currencyField: { amountMicros: { gt: 0 } } } }, + { filter: { currencyField: { currencyCode: { eq: 'USD' } } } }, + { filter: { currencyField: { currencyCode: { is: 'NULL' } } } }, + ], + [FieldMetadataType.EMAILS]: [ + { filter: { emailsField: { primaryEmail: { eq: 'test@test.com' } } } }, + { filter: { emailsField: { primaryEmail: { like: '%@test.com' } } } }, + { filter: { emailsField: { primaryEmail: { is: 'NULL' } } } }, + ], + [FieldMetadataType.PHONES]: [ + { filter: { phonesField: { primaryPhoneNumber: { eq: '1234567890' } } } }, + { + filter: { phonesField: { primaryPhoneNumber: { like: '%123%' } } }, + }, + { filter: { phonesField: { primaryPhoneCountryCode: { eq: 'FR' } } } }, + { filter: { phonesField: { primaryPhoneNumber: { is: 'NULL' } } } }, + ], + [FieldMetadataType.FULL_NAME]: [ + { filter: { fullNameField: { firstName: { eq: 'John' } } } }, + { filter: { fullNameField: { lastName: { like: '%Doe%' } } } }, + { filter: { fullNameField: { firstName: { is: 'NULL' } } } }, + ], + [FieldMetadataType.LINKS]: [ + { filter: { linksField: { primaryLinkUrl: { eq: 'https://test.com' } } } }, + { filter: { linksField: { primaryLinkLabel: { like: '%test%' } } } }, + { filter: { linksField: { primaryLinkUrl: { is: 'NULL' } } } }, + ], + [FieldMetadataType.ACTOR]: [ + { filter: { actorField: { source: { eq: 'MANUAL' } } } }, + { filter: { actorField: { name: { like: '%John%' } } } }, + { filter: { actorField: { workspaceMemberId: { eq: TEST_UUID } } } }, + { filter: { actorField: { workspaceMemberId: { is: 'NULL' } } } }, + ], +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/filter-arg-processor.service.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/filter-arg-processor.service.spec.ts new file mode 100644 index 0000000000..ab82480b90 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/__tests__/filter-arg-processor.service.spec.ts @@ -0,0 +1,283 @@ +import { Test, type TestingModule } from '@nestjs/testing'; + +import { FieldMetadataType } from 'twenty-shared/types'; + +import { fieldMetadataConfigByFieldName } from 'src/engine/api/common/common-args-processors/data-arg-processor/__tests__/constants/field-metadata-config-by-field-name.constant'; +import { FilterArgProcessorService } from 'src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.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 { failingFilterInputsByFieldMetadataType } from './constants/failing-filter-inputs-by-field-metadata-type.constant'; +import { successfulFilterInputsByFieldMetadataType } from './constants/successful-filter-inputs-by-field-metadata-type.constant'; + +describe('FilterArgProcessorService', () => { + let filterArgProcessorService: FilterArgProcessorService; + + 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: {}, + } as unknown as FlatEntityMaps; + }; + + 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 unknown as FlatObjectMetadata; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FilterArgProcessorService], + }).compile(); + + filterArgProcessorService = module.get( + FilterArgProcessorService, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('failing filter inputs validation', () => { + const fieldMetadataTypesToTest = Object.keys( + failingFilterInputsByFieldMetadataType, + ) as FieldMetadataType[]; + + for (const fieldMetadataType of fieldMetadataTypesToTest) { + const testCases = + failingFilterInputsByFieldMetadataType[fieldMetadataType]; + + if (!testCases) { + continue; + } + + describe(`${fieldMetadataType}`, () => { + for (const [index, testCase] of testCases.entries()) { + const fieldName = Object.keys(testCase.filter)[0]; + + it(`should throw for invalid filter #${index + 1}: ${JSON.stringify(testCase.filter)}`, () => { + const fieldNames = [fieldName]; + + const flatFieldMetadataMaps = + createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + expect(() => + filterArgProcessorService.process({ + filter: testCase.filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ).toThrowErrorMatchingSnapshot(); + }); + } + }); + } + }); + + describe('successful filter inputs validation', () => { + const fieldMetadataTypesToTest = Object.keys( + successfulFilterInputsByFieldMetadataType, + ) as FieldMetadataType[]; + + for (const fieldMetadataType of fieldMetadataTypesToTest) { + const testCases = + successfulFilterInputsByFieldMetadataType[fieldMetadataType]; + + if (!testCases) { + continue; + } + + describe(`${fieldMetadataType}`, () => { + for (const [index, testCase] of testCases.entries()) { + const fieldName = Object.keys(testCase.filter)[0]; + + it(`should process valid filter #${index + 1}: ${JSON.stringify(testCase.filter)}`, () => { + const fieldNames = [fieldName]; + + const flatFieldMetadataMaps = + createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const result = filterArgProcessorService.process({ + filter: testCase.filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + const expectedResult = testCase.expected ?? testCase.filter; + + expect(result).toBeDefined(); + expect(result).toEqual(expectedResult); + }); + } + }); + } + }); + + describe('logical operators', () => { + it('should process filter with "and" operator', () => { + const fieldNames = ['textField', 'numberField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const filter = { + and: [{ textField: { eq: 'test' } }, { numberField: { gt: 0 } }], + }; + + const result = filterArgProcessorService.process({ + filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual(filter); + }); + + it('should process filter with "or" operator', () => { + const fieldNames = ['textField', 'numberField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const filter = { + or: [{ textField: { eq: 'test' } }, { numberField: { gt: 0 } }], + }; + + const result = filterArgProcessorService.process({ + filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual(filter); + }); + + it('should process filter with "not" operator', () => { + const fieldNames = ['textField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const filter = { + not: { textField: { eq: 'test' } }, + }; + + const result = filterArgProcessorService.process({ + filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual(filter); + }); + + it('should process nested logical operators', () => { + const fieldNames = ['textField', 'numberField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const filter = { + and: [ + { + or: [ + { textField: { eq: 'test1' } }, + { textField: { eq: 'test2' } }, + ], + }, + { numberField: { gt: 0 } }, + ], + }; + + const result = filterArgProcessorService.process({ + filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual(filter); + }); + }); + + describe('value transformation', () => { + it('should coerce string to number for NUMBER field', () => { + const fieldNames = ['numberField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const result = filterArgProcessorService.process({ + filter: { numberField: { eq: '42' } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual({ numberField: { eq: 42 } }); + }); + + it('should coerce string to boolean for BOOLEAN field', () => { + const fieldNames = ['booleanField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const result = filterArgProcessorService.process({ + filter: { booleanField: { eq: 'true' } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual({ booleanField: { eq: true } }); + }); + + it('should preserve null values in "in" operator', () => { + const fieldNames = ['numberField']; + const flatFieldMetadataMaps = createFlatFieldMetadataMaps(fieldNames); + const flatObjectMetadata = createFlatObjectMetadata(fieldNames); + + const result = filterArgProcessorService.process({ + filter: { numberField: { in: [1, null, 3] } }, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + expect(result).toEqual({ numberField: { in: [1, null, 3] } }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/constants/filter-operators.constant.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/constants/filter-operators.constant.ts new file mode 100644 index 0000000000..b2da12dee4 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/constants/filter-operators.constant.ts @@ -0,0 +1,76 @@ +import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type'; + +export const STRING_FILTER_OPERATORS: FilterOperator[] = [ + 'eq', + 'neq', + 'gt', + 'gte', + 'lt', + 'lte', + 'in', + 'is', + 'like', + 'ilike', + 'startsWith', + 'endsWith', +]; + +export const NUMBER_FILTER_OPERATORS: FilterOperator[] = [ + 'eq', + 'neq', + 'gt', + 'gte', + 'lt', + 'lte', + 'in', + 'is', +]; + +export const BOOLEAN_FILTER_OPERATORS: FilterOperator[] = ['eq', 'is']; + +export const DATE_FILTER_OPERATORS: FilterOperator[] = [ + 'eq', + 'neq', + 'gt', + 'gte', + 'lt', + 'lte', + 'in', + 'is', +]; + +export const UUID_FILTER_OPERATORS: FilterOperator[] = [ + 'eq', + 'neq', + 'gt', + 'gte', + 'lt', + 'lte', + 'in', + 'is', +]; + +export const ARRAY_FILTER_OPERATORS: FilterOperator[] = [ + 'containsIlike', + 'is', + 'isEmptyArray', +]; + +export const MULTI_SELECT_FILTER_OPERATORS: FilterOperator[] = [ + 'containsAny', + 'is', + 'isEmptyArray', +]; + +export const ENUM_FILTER_OPERATORS: FilterOperator[] = [ + 'eq', + 'neq', + 'in', + 'containsAny', + 'is', + 'isEmptyArray', +]; + +export const RAW_JSON_FILTER_OPERATORS: FilterOperator[] = ['is', 'like']; + +export const RICH_TEXT_V2_FILTER_OPERATORS: FilterOperator[] = ['ilike']; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service.ts new file mode 100644 index 0000000000..122952907d --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service.ts @@ -0,0 +1,198 @@ +import { Injectable } from '@nestjs/common'; + +import { msg } from '@lingui/core/macro'; +import { + compositeTypeDefinitions, + FieldMetadataType, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { validateAndTransformOperatorAndValue } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-operator-and-value.util'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; +import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +@Injectable() +export class FilterArgProcessorService { + process({ + filter, + flatObjectMetadata, + flatFieldMetadataMaps, + }: { + filter: T; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; + }): T { + if (!isDefined(filter)) { + return filter; + } + + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + return this.validateAndTransformFilter( + filter, + flatObjectMetadata, + flatFieldMetadataMaps, + fieldIdByName, + fieldIdByJoinColumnName, + ) as T; + } + + private validateAndTransformFilter( + filterObject: ObjectRecordFilter, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + fieldIdByName: Record, + fieldIdByJoinColumnName: Record, + ): ObjectRecordFilter { + const transformedFilter: ObjectRecordFilter = {}; + + for (const [key, value] of Object.entries(filterObject)) { + if (key === 'and' || key === 'or') { + transformedFilter[key] = (value as ObjectRecordFilter[]).map( + (nestedFilter) => + this.validateAndTransformFilter( + nestedFilter, + flatObjectMetadata, + flatFieldMetadataMaps, + fieldIdByName, + fieldIdByJoinColumnName, + ), + ); + continue; + } + + if (key === 'not') { + transformedFilter[key] = this.validateAndTransformFilter( + value as ObjectRecordFilter, + flatObjectMetadata, + flatFieldMetadataMaps, + fieldIdByName, + fieldIdByJoinColumnName, + ); + continue; + } + + transformedFilter[key] = this.validateAndTransformFieldFilter( + key, + value, + flatObjectMetadata, + flatFieldMetadataMaps, + fieldIdByName, + fieldIdByJoinColumnName, + ); + } + + return transformedFilter; + } + + private validateAndTransformFieldFilter( + key: string, + filterValue: Record, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + fieldIdByName: Record, + fieldIdByJoinColumnName: Record, + ): Record { + const fieldMetadataId = fieldIdByName[key] || fieldIdByJoinColumnName[key]; + + if (!isDefined(fieldMetadataId)) { + const nameSingular = flatObjectMetadata.nameSingular; + + throw new CommonQueryRunnerException( + `Object ${flatObjectMetadata.nameSingular} doesn't have any "${key}" field.`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid filter : ${nameSingular} object doesn't have any "${key}" field.`, + }, + ); + } + + const fieldMetadata = findFlatEntityByIdInFlatEntityMaps( + { + flatEntityId: fieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }, + ); + + if (!fieldMetadata) { + throw new CommonQueryRunnerException( + `Field metadata not found for field ${key}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, + ); + } + + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + return this.validateAndTransformCompositeFieldFilter( + fieldMetadata, + filterValue, + ); + } + + return validateAndTransformOperatorAndValue( + key, + filterValue, + fieldMetadata, + ); + } + + private validateAndTransformCompositeFieldFilter( + fieldMetadata: FlatFieldMetadata, + filterValue: Record, + ): Record { + const compositeType = compositeTypeDefinitions.get( + fieldMetadata.type as CompositeFieldMetadataType, + ); + + if (!compositeType) { + throw new CommonQueryRunnerException( + `Composite type definition not found for type: ${fieldMetadata.type}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, + ); + } + + const transformedFilter: Record = {}; + + for (const [subFieldKey, subFieldFilter] of Object.entries(filterValue)) { + const subFieldMetadata = compositeType.properties.find( + (property) => property.name === subFieldKey, + ); + + if (!subFieldMetadata) { + throw new CommonQueryRunnerException( + `Sub field "${subFieldKey}" not found for composite type: ${fieldMetadata.type}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, + ); + } + + transformedFilter[subFieldKey] = validateAndTransformOperatorAndValue( + `${fieldMetadata.name}.${subFieldKey}`, + subFieldFilter as Record, + { + ...fieldMetadata, + type: subFieldMetadata.type as FieldMetadataType, + }, + ); + } + + return transformedFilter; + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type.ts new file mode 100644 index 0000000000..c823f07a79 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type.ts @@ -0,0 +1,17 @@ +export type FilterOperator = + | 'eq' + | 'neq' + | 'gt' + | 'gte' + | 'lt' + | 'lte' + | 'in' + | 'is' + | 'like' + | 'ilike' + | 'startsWith' + | 'endsWith' + | 'containsAny' + | 'containsIlike' + | 'isEmptyArray' + | 'search'; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/get-operators-for-field-type.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/get-operators-for-field-type.util.spec.ts new file mode 100644 index 0000000000..4a054bd22f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/get-operators-for-field-type.util.spec.ts @@ -0,0 +1,34 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { getOperatorsForFieldType } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/get-operators-for-field-type.util'; + +describe('getOperatorsForFieldType', () => { + it('should return STRING_FILTER_OPERATORS for TEXT', () => { + const result = getOperatorsForFieldType(FieldMetadataType.TEXT); + + expect(result).toContain('eq'); + expect(result).toContain('like'); + expect(result).toContain('startsWith'); + }); + + it('should return NUMBER_FILTER_OPERATORS for NUMBER', () => { + const result = getOperatorsForFieldType(FieldMetadataType.NUMBER); + + expect(result).toContain('eq'); + expect(result).toContain('gt'); + expect(result).toContain('in'); + }); + + it('should return BOOLEAN_FILTER_OPERATORS for BOOLEAN', () => { + const result = getOperatorsForFieldType(FieldMetadataType.BOOLEAN); + + expect(result).toEqual(['eq', 'is']); + }); + + it('should return ARRAY_FILTER_OPERATORS for ARRAY', () => { + const result = getOperatorsForFieldType(FieldMetadataType.ARRAY); + + expect(result).toContain('containsIlike'); + expect(result).toContain('isEmptyArray'); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/parse-number-value.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/parse-number-value.util.spec.ts new file mode 100644 index 0000000000..5989be8408 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/parse-number-value.util.spec.ts @@ -0,0 +1,24 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { parseNumberValue } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/parse-number-value.util'; + +describe('parseNumberValue', () => { + it('should return value unchanged when not a string', () => { + expect(parseNumberValue(42, FieldMetadataType.NUMBER)).toBe(42); + }); + + it('should parse string to number for NUMBER field', () => { + expect(parseNumberValue('42', FieldMetadataType.NUMBER)).toBe(42); + expect(parseNumberValue('-3.14', FieldMetadataType.NUMERIC)).toBe(-3.14); + }); + + it('should parse string to number for POSITION field', () => { + expect(parseNumberValue('1.5', FieldMetadataType.POSITION)).toBe(1.5); + }); + + it('should throw an error for unknown field type', () => { + expect(parseNumberValue('not-a-number', FieldMetadataType.POSITION)).toBe( + NaN, + ); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-and-transform-operator-and-value.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-and-transform-operator-and-value.util.spec.ts new file mode 100644 index 0000000000..0e07977a68 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-and-transform-operator-and-value.util.spec.ts @@ -0,0 +1,44 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { validateAndTransformOperatorAndValue } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-operator-and-value.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +const createFieldMetadata = (type: FieldMetadataType): FlatFieldMetadata => + ({ + type, + name: 'testField', + }) as FlatFieldMetadata; + +describe('validateAndTransformOperatorAndValue', () => { + it('should validate and transform single operator filter', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.TEXT); + const result = validateAndTransformOperatorAndValue( + 'testField', + { eq: 'hello' }, + fieldMetadata, + ); + + expect(result).toEqual({ eq: 'hello' }); + }); + + it('should throw when filter value is null', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.TEXT); + + expect(() => + validateAndTransformOperatorAndValue( + 'testField', + null as unknown as Record, + fieldMetadata, + ), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when filter has no operators', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.TEXT); + + expect(() => + validateAndTransformOperatorAndValue('testField', {}, fieldMetadata), + ).toThrow(CommonQueryRunnerException); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-array-operator-value-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-array-operator-value-or-throw.util.spec.ts new file mode 100644 index 0000000000..066c5c2abb --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-array-operator-value-or-throw.util.spec.ts @@ -0,0 +1,22 @@ +import { validateArrayOperatorValueOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-array-operator-value-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateArrayOperatorValueOrThrow', () => { + it('should not throw when value is an array', () => { + expect(() => + validateArrayOperatorValueOrThrow([1, 2], 'in', 'fieldName'), + ).not.toThrow(); + }); + + it('should throw when value is not an array', () => { + expect(() => + validateArrayOperatorValueOrThrow('not-array', 'in', 'fieldName'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is object', () => { + expect(() => + validateArrayOperatorValueOrThrow({}, 'containsAny', 'fieldName'), + ).toThrow(CommonQueryRunnerException); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-is-empty-array-operator-value-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-is-empty-array-operator-value-or-throw.util.spec.ts new file mode 100644 index 0000000000..73f5711238 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-is-empty-array-operator-value-or-throw.util.spec.ts @@ -0,0 +1,28 @@ +import { validateIsEmptyArrayOperatorValueOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-empty-array-operator-value-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateIsEmptyArrayOperatorValueOrThrow', () => { + it('should not throw when value is true', () => { + expect(() => + validateIsEmptyArrayOperatorValueOrThrow(true, 'fieldName'), + ).not.toThrow(); + }); + + it('should not throw when value is false', () => { + expect(() => + validateIsEmptyArrayOperatorValueOrThrow(false, 'fieldName'), + ).not.toThrow(); + }); + + it('should throw when value is not a boolean', () => { + expect(() => + validateIsEmptyArrayOperatorValueOrThrow('true', 'fieldName'), + ).toThrow(CommonQueryRunnerException); + }); + + it('should throw when value is an array', () => { + expect(() => + validateIsEmptyArrayOperatorValueOrThrow([], 'fieldName'), + ).toThrow(CommonQueryRunnerException); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-is-operator-filter-value-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-is-operator-filter-value-or-throw.util.spec.ts new file mode 100644 index 0000000000..f8ca91f000 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-is-operator-filter-value-or-throw.util.spec.ts @@ -0,0 +1,32 @@ +import { validateIsOperatorFilterValueOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-operator-filter-value-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +describe('validateIsOperatorFilterValueOrThrow', () => { + it('should not throw when value is NULL', () => { + expect(() => validateIsOperatorFilterValueOrThrow('NULL')).not.toThrow(); + }); + + it('should not throw when value is NOT_NULL', () => { + expect(() => + validateIsOperatorFilterValueOrThrow('NOT_NULL'), + ).not.toThrow(); + }); + + it('should throw when value is invalid', () => { + expect(() => validateIsOperatorFilterValueOrThrow('invalid')).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is null', () => { + expect(() => validateIsOperatorFilterValueOrThrow(null)).toThrow( + CommonQueryRunnerException, + ); + }); + + it('should throw when value is empty string', () => { + expect(() => validateIsOperatorFilterValueOrThrow('')).toThrow( + CommonQueryRunnerException, + ); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-operator-for-field-type-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-operator-for-field-type-or-throw.util.spec.ts new file mode 100644 index 0000000000..cffa6b1b5f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/__tests__/validate-operator-for-field-type-or-throw.util.spec.ts @@ -0,0 +1,49 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { validateOperatorForFieldTypeOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-operator-for-field-type-or-throw.util'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +const createFieldMetadata = (type: FieldMetadataType): FlatFieldMetadata => + ({ + type, + name: 'testField', + }) as FlatFieldMetadata; + +describe('validateOperatorForFieldTypeOrThrow', () => { + it('should not throw when operator is valid for TEXT field', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.TEXT); + + expect(() => + validateOperatorForFieldTypeOrThrow('eq', fieldMetadata, 'testField'), + ).not.toThrow(); + }); + + it('should throw when operator is invalid for TEXT field', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.TEXT); + + expect(() => + validateOperatorForFieldTypeOrThrow( + 'invalidOperator' as 'eq', + fieldMetadata, + 'testField', + ), + ).toThrow(CommonQueryRunnerException); + }); + + it('should not throw when eq is valid for BOOLEAN field', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.BOOLEAN); + + expect(() => + validateOperatorForFieldTypeOrThrow('eq', fieldMetadata, 'testField'), + ).not.toThrow(); + }); + + it('should throw when like is invalid for BOOLEAN field', () => { + const fieldMetadata = createFieldMetadata(FieldMetadataType.BOOLEAN); + + expect(() => + validateOperatorForFieldTypeOrThrow('like', fieldMetadata, 'testField'), + ).toThrow(CommonQueryRunnerException); + }); +}); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/get-operators-for-field-type.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/get-operators-for-field-type.util.ts new file mode 100644 index 0000000000..c13525dbc7 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/get-operators-for-field-type.util.ts @@ -0,0 +1,73 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + ARRAY_FILTER_OPERATORS, + BOOLEAN_FILTER_OPERATORS, + DATE_FILTER_OPERATORS, + ENUM_FILTER_OPERATORS, + MULTI_SELECT_FILTER_OPERATORS, + NUMBER_FILTER_OPERATORS, + RAW_JSON_FILTER_OPERATORS, + RICH_TEXT_V2_FILTER_OPERATORS, + STRING_FILTER_OPERATORS, + UUID_FILTER_OPERATORS, +} from 'src/engine/api/common/common-args-processors/filter-arg-processor/constants/filter-operators.constant'; +import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type'; + +export const getOperatorsForFieldType = ( + fieldType: FieldMetadataType, +): FilterOperator[] => { + switch (fieldType) { + case FieldMetadataType.TEXT: + case FieldMetadataType.RICH_TEXT: + return STRING_FILTER_OPERATORS; + + case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: + case FieldMetadataType.POSITION: + return NUMBER_FILTER_OPERATORS; + + case FieldMetadataType.BOOLEAN: + return BOOLEAN_FILTER_OPERATORS; + + case FieldMetadataType.DATE: + case FieldMetadataType.DATE_TIME: + return DATE_FILTER_OPERATORS; + + case FieldMetadataType.UUID: + case FieldMetadataType.RELATION: + case FieldMetadataType.MORPH_RELATION: + return UUID_FILTER_OPERATORS; + + case FieldMetadataType.ARRAY: + return ARRAY_FILTER_OPERATORS; + + case FieldMetadataType.MULTI_SELECT: + return MULTI_SELECT_FILTER_OPERATORS; + + case FieldMetadataType.SELECT: + case FieldMetadataType.RATING: + return ENUM_FILTER_OPERATORS; + + case FieldMetadataType.RAW_JSON: + case FieldMetadataType.FILES: + return RAW_JSON_FILTER_OPERATORS; + + case FieldMetadataType.RICH_TEXT_V2: + return RICH_TEXT_V2_FILTER_OPERATORS; + + case FieldMetadataType.TS_VECTOR: + case FieldMetadataType.ACTOR: + case FieldMetadataType.ADDRESS: + case FieldMetadataType.CURRENCY: + case FieldMetadataType.EMAILS: + case FieldMetadataType.FULL_NAME: + case FieldMetadataType.LINKS: + case FieldMetadataType.PHONES: + return ['eq', 'neq', 'is']; + + default: + assertUnreachable(fieldType); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/parse-number-value.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/parse-number-value.util.ts new file mode 100644 index 0000000000..c4fa98cddb --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/parse-number-value.util.ts @@ -0,0 +1,24 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { assertUnreachable } from 'twenty-shared/utils'; + +export const parseNumberValue = ( + value: unknown, + fieldType: + | FieldMetadataType.NUMBER + | FieldMetadataType.NUMERIC + | FieldMetadataType.POSITION, +): unknown => { + if (typeof value !== 'string') { + return value; + } + + switch (fieldType) { + case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: + case FieldMetadataType.POSITION: + return parseFloat(value); + + default: + assertUnreachable(fieldType); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-array-items.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-array-items.util.ts new file mode 100644 index 0000000000..352da7f3d3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-array-items.util.ts @@ -0,0 +1,17 @@ +import type { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +import { validateAndTransformValueByFieldType } from './validate-and-transform-value-by-field-type.util'; + +export const validateAndTransformArrayItems = ( + values: unknown[], + fieldMetadata: FlatFieldMetadata, + fieldName: string, +): unknown[] => { + return values.map((item) => { + if (item === null) { + return item; + } + + return validateAndTransformValueByFieldType(item, fieldMetadata, fieldName); + }); +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-operator-and-value.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-operator-and-value.util.ts new file mode 100644 index 0000000000..73907b9a9c --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-operator-and-value.util.ts @@ -0,0 +1,56 @@ +import { msg } from '@lingui/core/macro'; + +import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import type { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +import { validateAndTransformValueOrThrow } from './validate-and-transform-value-or-throw.util'; +import { validateOperatorForFieldTypeOrThrow } from './validate-operator-for-field-type-or-throw.util'; + +export const validateAndTransformOperatorAndValue = ( + fieldName: string, + filterValue: Record, + fieldMetadata: FlatFieldMetadata, +): Record => { + if (filterValue === null || typeof filterValue !== 'object') { + throw new CommonQueryRunnerException( + `Filter value for field "${fieldName}" must be an object`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid filter: filter value must be an object`, + }, + ); + } + + const entries = Object.entries(filterValue); + + if (entries.length !== 1) { + throw new CommonQueryRunnerException( + `Filter for field "${fieldName}" must have exactly one operator`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid filter: exactly one operator per field is required`, + }, + ); + } + + const [[operator, value]] = entries; + + validateOperatorForFieldTypeOrThrow( + operator as FilterOperator, + fieldMetadata, + fieldName, + ); + + const transformedValue = validateAndTransformValueOrThrow( + operator as FilterOperator, + value, + fieldMetadata, + fieldName, + ); + + return { [operator]: transformedValue }; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-value-by-field-type.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-value-by-field-type.util.ts new file mode 100644 index 0000000000..c8a53bb66f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-value-by-field-type.util.ts @@ -0,0 +1,62 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { parseBooleanFromStringValue } from 'twenty-shared/workflow'; + +import { validateBooleanFieldOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-boolean-field-or-throw.util'; +import { validateDateFieldOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-date-field-or-throw.util'; +import { validateDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-date-time-field-or-throw.util'; +import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-number-field-or-throw.util'; +import { validateUUIDFieldOrThrow } from 'src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-uuid-field-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +import { parseNumberValue } from './parse-number-value.util'; + +export const validateAndTransformValueByFieldType = ( + value: unknown, + fieldMetadata: FlatFieldMetadata, + fieldName: string, +): unknown => { + const fieldType = fieldMetadata.type; + + switch (fieldType) { + case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: + case FieldMetadataType.POSITION: { + const coercedNumber = parseNumberValue(value, fieldType); + + validateNumberFieldOrThrow(coercedNumber, fieldName); + + return coercedNumber; + } + + case FieldMetadataType.BOOLEAN: { + const coercedBoolean = + typeof value === 'string' + ? parseBooleanFromStringValue(value.toString()) + : value; + + validateBooleanFieldOrThrow(coercedBoolean, fieldName); + + return coercedBoolean; + } + + case FieldMetadataType.UUID: + case FieldMetadataType.RELATION: + case FieldMetadataType.MORPH_RELATION: + validateUUIDFieldOrThrow(value, fieldName); + + return value; + + case FieldMetadataType.DATE: + validateDateFieldOrThrow(value, fieldName); + + return value; + + case FieldMetadataType.DATE_TIME: + validateDateTimeFieldOrThrow(value, fieldName); + + return value; + + default: + return value; + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-value-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-value-or-throw.util.ts new file mode 100644 index 0000000000..721ea24b33 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-and-transform-value-or-throw.util.ts @@ -0,0 +1,56 @@ +import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type'; +import type { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +import { validateAndTransformArrayItems } from './validate-and-transform-array-items.util'; +import { validateAndTransformValueByFieldType } from './validate-and-transform-value-by-field-type.util'; +import { validateArrayOperatorValueOrThrow } from './validate-array-operator-value-or-throw.util'; +import { validateIsEmptyArrayOperatorValueOrThrow } from './validate-is-empty-array-operator-value-or-throw.util'; +import { validateIsOperatorFilterValueOrThrow } from './validate-is-operator-filter-value-or-throw.util'; + +export const validateAndTransformValueOrThrow = ( + operator: FilterOperator, + value: unknown, + fieldMetadata: FlatFieldMetadata, + fieldName: string, +): unknown => { + switch (operator) { + case 'is': + validateIsOperatorFilterValueOrThrow(value); + + return value; + + case 'isEmptyArray': + validateIsEmptyArrayOperatorValueOrThrow(value, fieldName); + + return value; + + case 'in': + validateArrayOperatorValueOrThrow(value, operator, fieldName); + + return validateAndTransformArrayItems( + value as unknown[], + fieldMetadata, + fieldName, + ); + + case 'containsAny': + validateArrayOperatorValueOrThrow(value, operator, fieldName); + + return value; + + case 'eq': + case 'neq': + case 'gt': + case 'gte': + case 'lt': + case 'lte': + return validateAndTransformValueByFieldType( + value, + fieldMetadata, + fieldName, + ); + + default: + return value; + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-array-operator-value-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-array-operator-value-or-throw.util.ts new file mode 100644 index 0000000000..109771452e --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-array-operator-value-or-throw.util.ts @@ -0,0 +1,23 @@ +import { msg } from '@lingui/core/macro'; + +import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateArrayOperatorValueOrThrow = ( + value: unknown, + operator: FilterOperator, + fieldName: string, +): void => { + if (!Array.isArray(value)) { + throw new CommonQueryRunnerException( + `Filter operator "${operator}" requires an array value for field ${fieldName}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid filter: "${operator}" operator requires an array`, + }, + ); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-empty-array-operator-value-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-empty-array-operator-value-or-throw.util.ts new file mode 100644 index 0000000000..3583326ac1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-empty-array-operator-value-or-throw.util.ts @@ -0,0 +1,22 @@ +import { msg } from '@lingui/core/macro'; +import { isBoolean } from 'class-validator'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateIsEmptyArrayOperatorValueOrThrow = ( + value: unknown, + fieldName: string, +): void => { + if (!isBoolean(value)) { + throw new CommonQueryRunnerException( + `Filter operator "isEmptyArray" requires a boolean value for field ${fieldName}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid filter: "isEmptyArray" operator requires a boolean`, + }, + ); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-operator-filter-value-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-operator-filter-value-or-throw.util.ts new file mode 100644 index 0000000000..7d75901ea5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-is-operator-filter-value-or-throw.util.ts @@ -0,0 +1,16 @@ +import { msg } from '@lingui/core/macro'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateIsOperatorFilterValueOrThrow = (value: unknown): void => { + if (value !== 'NULL' && value !== 'NOT_NULL') { + throw new CommonQueryRunnerException( + `Invalid filter value for "is" operator. Expected "NULL" or "NOT_NULL"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { userFriendlyMessage: msg`Invalid value for "is" operator` }, + ); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-operator-for-field-type-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-operator-for-field-type-or-throw.util.ts new file mode 100644 index 0000000000..1951aeaf47 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/utils/validate-operator-for-field-type-or-throw.util.ts @@ -0,0 +1,30 @@ +import { msg } from '@lingui/core/macro'; + +import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type'; +import { getOperatorsForFieldType } from 'src/engine/api/common/common-args-processors/filter-arg-processor/utils/get-operators-for-field-type.util'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import type { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +export const validateOperatorForFieldTypeOrThrow = ( + operator: FilterOperator, + fieldMetadata: FlatFieldMetadata, + fieldName: string, +): void => { + const allowedOperators = getOperatorsForFieldType(fieldMetadata.type); + + if (!allowedOperators.includes(operator)) { + const fieldType = fieldMetadata.type; + const allowedOperatorsString = allowedOperators.join(', '); + + throw new CommonQueryRunnerException( + `Operator "${operator}" is not valid for field "${fieldName}" of type ${fieldType} - Allowed operators: ${allowedOperatorsString}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid filter : Operator "${operator}" is not valid for this "${fieldName}" ${fieldType} field`, + }, + ); + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts new file mode 100644 index 0000000000..5426195aa6 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-boolean-field-or-throw.util.ts @@ -0,0 +1,26 @@ +import { inspect } from 'util'; + +import { msg } from '@lingui/core/macro'; +import { isBoolean, isNull } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateBooleanFieldOrThrow = ( + value: unknown, + fieldName: string, +): boolean | null => { + if (!isBoolean(value) && !isNull(value)) { + const inspectedValue = inspect(value); + + throw new CommonQueryRunnerException( + `Invalid boolean value ${inspect(value)} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` }, + ); + } + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-date-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-date-field-or-throw.util.ts new file mode 100644 index 0000000000..6f0832ee6d --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-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_FILTER, + { + 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/filter-arg-processor/validator-utils/validate-date-time-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-date-time-field-or-throw.util.ts new file mode 100644 index 0000000000..0e570abc65 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-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_FILTER, + { + userFriendlyMessage: msg`Invalid value for date-time: "${inspectedValue}". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'`, + }, + ); +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-number-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-number-field-or-throw.util.ts new file mode 100644 index 0000000000..b3868ad699 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-number-field-or-throw.util.ts @@ -0,0 +1,32 @@ +import { inspect } from 'util'; + +import { msg } from '@lingui/core/macro'; +import { isNull, isNumber } from '@sniptt/guards'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateNumberFieldOrThrow = ( + value: unknown, + fieldName: string, +): number | null => { + if ( + (!isNumber(value) && !isNull(value)) || + (isNumber(value) && + (isNaN(value) || value === Infinity || value === -Infinity)) + ) { + const inspectedValue = inspect(value); + + throw new CommonQueryRunnerException( + `Invalid number value ${inspectedValue} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { + userFriendlyMessage: msg`Invalid value for number: "${inspectedValue}"`, + }, + ); + } + + return value; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts new file mode 100644 index 0000000000..abf856bce9 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/filter-arg-processor/validator-utils/validate-uuid-field-or-throw.util.ts @@ -0,0 +1,30 @@ +import { inspect } from 'util'; + +import { msg } from '@lingui/core/macro'; +import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { isValidUuid } from 'twenty-shared/utils'; + +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const validateUUIDFieldOrThrow = ( + value: unknown, + fieldName: string, +): string | null => { + if ( + (!isNonEmptyString(value) && !isNull(value)) || + (isNonEmptyString(value) && !isValidUuid(value)) + ) { + const inspectedValue = inspect(value); + + throw new CommonQueryRunnerException( + `Invalid UUID value ${inspectedValue} for field "${fieldName}"`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER, + { userFriendlyMessage: msg`Invalid value for UUID: "${inspectedValue}"` }, + ); + } + + return value as string; +}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts index 10be2a71eb..c6f0c1a870 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts @@ -1,15 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; - import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() @@ -18,87 +12,6 @@ export class QueryRunnerArgsFactory { private readonly recordInputTransformerService: RecordInputTransformerService, ) {} - public overrideFilterByFieldMetadata< - T extends ObjectRecordFilter | undefined, - >( - filter: T, - flatObjectMetadata: FlatObjectMetadata, - flatFieldMetadataMaps: FlatEntityMaps, - ): T { - if (!isDefined(filter)) { - return filter; - } - - const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( - flatFieldMetadataMaps, - flatObjectMetadata, - ); - - const overrideFilter = (filterObject: ObjectRecordFilter) => { - return Object.entries(filterObject).reduce((acc, [key, value]) => { - if (key === 'and' || key === 'or') { - // @ts-expect-error legacy noImplicitAny - acc[key] = value.map((nestedFilter: ObjectRecordFilter) => - overrideFilter(nestedFilter), - ); - } else if (key === 'not') { - // @ts-expect-error legacy noImplicitAny - acc[key] = overrideFilter(value); - } else { - // @ts-expect-error legacy noImplicitAny - acc[key] = this.transformFilterValueByType( - key, - value, - fieldIdByName, - flatFieldMetadataMaps, - ); - } - - return acc; - }, {}); - }; - - return overrideFilter(filter) as T; - } - - private transformFilterValueByType( - key: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any, - fieldIdByName: Record, - flatFieldMetadataMaps: FlatEntityMaps, - ) { - const fieldMetadataId = fieldIdByName[key]; - const fieldMetadata = fieldMetadataId - ? findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: fieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }) - : undefined; - - if (!fieldMetadata) { - return value; - } - - // Special handling for filter values, which have a specific structure - switch (fieldMetadata.type) { - case FieldMetadataType.NUMBER: { - if (value?.is === 'NULL') { - return value; - } else { - return Object.fromEntries( - Object.entries(value).map(([filterKey, filterValue]) => [ - filterKey, - Number(filterValue), - ]), - ); - } - } - default: - return value; - } - } - async overrideValueByFieldMetadata( key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 23b694c974..cfdc1fcb50 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -5,7 +5,8 @@ import { isDefined } from 'twenty-shared/utils'; import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; -import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor'; +import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service'; +import { FilterArgProcessorService } from 'src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service'; import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory'; import { ProcessNestedRelationsHelper } from 'src/engine/api/common/common-nested-relations-processor/process-nested-relations.helper'; import { @@ -63,7 +64,9 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory; @Inject() - protected readonly dataArgProcessor: DataArgProcessor; + protected readonly dataArgProcessor: DataArgProcessorService; + @Inject() + protected readonly filterArgProcessor: FilterArgProcessorService; @Inject() protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; @Inject() diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts index b8d266ab37..97956f0293 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts @@ -103,11 +103,11 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts index ca28d217a6..5948a7737e 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts @@ -104,11 +104,11 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts index b0c38ac4f7..1b0010566f 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -231,11 +231,11 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index be3289162c..fcb950bb33 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -120,11 +120,11 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index bca66e6dff..6056e035bc 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -402,11 +402,11 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts index b6d580644d..c7add34150 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts @@ -104,11 +104,11 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts index e2dad154fb..d776671751 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts @@ -105,11 +105,11 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer return { ...args, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, + filter: this.filterArgProcessor.process({ + filter: args.filter, flatObjectMetadata, flatFieldMetadataMaps, - ), + }), data: ( await this.dataArgProcessor.process({ partialRecordInputs: [args.data], diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts index 7a69b0e1cd..402d0a618f 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts @@ -8,6 +8,7 @@ export enum CommonQueryRunnerExceptionCode { INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT', ARGS_CONFLICT = 'ARGS_CONFLICT', INVALID_ARGS_DATA = 'INVALID_ARGS_DATA', + INVALID_ARGS_FILTER = 'INVALID_ARGS_FILTER', INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', INVALID_ARGS_LAST = 'INVALID_ARGS_LAST', UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT', diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts index f4abde80d5..6016cca0da 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts @@ -22,6 +22,7 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = ( case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST: case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER: case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT: case CommonQueryRunnerExceptionCode.INVALID_CURSOR: case CommonQueryRunnerExceptionCode.TOO_MANY_RECORDS_TO_UPDATE: diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts index a1398228e7..e9a0ffc760 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts @@ -21,6 +21,7 @@ export const commonQueryRunnerToRestApiExceptionHandler = ( case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST: case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER: case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT: case CommonQueryRunnerExceptionCode.INVALID_CURSOR: case CommonQueryRunnerExceptionCode.TOO_MANY_RECORDS_TO_UPDATE: diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/array-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/array-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..46e2e93b5c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/array-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter args validation - ARRAY Gql filter input - failure ARRAY field type - should fail with filter : {"arrayField":{"containsIlike":[]}} 1`] = `"String cannot represent a non string value: []"`; + +exports[`Filter args validation - ARRAY Gql filter input - failure ARRAY field type - should fail with filter : {"arrayField":{"containsIlike":{}}} 1`] = `"String cannot represent a non string value: {}"`; + +exports[`Filter args validation - ARRAY Gql filter input - failure ARRAY field type - should fail with filter : {"arrayField":{"containsIlike":2}} 1`] = `"String cannot represent a non string value: 2"`; + +exports[`Filter args validation - ARRAY Gql filter input - failure ARRAY field type - should fail with filter : {"arrayField":{"containsIlike":true}} 1`] = `"String cannot represent a non string value: true"`; + +exports[`Filter args validation - ARRAY Rest filter input - failure ARRAY field type - should fail with filter : "arrayField[containsAny]:\\"[]\\"" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '"[]"' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; + +exports[`Filter args validation - ARRAY Rest filter input - failure ARRAY field type - should fail with filter : "arrayField[containsAny]:\\"{}\\"" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '"{}"' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; + +exports[`Filter args validation - ARRAY Rest filter input - failure ARRAY field type - should fail with filter : "arrayField[containsAny]:\\"true\\"" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '"true"' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; + +exports[`Filter args validation - ARRAY Rest filter input - failure ARRAY field type - should fail with filter : "arrayField[containsAny]:2" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '2' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/boolean-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/boolean-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..b4590eba87 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/boolean-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - BOOLEAN Gql filter input - failure BOOLEAN field type - should fail with filter : {"booleanField":{"eq":"not-a-boolean"}} 1`] = `"Boolean cannot represent a non boolean value: "not-a-boolean""`; + +exports[`Filter input validation - BOOLEAN Gql filter input - failure BOOLEAN field type - should fail with filter : {"booleanField":{"eq":[]}} 1`] = `"Boolean cannot represent a non boolean value: []"`; + +exports[`Filter input validation - BOOLEAN Gql filter input - failure BOOLEAN field type - should fail with filter : {"booleanField":{"eq":2}} 1`] = `"Boolean cannot represent a non boolean value: 2"`; + +exports[`Filter input validation - BOOLEAN Rest filter input - failure BOOLEAN field type - should fail with filter : "booleanField[eq]:\\"[]\\"" 1`] = ` +[ + "Invalid boolean value '[]' for field "booleanField"", +] +`; + +exports[`Filter input validation - BOOLEAN Rest filter input - failure BOOLEAN field type - should fail with filter : "booleanField[eq]:\\"not-a-boolean\\"" 1`] = ` +[ + "Invalid boolean value 'not-a-boolean' for field "booleanField"", +] +`; + +exports[`Filter input validation - BOOLEAN Rest filter input - failure BOOLEAN field type - should fail with filter : "booleanField[eq]:2" 1`] = ` +[ + "Invalid boolean value '2' for field "booleanField"", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/date-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/date-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..5180ab14e0 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/date-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - DATE Gql filter input - failure DATE field type - should fail with filter : {"dateField":{"eq":"not-a-date"}} 1`] = `"Invalid value 'not-a-date' for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`Filter input validation - DATE Gql filter input - failure DATE field type - should fail with filter : {"dateField":{"eq":[]}} 1`] = `"Invalid value [] for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`Filter input validation - DATE Gql filter input - failure DATE field type - should fail with filter : {"dateField":{"eq":{}}} 1`] = `"Invalid value {} for date field "dateField". Expected format: 'YYYY-MM-DD'"`; + +exports[`Filter input validation - DATE Rest filter input - failure DATE field type - should fail with filter : "dateField[eq]:\\"[]\\"" 1`] = ` +[ + "Invalid value '[]' for date field "dateField". Expected format: 'YYYY-MM-DD'", +] +`; + +exports[`Filter input validation - DATE Rest filter input - failure DATE field type - should fail with filter : "dateField[eq]:\\"{}\\"" 1`] = ` +[ + "Invalid value '{}' for date field "dateField". Expected format: 'YYYY-MM-DD'", +] +`; + +exports[`Filter input validation - DATE Rest filter input - failure DATE field type - should fail with filter : "dateField[eq]:\\"not-a-date\\"" 1`] = ` +[ + "Invalid value 'not-a-date' for date field "dateField". Expected format: 'YYYY-MM-DD'", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/date-time-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/date-time-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..f9beee8618 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/date-time-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - DATE_TIME Gql filter input - failure DATE_TIME field type - should fail with filter : {"dateTimeField":{"eq":"not-a-date-time"}} 1`] = `"Invalid value 'not-a-date-time' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`Filter input validation - DATE_TIME Gql filter input - failure DATE_TIME field type - should fail with filter : {"dateTimeField":{"eq":[]}} 1`] = `"Invalid value [] for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`Filter input validation - DATE_TIME Gql filter input - failure DATE_TIME field type - should fail with filter : {"dateTimeField":{"eq":{}}} 1`] = `"Invalid value {} for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`Filter input validation - DATE_TIME Gql filter input - failure DATE_TIME field type - should fail with filter : {"dateTimeField":{"eq":true}} 1`] = `"Invalid value true for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'"`; + +exports[`Filter input validation - DATE_TIME Rest filter input - failure DATE_TIME field type - should fail with filter : "dateTimeField[eq]:\\"[]\\"" 1`] = ` +[ + "Invalid value '[]' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'", +] +`; + +exports[`Filter input validation - DATE_TIME Rest filter input - failure DATE_TIME field type - should fail with filter : "dateTimeField[eq]:\\"{}\\"" 1`] = ` +[ + "Invalid value '{}' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'", +] +`; + +exports[`Filter input validation - DATE_TIME Rest filter input - failure DATE_TIME field type - should fail with filter : "dateTimeField[eq]:\\"not-a-date-time\\"" 1`] = ` +[ + "Invalid value 'not-a-date-time' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'", +] +`; + +exports[`Filter input validation - DATE_TIME Rest filter input - failure DATE_TIME field type - should fail with filter : "dateTimeField[eq]:\\"true\\"" 1`] = ` +[ + "Invalid value 'true' for date-time field "dateTimeField". Expected format: 'YYYY-MM-DDTHH:mm:ssZ'", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/files-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/files-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..7266a2237c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/files-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - FILES Gql filter input - failure FILES field type - should fail with filter : {"filesField":{"containsIlike":[]}} 1`] = `"Field "containsIlike" is not defined by type "RawJsonFilter"."`; + +exports[`Filter input validation - FILES Gql filter input - failure FILES field type - should fail with filter : {"filesField":{"containsIlike":{}}} 1`] = `"Field "containsIlike" is not defined by type "RawJsonFilter"."`; + +exports[`Filter input validation - FILES Gql filter input - failure FILES field type - should fail with filter : {"filesField":{"containsIlike":2}} 1`] = `"Field "containsIlike" is not defined by type "RawJsonFilter"."`; + +exports[`Filter input validation - FILES Gql filter input - failure FILES field type - should fail with filter : {"filesField":{"containsIlike":true}} 1`] = `"Field "containsIlike" is not defined by type "RawJsonFilter"."`; + +exports[`Filter input validation - FILES Rest filter input - failure FILES field type - should fail with filter : "filesField[containsAny]:\\"[]\\"" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '"[]"' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; + +exports[`Filter input validation - FILES Rest filter input - failure FILES field type - should fail with filter : "filesField[containsAny]:\\"{}\\"" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '"{}"' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; + +exports[`Filter input validation - FILES Rest filter input - failure FILES field type - should fail with filter : "filesField[containsAny]:\\"true\\"" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '"true"' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; + +exports[`Filter input validation - FILES Rest filter input - failure FILES field type - should fail with filter : "filesField[containsAny]:2" 1`] = ` +[ + "'filter' invalid for 'containsAny' operator. Received '2' but array value expected eg: 'field[containsAny]:[value_1,value_2]'", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/morph-relation-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/morph-relation-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..0abd06c5d1 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/morph-relation-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - MORPH_RELATION Gql filter input - failure MORPH_RELATION field type - should fail with filter : {"manyToOneMorphRelationFieldApiInputValidationTargetTestObject1":{"eq":"6dd71a46-68fe-4420-82b3-0d5b00ad2642"}} 1`] = `"Field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1" is not defined by type "ApiInputValidationTestObjectFilterInput". Did you mean "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id" or "manyToOneMorphRelationFieldApiInputValidationTargetTestObject2Id"?"`; + +exports[`Filter input validation - MORPH_RELATION Gql filter input - failure MORPH_RELATION field type - should fail with filter : {"manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id":{"eq":"invalid-uuid"}} 1`] = `"Invalid UUID: 'invalid-uuid'"`; + +exports[`Filter input validation - MORPH_RELATION Rest filter input - failure MORPH_RELATION field type - should fail with filter : "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1[eq]:\\"6dd71a46-68fe-4420-82b3-0d5b00ad2642\\"" 1`] = ` +[ + "Data validation error.", +] +`; + +exports[`Filter input validation - MORPH_RELATION Rest filter input - failure MORPH_RELATION field type - should fail with filter : "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id[eq]:\\"invalid-uuid\\"" 1`] = ` +[ + "Invalid UUID value 'invalid-uuid' for field "manyToOneMorphRelationFieldApiInputValidationTargetTestObject1Id"", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/multi-select-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/multi-select-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..087bd5c47d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/multi-select-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - MULTI_SELECT Gql filter input - failure MULTI_SELECT field type - should fail with filter : {"multiSelectField":{"eq":"not-a-multi-select"}} 1`] = `"Value "not-a-multi-select" does not exist in "ApiInputValidationTestObjectMultiSelectFieldEnum" enum."`; + +exports[`Filter input validation - MULTI_SELECT Gql filter input - failure MULTI_SELECT field type - should fail with filter : {"multiSelectField":{"in":["test"]}} 1`] = `"Value "test" does not exist in "ApiInputValidationTestObjectMultiSelectFieldEnum" enum."`; + +exports[`Filter input validation - MULTI_SELECT Gql filter input - failure MULTI_SELECT field type - should fail with filter : {"multiSelectField":{"neq":"test"}} 1`] = `"Value "test" does not exist in "ApiInputValidationTestObjectMultiSelectFieldEnum" enum."`; + +exports[`Filter input validation - MULTI_SELECT Rest filter input - failure MULTI_SELECT field type - should fail with filter : "multiSelectField[eq]:\\"not-a-multi-select\\"" 1`] = ` +[ + "Operator "eq" is not valid for field "multiSelectField" of type MULTI_SELECT - Allowed operators: containsAny, is, isEmptyArray", +] +`; + +exports[`Filter input validation - MULTI_SELECT Rest filter input - failure MULTI_SELECT field type - should fail with filter : "multiSelectField[in]:[\\"test\\"]" 1`] = ` +[ + "Operator "in" is not valid for field "multiSelectField" of type MULTI_SELECT - Allowed operators: containsAny, is, isEmptyArray", +] +`; + +exports[`Filter input validation - MULTI_SELECT Rest filter input - failure MULTI_SELECT field type - should fail with filter : "multiSelectField[neq]:\\"test\\"" 1`] = ` +[ + "Operator "neq" is not valid for field "multiSelectField" of type MULTI_SELECT - Allowed operators: containsAny, is, isEmptyArray", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/number-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/number-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..172c3202d4 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/number-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - NUMBER Gql filter input - failure NUMBER field type - should fail with filter : {"numberField":{"eq":"not-a-number"}} 1`] = `"Float cannot represent non numeric value: "not-a-number""`; + +exports[`Filter input validation - NUMBER Gql filter input - failure NUMBER field type - should fail with filter : {"numberField":{"eq":[]}} 1`] = `"Float cannot represent non numeric value: []"`; + +exports[`Filter input validation - NUMBER Gql filter input - failure NUMBER field type - should fail with filter : {"numberField":{"eq":{}}} 1`] = `"Float cannot represent non numeric value: {}"`; + +exports[`Filter input validation - NUMBER Gql filter input - failure NUMBER field type - should fail with filter : {"numberField":{"eq":true}} 1`] = `"Float cannot represent non numeric value: true"`; + +exports[`Filter input validation - NUMBER Rest filter input - failure NUMBER field type - should fail with filter : "numberField[eq]:\\"[]\\"" 1`] = ` +[ + "Invalid number value NaN for field "numberField"", +] +`; + +exports[`Filter input validation - NUMBER Rest filter input - failure NUMBER field type - should fail with filter : "numberField[eq]:\\"{}\\"" 1`] = ` +[ + "Invalid number value NaN for field "numberField"", +] +`; + +exports[`Filter input validation - NUMBER Rest filter input - failure NUMBER field type - should fail with filter : "numberField[eq]:\\"not-a-number\\"" 1`] = ` +[ + "Invalid number value NaN for field "numberField"", +] +`; + +exports[`Filter input validation - NUMBER Rest filter input - failure NUMBER field type - should fail with filter : "numberField[eq]:\\"true\\"" 1`] = ` +[ + "Invalid number value NaN for field "numberField"", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/rating-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/rating-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..560050dfaf --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/rating-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - RATING Gql filter input - failure RATING field type - should fail with filter : {"ratingField":{"eq":"not-a-rating"}} 1`] = `"Value "not-a-rating" does not exist in "ApiInputValidationTestObjectRatingFieldEnum" enum."`; + +exports[`Filter input validation - RATING Gql filter input - failure RATING field type - should fail with filter : {"ratingField":{"eq":[]}} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: []."`; + +exports[`Filter input validation - RATING Gql filter input - failure RATING field type - should fail with filter : {"ratingField":{"eq":{}}} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: {}."`; + +exports[`Filter input validation - RATING Gql filter input - failure RATING field type - should fail with filter : {"ratingField":{"eq":2}} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: 2."`; + +exports[`Filter input validation - RATING Gql filter input - failure RATING field type - should fail with filter : {"ratingField":{"eq":true}} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: true."`; + +exports[`Filter input validation - RATING Rest filter input - failure RATING field type - should fail with filter : "ratingField[eq]:\\"[]\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_ratingField_enum": "[]"", +] +`; + +exports[`Filter input validation - RATING Rest filter input - failure RATING field type - should fail with filter : "ratingField[eq]:\\"{}\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_ratingField_enum": "{}"", +] +`; + +exports[`Filter input validation - RATING Rest filter input - failure RATING field type - should fail with filter : "ratingField[eq]:\\"not-a-rating\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_ratingField_enum": "not-a-rating"", +] +`; + +exports[`Filter input validation - RATING Rest filter input - failure RATING field type - should fail with filter : "ratingField[eq]:\\"true\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_ratingField_enum": "true"", +] +`; + +exports[`Filter input validation - RATING Rest filter input - failure RATING field type - should fail with filter : "ratingField[eq]:2" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_ratingField_enum": "2"", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/raw-json-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/raw-json-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..d44d8ea289 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/raw-json-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - RAW_JSON Gql filter input - failure RAW_JSON field type - should fail with filter : {"rawJsonField":{"like":[]}} 1`] = `"String cannot represent a non string value: []"`; + +exports[`Filter input validation - RAW_JSON Gql filter input - failure RAW_JSON field type - should fail with filter : {"rawJsonField":{"like":{}}} 1`] = `"String cannot represent a non string value: {}"`; + +exports[`Filter input validation - RAW_JSON Gql filter input - failure RAW_JSON field type - should fail with filter : {"rawJsonField":{"like":2}} 1`] = `"String cannot represent a non string value: 2"`; + +exports[`Filter input validation - RAW_JSON Gql filter input - failure RAW_JSON field type - should fail with filter : {"rawJsonField":{"like":true}} 1`] = `"String cannot represent a non string value: true"`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/relation-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/relation-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..797bd70950 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/relation-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - RELATION Gql filter input - failure RELATION field type - should fail with filter : {"manyToOneRelationField":{"eq":"6dd71a46-68fe-4420-82b3-0d5b00ad2642"}} 1`] = `"Field "manyToOneRelationField" is not defined by type "ApiInputValidationTestObjectFilterInput". Did you mean "manyToOneRelationFieldId"?"`; + +exports[`Filter input validation - RELATION Gql filter input - failure RELATION field type - should fail with filter : {"manyToOneRelationFieldId":{"eq":"invalid-uuid"}} 1`] = `"Invalid UUID: 'invalid-uuid'"`; + +exports[`Filter input validation - RELATION Gql filter input - failure RELATION field type - should fail with filter : {"oneToManyRelationField":{"eq":"6dd71a46-68fe-4420-82b3-0d5b00ad2642"}} 1`] = `"Field "oneToManyRelationField" is not defined by type "ApiInputValidationTestObjectFilterInput". Did you mean "manyToOneRelationFieldId"?"`; + +exports[`Filter input validation - RELATION Gql filter input - failure RELATION field type - should fail with filter : {"oneToManyRelationFieldId":{"eq":"invalid-uuid"}} 1`] = `"Field "oneToManyRelationFieldId" is not defined by type "ApiInputValidationTestObjectFilterInput". Did you mean "manyToOneRelationFieldId"?"`; + +exports[`Filter input validation - RELATION Rest filter input - failure RELATION field type - should fail with filter : "manyToOneRelationField[eq]:\\"6dd71a46-68fe-4420-82b3-0d5b00ad2642\\"" 1`] = ` +[ + "Data validation error.", +] +`; + +exports[`Filter input validation - RELATION Rest filter input - failure RELATION field type - should fail with filter : "manyToOneRelationFieldId[eq]:\\"invalid-uuid\\"" 1`] = ` +[ + "Invalid UUID value 'invalid-uuid' for field "manyToOneRelationFieldId"", +] +`; + +exports[`Filter input validation - RELATION Rest filter input - failure RELATION field type - should fail with filter : "oneToManyRelationFieldId[eq]:\\"6dd71a46-68fe-4420-82b3-0d5b00ad2642\\"" 1`] = ` +[ + "Object apiInputValidationTestObject doesn't have any "oneToManyRelationFieldId" field.", +] +`; + +exports[`Filter input validation - RELATION Rest filter input - failure RELATION field type - should fail with filter : "oneToManyRelationFieldId[eq]:\\"invalid-uuid\\"" 1`] = ` +[ + "Object apiInputValidationTestObject doesn't have any "oneToManyRelationFieldId" field.", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/select-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/select-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..c312ac28cc --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/select-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - SELECT Gql filter input - failure SELECT field type - should fail with filter : {"selectField":{"eq":"not-a-select"}} 1`] = `"Value "not-a-select" does not exist in "ApiInputValidationTestObjectSelectFieldEnum" enum."`; + +exports[`Filter input validation - SELECT Gql filter input - failure SELECT field type - should fail with filter : {"selectField":{"eq":[]}} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: []."`; + +exports[`Filter input validation - SELECT Gql filter input - failure SELECT field type - should fail with filter : {"selectField":{"eq":{}}} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: {}."`; + +exports[`Filter input validation - SELECT Gql filter input - failure SELECT field type - should fail with filter : {"selectField":{"eq":true}} 1`] = `"Enum "ApiInputValidationTestObjectSelectFieldEnum" cannot represent non-string value: true."`; + +exports[`Filter input validation - SELECT Rest filter input - failure SELECT field type - should fail with filter : "selectField[eq]:\\"[]\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_selectField_enum": "[]"", +] +`; + +exports[`Filter input validation - SELECT Rest filter input - failure SELECT field type - should fail with filter : "selectField[eq]:\\"{}\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_selectField_enum": "{}"", +] +`; + +exports[`Filter input validation - SELECT Rest filter input - failure SELECT field type - should fail with filter : "selectField[eq]:\\"not-a-select\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_selectField_enum": "not-a-select"", +] +`; + +exports[`Filter input validation - SELECT Rest filter input - failure SELECT field type - should fail with filter : "selectField[eq]:\\"true\\"" 1`] = ` +[ + "invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5."_apiInputValidationTestObject_selectField_enum": "true"", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/text-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/text-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..d003085d2a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/text-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - TEXT Gql filter input - failure TEXT field type - should fail with filter : {"textField":{"iregex":"^test$"}} 1`] = `"Operator "iregex" is not valid for field "textField" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`Filter input validation - TEXT Gql filter input - failure TEXT field type - should fail with filter : {"textField":{"regex":"^test$"}} 1`] = `"Operator "regex" is not valid for field "textField" of type TEXT - Allowed operators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, endsWith"`; + +exports[`Filter input validation - TEXT Rest filter input - failure TEXT field type - should fail with filter : "textField[iregex]:\\"^test$\\"" 1`] = ` +[ + "'filter' invalid for 'textField[iregex]:"^test$"', comparator iregex not in eq, neq, in, containsAny, is, gt, gte, lt, lte, startsWith, endsWith, like, ilike", +] +`; + +exports[`Filter input validation - TEXT Rest filter input - failure TEXT field type - should fail with filter : "textField[regex]:\\"^test$\\"" 1`] = ` +[ + "'filter' invalid for 'textField[regex]:"^test$"', comparator regex not in eq, neq, in, containsAny, is, gt, gte, lt, lte, startsWith, endsWith, like, ilike", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/uuid-field-filter-input-validation.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/uuid-field-filter-input-validation.integration-spec.ts.snap new file mode 100644 index 0000000000..38b5c52224 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/__snapshots__/uuid-field-filter-input-validation.integration-spec.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Filter input validation - UUID Gql filter input - failure UUID field type - should fail with filter : {"uuidField":{"eq":"2025-01-01"}} 1`] = `"Invalid UUID: '2025-01-01'"`; + +exports[`Filter input validation - UUID Gql filter input - failure UUID field type - should fail with filter : {"uuidField":{"eq":"invalid-uuid"}} 1`] = `"Invalid UUID: 'invalid-uuid'"`; + +exports[`Filter input validation - UUID Gql filter input - failure UUID field type - should fail with filter : {"uuidField":{"eq":[]}} 1`] = `"UUID must be a string"`; + +exports[`Filter input validation - UUID Gql filter input - failure UUID field type - should fail with filter : {"uuidField":{"eq":{}}} 1`] = `"UUID must be a string"`; + +exports[`Filter input validation - UUID Gql filter input - failure UUID field type - should fail with filter : {"uuidField":{"eq":2}} 1`] = `"UUID must be a string"`; + +exports[`Filter input validation - UUID Gql filter input - failure UUID field type - should fail with filter : {"uuidField":{"eq":true}} 1`] = `"UUID must be a string"`; + +exports[`Filter input validation - UUID Rest filter input - failure UUID field type - should fail with filter : "uuidField[eq]:\\"[]\\"" 1`] = ` +[ + "Invalid UUID value '[]' for field "uuidField"", +] +`; + +exports[`Filter input validation - UUID Rest filter input - failure UUID field type - should fail with filter : "uuidField[eq]:\\"{}\\"" 1`] = ` +[ + "Invalid UUID value '{}' for field "uuidField"", +] +`; + +exports[`Filter input validation - UUID Rest filter input - failure UUID field type - should fail with filter : "uuidField[eq]:\\"2025-01-01\\"" 1`] = ` +[ + "Invalid UUID value '2025-01-01' for field "uuidField"", +] +`; + +exports[`Filter input validation - UUID Rest filter input - failure UUID field type - should fail with filter : "uuidField[eq]:\\"invalid-uuid\\"" 1`] = ` +[ + "Invalid UUID value 'invalid-uuid' for field "uuidField"", +] +`; + +exports[`Filter input validation - UUID Rest filter input - failure UUID field type - should fail with filter : "uuidField[eq]:\\"true\\"" 1`] = ` +[ + "Invalid UUID value 'true' for field "uuidField"", +] +`; + +exports[`Filter input validation - UUID Rest filter input - failure UUID field type - should fail with filter : "uuidField[eq]:2" 1`] = ` +[ + "Invalid UUID value '2' for field "uuidField"", +] +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts index 88a74f32ac..7a49c7b4a9 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); @@ -97,10 +94,12 @@ describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - success', () => { it.each( - successfulTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + successfulTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, async ({ restFilterInput, validateFilter }) => { diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts index 70ef65c411..f13652a3df 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts @@ -46,12 +46,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -59,18 +58,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts index 59bc80f562..dc25d2ce41 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts @@ -10,9 +10,7 @@ export const failingFilterInputByFieldMetadataType: { CompositeFieldMetadataType >]: { gqlFilterInput: any; - restFilterInput: any; - gqlErrorMessage: string; - restErrorMessage: string; + restFilterInput: string; }[]; } = { [FieldMetadataType.RELATION]: [ @@ -22,10 +20,8 @@ export const failingFilterInputByFieldMetadataType: { eq: '6dd71a46-68fe-4420-82b3-0d5b00ad2642', }, }, - gqlErrorMessage: 'is not defined by type', restFilterInput: 'manyToOneRelationField[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"', - restErrorMessage: 'Data validation error', }, { gqlFilterInput: { @@ -33,9 +29,7 @@ export const failingFilterInputByFieldMetadataType: { eq: 'invalid-uuid', }, }, - gqlErrorMessage: 'Invalid UUID', restFilterInput: 'manyToOneRelationFieldId[eq]:"invalid-uuid"', - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { @@ -43,31 +37,24 @@ export const failingFilterInputByFieldMetadataType: { eq: '6dd71a46-68fe-4420-82b3-0d5b00ad2642', }, }, - gqlErrorMessage: 'is not defined by type', restFilterInput: 'oneToManyRelationFieldId[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"', - restErrorMessage: - 'Field metadata not found for field: oneToManyRelationFieldId', }, - // { - // gqlFilterInput: { - // oneToManyRelationFieldId: { - // eq: 'invalid-uuid', - // }, - // }, - // restFilterInput: 'oneToManyRelationFieldId[eq]:"invalid-uuid"', - // gqlErrorMessage: 'is not defined by type', - // restErrorMessage: "'oneToManyRelationFieldId' does not exist", - // }, + { + gqlFilterInput: { + oneToManyRelationFieldId: { + eq: 'invalid-uuid', + }, + }, + restFilterInput: 'oneToManyRelationFieldId[eq]:"invalid-uuid"', + }, ], [FieldMetadataType.MORPH_RELATION]: [ { gqlFilterInput: { [joinColumnNameForManyToOneMorphRelationField1]: { eq: 'invalid-uuid' }, }, - gqlErrorMessage: 'Invalid UUID', restFilterInput: `${joinColumnNameForManyToOneMorphRelationField1}[eq]:"invalid-uuid"`, - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { @@ -75,368 +62,215 @@ export const failingFilterInputByFieldMetadataType: { eq: '6dd71a46-68fe-4420-82b3-0d5b00ad2642', }, }, - gqlErrorMessage: 'is not defined by type', restFilterInput: `${[joinColumnNameForManyToOneMorphRelationField1.replace('Id', '')]}[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"`, - restErrorMessage: `Data validation error`, }, ], [FieldMetadataType.UUID]: [ { gqlFilterInput: { uuidField: { eq: 'invalid-uuid' } }, - gqlErrorMessage: 'Invalid UUID', restFilterInput: 'uuidField[eq]:"invalid-uuid"', - restErrorMessage: 'invalid input syntax for type uuid', - }, - { - gqlFilterInput: { uuidField: { eq: undefined } }, - gqlErrorMessage: - 'undefined is not iterable (cannot read property Symbol(Symbol.iterator))', - restFilterInput: 'uuidField[eq]:"undefined"', - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { uuidField: { eq: 2 } }, - gqlErrorMessage: 'UUID must be a string', restFilterInput: 'uuidField[eq]:2', - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { uuidField: { eq: {} } }, - gqlErrorMessage: 'UUID must be a string', restFilterInput: 'uuidField[eq]:"{}"', - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { uuidField: { eq: [] } }, - gqlErrorMessage: 'UUID must be a string', restFilterInput: 'uuidField[eq]:"[]"', - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { uuidField: { eq: true } }, - gqlErrorMessage: 'UUID must be a string', restFilterInput: 'uuidField[eq]:"true"', - restErrorMessage: 'invalid input syntax for type uuid', }, { gqlFilterInput: { uuidField: { eq: '2025-01-01' } }, - gqlErrorMessage: 'Invalid UUID', restFilterInput: 'uuidField[eq]:"2025-01-01"', - restErrorMessage: 'invalid input syntax for type uuid', }, ], [FieldMetadataType.TEXT]: [ { - gqlFilterInput: { - textField: { regex: 'test' }, - }, - gqlErrorMessage: 'Operator "regex" is not supported', - restFilterInput: 'textField[regex]:test', - restErrorMessage: 'comparator regex not in', + gqlFilterInput: { textField: { regex: '^test$' } }, + restFilterInput: 'textField[regex]:"^test$"', }, { - gqlFilterInput: { - textField: { iregex: 'test' }, - }, - gqlErrorMessage: 'Operator "iregex" is not supported', - restFilterInput: 'textField[iregex]:test', - restErrorMessage: 'comparator iregex not in', + gqlFilterInput: { textField: { iregex: '^test$' } }, + restFilterInput: 'textField[iregex]:"^test$"', }, ], [FieldMetadataType.DATE_TIME]: [ { gqlFilterInput: { dateTimeField: { eq: 'not-a-date-time' } }, - gqlErrorMessage: 'Data validation error.', restFilterInput: 'dateTimeField[eq]:"not-a-date-time"', - restErrorMessage: 'Data validation error.', }, { gqlFilterInput: { dateTimeField: { eq: {} } }, - gqlErrorMessage: 'Data validation error.', restFilterInput: 'dateTimeField[eq]:"{}"', - restErrorMessage: 'Data validation error.', }, { gqlFilterInput: { dateTimeField: { eq: [] } }, - gqlErrorMessage: 'Data validation error.', restFilterInput: 'dateTimeField[eq]:"[]"', - restErrorMessage: 'Data validation error.', }, - // TODO - fix this, should throw an error - // { - // gqlFilterInput: { dateTimeField: { eq: true } }, - // gqlErrorMessage: - // 'invalid input syntax for type timestamp with time zone: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', - // }, + { + gqlFilterInput: { dateTimeField: { eq: true } }, + restFilterInput: 'dateTimeField[eq]:"true"', + }, ], [FieldMetadataType.DATE]: [ { gqlFilterInput: { dateField: { eq: 'not-a-date' } }, - gqlErrorMessage: 'Data validation error', - restFilterInput: 'dateField[eq]:"{}"', - restErrorMessage: 'Data validation error', + restFilterInput: 'dateField[eq]:"not-a-date"', }, { gqlFilterInput: { dateField: { eq: {} } }, - gqlErrorMessage: 'Data validation error', restFilterInput: 'dateField[eq]:"{}"', - restErrorMessage: 'Data validation error', }, { gqlFilterInput: { dateField: { eq: [] } }, - gqlErrorMessage: 'Data validation error', restFilterInput: 'dateField[eq]:"[]"', - restErrorMessage: 'Data validation error', }, ], [FieldMetadataType.BOOLEAN]: [ { gqlFilterInput: { booleanField: { eq: 'not-a-boolean' } }, - gqlErrorMessage: - 'Boolean cannot represent a non boolean value: "not-a-boolean"', restFilterInput: 'booleanField[eq]:"not-a-boolean"', - restErrorMessage: 'invalid input syntax for type boolean', }, { gqlFilterInput: { booleanField: { eq: [] } }, - gqlErrorMessage: 'Boolean cannot represent a non boolean value: []', restFilterInput: 'booleanField[eq]:"[]"', - restErrorMessage: 'invalid input syntax for type boolean', }, { gqlFilterInput: { booleanField: { eq: 2 } }, - gqlErrorMessage: 'Boolean cannot represent a non boolean value: 2', restFilterInput: 'booleanField[eq]:2', - restErrorMessage: 'invalid input syntax for type boolean', }, ], [FieldMetadataType.NUMBER]: [ - // { - // gqlFilterInput: { numberField: { eq: 'not-a-number' } }, - // gqlErrorMessage: - // 'Float cannot represent non numeric value: "not-a-number"', - // // TODO - fix this, should throw an error - // // restFilterInput: 'numberField[eq]:"not-a-number"', - // // restErrorMessage: 'invalid input syntax for type float', - // }, - // { - // gqlFilterInput: { numberField: { eq: {} } }, - // gqlErrorMessage: 'Float cannot represent non numeric value: {}', - // // TODO - fix this, should throw an error - // // restFilterInput: 'numberField[eq]:"{}"', - // // restErrorMessage: 'invalid input syntax for type float', - // }, - // { - // gqlFilterInput: { numberField: { eq: [] } }, - // gqlErrorMessage: 'Float cannot represent non numeric value: []', - // // TODO - fix this, should throw an error - // // restFilterInput: 'numberField[eq]:"[]"', - // // restErrorMessage: 'invalid input syntax for type float', - // }, - // { - // gqlFilterInput: { numberField: { eq: true } }, - // gqlErrorMessage: 'Float cannot represent non numeric value: true', - // // TODO - fix this, should throw an error - // // restFilterInput: 'numberField[eq]:"true"', - // // restErrorMessage: 'invalid input syntax for type float', - // }, - // TODO - ensure it should throw - // { - // gqlFilterInput: { numberField: { eq: null } }, - // gqlErrorMessage: 'Float cannot represent non numeric value: null', - // }, + { + gqlFilterInput: { numberField: { eq: 'not-a-number' } }, + restFilterInput: 'numberField[eq]:"not-a-number"', + }, + { + gqlFilterInput: { numberField: { eq: {} } }, + restFilterInput: 'numberField[eq]:"{}"', + }, + { + gqlFilterInput: { numberField: { eq: [] } }, + restFilterInput: 'numberField[eq]:"[]"', + }, + { + gqlFilterInput: { numberField: { eq: true } }, + restFilterInput: 'numberField[eq]:"true"', + }, ], [FieldMetadataType.RATING]: [ { gqlFilterInput: { ratingField: { eq: 'not-a-rating' } }, - gqlErrorMessage: 'Value "not-a-rating" does not exist in ', restFilterInput: 'ratingField[eq]:"not-a-rating"', - restErrorMessage: 'invalid input value for enum', }, { gqlFilterInput: { ratingField: { eq: {} } }, - gqlErrorMessage: 'cannot represent non-string value: {}.', restFilterInput: 'ratingField[eq]:"{}"', - restErrorMessage: 'invalid input value for enum', }, { gqlFilterInput: { ratingField: { eq: [] } }, - gqlErrorMessage: 'cannot represent non-string value: [].', restFilterInput: 'ratingField[eq]:"[]"', - restErrorMessage: 'invalid input value for enum', }, { gqlFilterInput: { ratingField: { eq: true } }, - gqlErrorMessage: 'cannot represent non-string value: true.', restFilterInput: 'ratingField[eq]:"true"', - restErrorMessage: 'invalid input value for enum', }, { gqlFilterInput: { ratingField: { eq: 2 } }, - gqlErrorMessage: 'cannot represent non-string value: 2.', restFilterInput: 'ratingField[eq]:2', - restErrorMessage: 'invalid input value for enum', }, - // TODO - ensure it should throw - // { - // gqlFilterInput: { ratingField: { eq: null } }, - // gqlErrorMessage: 'cannot represent non-string value: null.', - // }, ], [FieldMetadataType.SELECT]: [ { gqlFilterInput: { selectField: { eq: 'not-a-select' } }, - gqlErrorMessage: 'Value "not-a-select" does not exist in', restFilterInput: 'selectField[eq]:"not-a-select"', - restErrorMessage: "not available in 'selectField'", }, { gqlFilterInput: { selectField: { eq: {} } }, - gqlErrorMessage: 'cannot represent non-string value: {}.', restFilterInput: 'selectField[eq]:"{}"', - restErrorMessage: "not available in 'selectField'", }, { gqlFilterInput: { selectField: { eq: [] } }, - gqlErrorMessage: 'cannot represent non-string value: [].', restFilterInput: 'selectField[eq]:"[]"', - restErrorMessage: "not available in 'selectField'", }, { gqlFilterInput: { selectField: { eq: true } }, - gqlErrorMessage: 'cannot represent non-string value: true.', restFilterInput: 'selectField[eq]:"true"', - restErrorMessage: "not available in 'selectField'", }, - // TODO - ensure it should throw - // { - // gqlFilterInput: { selectField: { eq: null } }, - // gqlErrorMessage: 'cannot represent non-string value: null.', - // }, ], [FieldMetadataType.MULTI_SELECT]: [ { gqlFilterInput: { multiSelectField: { eq: 'not-a-multi-select' } }, - gqlErrorMessage: 'Value "not-a-multi-select" does not exist ', restFilterInput: 'multiSelectField[eq]:"not-a-multi-select"', - restErrorMessage: 'malformed array literal', - }, - // TODO - fix this, should throw - // { - // gqlFilterInput: { multiSelectField: { eq: {} } }, - // gqlErrorMessage: 'cannot represent non-string value: {}.', - // restFilterInput: 'multiSelectField[eq]:"{}"', - // restErrorMessage: "not available in 'multiSelectField'", - // }, - { - gqlFilterInput: { multiSelectField: { eq: [] } }, - gqlErrorMessage: 'cannot represent non-string value: [].', - restFilterInput: 'multiSelectField[eq]:"[]"', - restErrorMessage: 'malformed array literal', }, { - gqlFilterInput: { multiSelectField: { eq: true } }, - gqlErrorMessage: 'cannot represent non-string value: true.', - restFilterInput: 'multiSelectField[eq]:"true"', - restErrorMessage: 'malformed array literal', + gqlFilterInput: { multiSelectField: { neq: 'test' } }, + restFilterInput: 'multiSelectField[neq]:"test"', }, { - gqlFilterInput: { multiSelectField: { eq: 2 } }, - gqlErrorMessage: 'cannot represent non-string value: 2.', - restFilterInput: 'multiSelectField[eq]:2', - restErrorMessage: 'malformed array literal', + gqlFilterInput: { multiSelectField: { in: ['test'] } }, + restFilterInput: 'multiSelectField[in]:["test"]', }, - // TODO - ensure it should throw - // { - // gqlFilterInput: { multiSelectField: { eq: null } }, - // gqlErrorMessage: 'cannot represent non-string value: null.', - // }, ], [FieldMetadataType.RAW_JSON]: [ - // { - // gqlFilterInput: { rawJsonField: { like: {} } }, - // gqlErrorMessage: 'cannot represent a non string value', - // // TODO - fix this ? for rest - // // restFilterInput: 'rawJsonField[like]:"{}"', - // // restErrorMessage: 'cannot represent a non string value', - // }, - // { - // gqlFilterInput: { rawJsonField: { like: [] } }, - // gqlErrorMessage: 'cannot represent a non string value', - // // TODO - fix this ? for rest - // // restFilterInput: 'rawJsonField[like]:"[]"', - // // restErrorMessage: 'cannot represent a non string value', - // }, - // { - // gqlFilterInput: { rawJsonField: { like: true } }, - // gqlErrorMessage: 'cannot represent a non string value', - // // TODO - fix this ? for rest - // // restFilterInput: 'rawJsonField[like]:"true"', - // // restErrorMessage: 'cannot represent a non string value', - // }, - // { - // gqlFilterInput: { rawJsonField: { like: 2 } }, - // gqlErrorMessage: 'cannot represent a non string value', - // // TODO - fix this ? for rest - // // restFilterInput: 'rawJsonField[like]:2', - // // restErrorMessage: 'cannot represent a non string value', - // }, - // TODO - ensure it should throw - // { - // gqlFilterInput: { rawJsonField: { like: null } }, - // gqlErrorMessage: 'cannot represent non-string value: null.', - // }, + { + gqlFilterInput: { rawJsonField: { like: {} } }, + restFilterInput: '', + }, + { + gqlFilterInput: { rawJsonField: { like: [] } }, + restFilterInput: '', + }, + { + gqlFilterInput: { rawJsonField: { like: true } }, + restFilterInput: '', + }, + { + gqlFilterInput: { rawJsonField: { like: 2 } }, + restFilterInput: '', + }, ], [FieldMetadataType.ARRAY]: [ { gqlFilterInput: { arrayField: { containsIlike: {} } }, - gqlErrorMessage: 'cannot represent a non string value', restFilterInput: 'arrayField[containsAny]:"{}"', - restErrorMessage: 'array value expected', }, { gqlFilterInput: { arrayField: { containsIlike: [] } }, - gqlErrorMessage: 'cannot represent a non string value', restFilterInput: 'arrayField[containsAny]:"[]"', - restErrorMessage: 'array value expected', }, { gqlFilterInput: { arrayField: { containsIlike: true } }, - gqlErrorMessage: 'cannot represent a non string value', restFilterInput: 'arrayField[containsAny]:"true"', - restErrorMessage: 'array value expected', }, { gqlFilterInput: { arrayField: { containsIlike: 2 } }, - gqlErrorMessage: 'cannot represent a non string value', restFilterInput: 'arrayField[containsAny]:2', - restErrorMessage: 'array value expected', }, ], [FieldMetadataType.FILES]: [ { gqlFilterInput: { filesField: { containsIlike: {} } }, - gqlErrorMessage: 'is not defined by type', restFilterInput: 'filesField[containsAny]:"{}"', - restErrorMessage: 'array value expected', }, { gqlFilterInput: { filesField: { containsIlike: [] } }, - gqlErrorMessage: 'is not defined by type', restFilterInput: 'filesField[containsAny]:"[]"', - restErrorMessage: 'array value expected', }, { gqlFilterInput: { filesField: { containsIlike: true } }, - gqlErrorMessage: 'is not defined by type', restFilterInput: 'filesField[containsAny]:"true"', - restErrorMessage: 'array value expected', }, { gqlFilterInput: { filesField: { containsIlike: 2 } }, - gqlErrorMessage: 'is not defined by type', restFilterInput: 'filesField[containsAny]:2', - restErrorMessage: 'array value expected', }, ], }; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts index cc00dd8af5..08dcb88351 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts @@ -40,18 +40,6 @@ export const successfulFilterInputByFieldMetadataType: { return record.manyToOneRelationFieldId === TEST_TARGET_OBJECT_RECORD_ID; }, }, - // TODO - fix this, should be returning or not be allowed - // { - // gqlFilterInput: { - // manyToOneRelationFieldId: { - // eq: null, - // }, - // }, - // restFilterInput: 'manyToOneRelationFieldId[eq]=null', - // validateFilter: (record: Record) => { - // return record.manyToOneRelationField === null; - // }, - // }, { gqlFilterInput: { manyToOneRelationFieldId: { @@ -133,7 +121,7 @@ export const successfulFilterInputByFieldMetadataType: { gqlFilterInput: { manyToOneRelationFieldId: { is: 'NOT_NULL' }, }, - restFilterInput: 'manyToOneRelationFieldId[is]:"NOT_NULL"', + restFilterInput: 'manyToOneRelationFieldId[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.manyToOneRelationFieldId); }, @@ -176,7 +164,6 @@ export const successfulFilterInputByFieldMetadataType: { }, restFilterInput: `${joinColumnNameForManyToOneMorphRelationField1}[gt]:"00000000-0000-4000-8000-000000000000"`, validateFilter: (record: Record) => { - // Morph relation join column is a UUID stored as string return ( record[joinColumnNameForManyToOneMorphRelationField1] > '00000000-0000-4000-8000-000000000000' @@ -252,7 +239,7 @@ export const successfulFilterInputByFieldMetadataType: { gqlFilterInput: { [joinColumnNameForManyToOneMorphRelationField1]: { is: 'NOT_NULL' }, }, - restFilterInput: `${joinColumnNameForManyToOneMorphRelationField1}[is]:"NOT_NULL"`, + restFilterInput: `${joinColumnNameForManyToOneMorphRelationField1}[is]:NOT_NULL`, validateFilter: (record: Record) => { return isDefined(record[joinColumnNameForManyToOneMorphRelationField1]); }, @@ -277,14 +264,6 @@ export const successfulFilterInputByFieldMetadataType: { return record.uuidField === TEST_UUID_FIELD_VALUE; }, }, - // TODO - fix this, should be returning or not be allowed - // { - // gqlFilterInput: { uuidField: { eq: null } }, - // restFilterInput: 'uuidField[eq]=null', - // validateFilter: (record: Record) => { - // return record.uuidField === null; - // }, - // }, { gqlFilterInput: { uuidField: { gt: '00000000-0000-4000-8000-000000000000' }, @@ -339,7 +318,7 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { uuidField: { is: 'NOT_NULL' } }, - restFilterInput: 'uuidField[is]:"NOT_NULL"', + restFilterInput: 'uuidField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.uuidField); }, @@ -360,14 +339,13 @@ export const successfulFilterInputByFieldMetadataType: { return record.textField === 'test'; }, }, - // TODO - fix this ? not working - // { - // gqlFilterInput: { textField: { eq: null } }, - // restFilterInput: 'textField[eq]:"null"', - // validateFilter: (record: Record) => { - // return record.textField === ''; - // }, - // }, + { + gqlFilterInput: { textField: { eq: null } }, + restFilterInput: '', + validateFilter: (record: Record) => { + return record.textField === ''; + }, + }, { gqlFilterInput: { textField: { gt: 'tess' } }, restFilterInput: 'textField[gt]:"tess"', @@ -403,29 +381,27 @@ export const successfulFilterInputByFieldMetadataType: { return record.textField === 'test'; }, }, - // TODO - fix this ? not working - // { - // gqlFilterInput: { textField: { is: 'NULL' } }, - // restFilterInput: 'textField[is]:"NULL"', - // validateFilter: (record: Record) => { - // return record.textField === ''; - // }, - // }, + { + gqlFilterInput: { textField: { is: 'NULL' } }, + restFilterInput: 'textField[is]:NULL', + validateFilter: (record: Record) => { + return record.textField === ''; + }, + }, { gqlFilterInput: { textField: { is: 'NOT_NULL' } }, - restFilterInput: 'textField[is]:"NOT_NULL"', + restFilterInput: 'textField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.textField); }, }, - // TODO - fix this ? not working - // { - // gqlFilterInput: { textField: { startsWith: 'tes' } }, - // restFilterInput: 'textField[startsWith]:"tes"', - // validateFilter: (record: Record) => { - // return record.textField.startsWith('tes'); - // }, - // }, + { + gqlFilterInput: { textField: { startsWith: 'tes' } }, + restFilterInput: 'textField[startsWith]:"tes"', + validateFilter: (record: Record) => { + return record.textField.startsWith('tes'); + }, + }, { gqlFilterInput: { textField: { like: '%es%' } }, restFilterInput: 'textField[like]:"%es%"', @@ -440,21 +416,6 @@ export const successfulFilterInputByFieldMetadataType: { return record.textField.toLowerCase().includes('es'); }, }, - //TODO - regex, iregex not working ? to remove from gql schema ? - // { - // gqlFilterInput: { textField: { regex: '^test$' } }, - // restFilterInput: 'textField[regex]:"^test$"', - // validateFilter: (record: Record) => { - // return record.textField.includes('test'); - // }, - // }, - // { - // gqlFilterInput: { textField: { iregex: '^test$' } }, - // restFilterInput: 'textField[iregex]:"^test$"', - // validateFilter: (record: Record) => { - // return record.textField.toLowerCase().includes('test'); - // }, - // }, ], [FieldMetadataType.PHONES]: [ { @@ -470,7 +431,7 @@ export const successfulFilterInputByFieldMetadataType: { gqlFilterInput: { phonesField: { primaryPhoneCallingCode: { is: 'NOT_NULL' } }, }, - restFilterInput: 'phonesField.primaryPhoneCallingCode[is]:"NOT_NULL"', + restFilterInput: 'phonesField.primaryPhoneCallingCode[is]:NOT_NULL', validateFilter: (record: Record) => { return record.phonesField.primaryPhoneCallingCode !== null; }, @@ -483,7 +444,7 @@ export const successfulFilterInputByFieldMetadataType: { }, }, }, - restFilterInput: 'phonesField.additionalPhones[is]:"NOT_NULL"', + restFilterInput: 'phonesField.additionalPhones[is]:NOT_NULL', validateFilter: (record: Record) => { return record.phonesField.additionalPhones !== null; }, @@ -497,17 +458,6 @@ export const successfulFilterInputByFieldMetadataType: { return record.phonesField.primaryPhoneCountryCode.includes('FR'); }, }, - // TODO - should be allowed ? two conditions simultaneously - // { - // gqlFilterInput: { - // phonesField: { - // primaryPhoneCountryCode: { is: 'NULL' }, - // primaryPhoneCallingCode: { is: 'NULL' }, - // }, - // }, - // restFilterInput: - // 'phonesField[primaryPhoneCountryCode][is]=NULL&phonesField[primaryPhoneCallingCode][is]=NULL', - // }, ], [FieldMetadataType.EMAILS]: [ { @@ -523,21 +473,23 @@ export const successfulFilterInputByFieldMetadataType: { gqlFilterInput: { emailsField: { additionalEmails: { is: 'NOT_NULL' } }, }, - restFilterInput: 'emailsField.additionalEmails[is]:"NOT_NULL"', + restFilterInput: 'emailsField.additionalEmails[is]:NOT_NULL', validateFilter: (record: Record) => { return record.emailsField.additionalEmails !== null; }, }, ], [FieldMetadataType.DATE_TIME]: [ - // TODO - fix this ? not working - // { - // gqlFilterInput: { dateTimeField: { eq: '2025-01-01T00:00:00Z' } }, - // restFilterInput: 'dateTimeField[eq]=2025-01-01T00:00:00Z', - // validateFilter: (record: Record) => { - // return record.dateTimeField === '2025-01-01T00:00:00Z'; - // }, - // }, + { + gqlFilterInput: { dateTimeField: { eq: '2025-01-01T00:00:00Z' } }, + restFilterInput: 'dateTimeField[eq]:"2025-01-01T00:00:00Z"', + validateFilter: (record: Record) => { + return ( + new Date(record.dateTimeField).getTime() === + new Date('2025-01-01T00:00:00Z').getTime() + ); + }, + }, { gqlFilterInput: { dateTimeField: { gt: '2005-01-01T00:00:00Z' } }, restFilterInput: 'dateTimeField[gt]:"2005-01-01T00:00:00Z"', @@ -552,14 +504,16 @@ export const successfulFilterInputByFieldMetadataType: { return record.dateTimeField >= '2005-01-01T00:00:00Z'; }, }, - // TODO - fix this ? not working - // { - // gqlFilterInput: { dateTimeField: { in: ['2025-01-01T00:00:00Z'] } }, - // restFilterInput: 'dateTimeField[in]=2025-01-01T00:00:00Z', - // validateFilter: (record: Record) => { - // return record.dateTimeField === '2025-01-01T00:00:00Z'; - // }, - // }, + { + gqlFilterInput: { dateTimeField: { in: ['2025-01-01T00:00:00Z'] } }, + restFilterInput: 'dateTimeField[in]:["2025-01-01T00:00:00Z"]', + validateFilter: (record: Record) => { + return ( + new Date(record.dateTimeField).getTime() === + new Date('2025-01-01T00:00:00Z').getTime() + ); + }, + }, { gqlFilterInput: { dateTimeField: { lt: '2125-01-01T00:00:00Z' } }, restFilterInput: 'dateTimeField[lt]:"2125-01-01T00:00:00Z"', @@ -583,45 +537,27 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { dateTimeField: { is: 'NOT_NULL' } }, - restFilterInput: 'dateTimeField[is]:"NOT_NULL"', + restFilterInput: 'dateTimeField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.dateTimeField); }, }, - // { - // gqlFilterInput: { dateTimeField: { is: 'NULL' } }, - // // TODO - fix this ? not working - // // restFilterInput: 'dateTimeField[is]:"NULL"', - // validateFilter: (record: Record) => { - // return record.dateTimeField === null; - // }, - // }, - // TODO - fix this, should be returning or not be allowed - // { - // gqlFilterInput: { dateTimeField: { eq: null } }, - // restFilterInput: 'dateTimeField[eq]=null', - // validateFilter: (record: Record) => { - // return record.dateTimeField === null; - // }, - // }, - // TODO - why float are allowed ? - // { - // gqlFilterInput: { dateTimeField: { eq: 2 } }, - // restFilterInput: 'dateTimeField[eq]=2', - // validateFilter: (record: Record) => { - // return record.dateTimeField === 2; - // }, - // }, + { + gqlFilterInput: { dateTimeField: { is: 'NULL' } }, + restFilterInput: 'dateTimeField[is]:NULL', + validateFilter: (record: Record) => { + return record.dateTimeField === null; + }, + }, ], [FieldMetadataType.DATE]: [ - // TODO - fix this ? not working - // { - // gqlFilterInput: { dateField: { eq: '2025-01-01' } }, - // restFilterInput: 'dateField[eq]=2025-01-01', - // validateFilter: (record: Record) => { - // return record.dateField === '2025-01-01'; - // }, - // }, + { + gqlFilterInput: { dateField: { eq: '2025-01-01' } }, + restFilterInput: 'dateField[eq]:"2025-01-01"', + validateFilter: (record: Record) => { + return record.dateField === '2025-01-01'; + }, + }, { gqlFilterInput: { dateField: { neq: '2005-01-01' } }, restFilterInput: 'dateField[neq]:"2005-01-01"', @@ -643,14 +579,13 @@ export const successfulFilterInputByFieldMetadataType: { return record.dateField >= '2005-01-01'; }, }, - // TODO - fix this ? not working - // { - // gqlFilterInput: { dateField: { in: ['2025-01-01'] } }, - // restFilterInput: 'dateField[in]=2025-01-01', - // validateFilter: (record: Record) => { - // return record.dateField === '2025-01-01'; - // }, - // }, + { + gqlFilterInput: { dateField: { in: ['2025-01-01'] } }, + restFilterInput: 'dateField[in]:["2025-01-01"]', + validateFilter: (record: Record) => { + return record.dateField === '2025-01-01'; + }, + }, { gqlFilterInput: { dateField: { lt: '2125-01-01' } }, restFilterInput: 'dateField[lt]:"2125-01-01"', @@ -667,7 +602,7 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { dateField: { is: 'NOT_NULL' } }, - restFilterInput: 'dateField[is]:"NOT_NULL"', + restFilterInput: 'dateField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.dateField); }, @@ -679,32 +614,15 @@ export const successfulFilterInputByFieldMetadataType: { return record.dateField === null; }, }, - // TODO - fix this, should be returning or not be allowed - // { - // gqlFilterInput: { dateField: { eq: null } }, - // restFilterInput: 'dateField[eq]=null', - // validateFilter: (record: Record) => { - // return record.dateField === null; - // }, - // }, - // TODO - why float are allowed ? - // { - // gqlFilterInput: { dateField: { eq: 2 } }, - // restFilterInput: 'dateField[eq]=2', - // validateFilter: (record: Record) => { - // return record.dateField === 2; - // }, - // }, ], [FieldMetadataType.BOOLEAN]: [ - // { - // gqlFilterInput: { booleanField: { eq: true } }, - // // TODO - fix this ? not working - // // restFilterInput: 'booleanField[eq]:"true"', - // validateFilter: (record: Record) => { - // return record.booleanField === true; - // }, - // }, + { + gqlFilterInput: { booleanField: { eq: true } }, + restFilterInput: 'booleanField[eq]:"true"', + validateFilter: (record: Record) => { + return record.booleanField === true; + }, + }, { gqlFilterInput: { booleanField: { is: 'NULL' } }, restFilterInput: 'booleanField[is]:NULL', @@ -714,7 +632,7 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { booleanField: { is: 'NOT_NULL' } }, - restFilterInput: 'booleanField[is]:"NOT_NULL"', + restFilterInput: 'booleanField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.booleanField); }, @@ -772,7 +690,7 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { numberField: { is: 'NOT_NULL' } }, - restFilterInput: 'numberField[is]:"NOT_NULL"', + restFilterInput: 'numberField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.numberField); }, @@ -877,16 +795,11 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { ratingField: { is: 'NOT_NULL' } }, - restFilterInput: 'ratingField[is]:"NOT_NULL"', + restFilterInput: 'ratingField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.ratingField); }, }, - // TODO - fix this, should be ok - // { - // gqlFilterInput: { ratingField: { isEmptyArray: true } }, - // restFilterInput: 'ratingField[isEmptyArray]=true', - // }, ], [FieldMetadataType.SELECT]: [ { @@ -936,14 +849,6 @@ export const successfulFilterInputByFieldMetadataType: { return isDefined(record.multiSelectField); }, }, - //TODO - null and empty array should be equivalent - // { - // gqlFilterInput: { multiSelectField: { isEmptyArray: true } }, - // restFilterInput: 'multiSelectField[isEmptyArray]=true', - // validateFilter: (record: Record) => { - // return record.multiSelectField.length === 0; - // }, - // }, ], [FieldMetadataType.ADDRESS]: [ { @@ -968,7 +873,7 @@ export const successfulFilterInputByFieldMetadataType: { gqlFilterInput: { addressField: { addressStreet2: { is: 'NOT_NULL' } }, }, - restFilterInput: 'addressField.addressStreet2[is]:"NOT_NULL"', + restFilterInput: 'addressField.addressStreet2[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.addressField.addressStreet2); }, @@ -1020,7 +925,7 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { rawJsonField: { is: 'NOT_NULL' } }, - restFilterInput: 'rawJsonField[is]:"NOT_NULL"', + restFilterInput: 'rawJsonField[is]:NOT_NULL', validateFilter: (record: Record) => { return isDefined(record.rawJsonField); }, @@ -1034,14 +939,13 @@ export const successfulFilterInputByFieldMetadataType: { }, ], [FieldMetadataType.ARRAY]: [ - // { - // gqlFilterInput: { arrayField: { containsIlike: 'test' } }, - // // TODO - fix this ? not existing for rest - // // restFilterInput: 'arrayField[containsIlike]:"test"', - // validateFilter: (record: Record) => { - // return JSON.stringify(record.arrayField).includes('test'); - // }, - // }, + { + gqlFilterInput: { arrayField: { containsIlike: 'test' } }, + restFilterInput: '', + validateFilter: (record: Record) => { + return JSON.stringify(record.arrayField).includes('test'); + }, + }, { gqlFilterInput: { arrayField: { is: 'NULL' } }, restFilterInput: 'arrayField[is]:NULL', @@ -1052,14 +956,6 @@ export const successfulFilterInputByFieldMetadataType: { ); }, }, - //TODO - null and empty array should be equivalent - // { - // gqlFilterInput: { arrayField: { isEmptyArray: true } }, - // restFilterInput: 'arrayField[isEmptyArray]=true', - // validateFilter: (record: Record) => { - // return record.arrayField.length === 0; - // }, - // }, ], [FieldMetadataType.FILES]: [ { @@ -1071,7 +967,7 @@ export const successfulFilterInputByFieldMetadataType: { }, { gqlFilterInput: { filesField: { is: 'NOT_NULL' } }, - restFilterInput: 'filesField[is]:"NOT_NULL"', + restFilterInput: 'filesField[is]:NOT_NULL', validateFilter: (record: Record) => { return Array.isArray(record.filesField) && record.filesField.length > 0; }, diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts index e04c5fb671..6920511336 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts index d4baa7c5d2..5a4cd5b881 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/files-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/files-field-filter-input-validation.integration-spec.ts index 420e754f25..d3a44e286b 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/files-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/files-field-filter-input-validation.integration-spec.ts @@ -48,12 +48,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -61,18 +60,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/morph-relation-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/morph-relation-field-filter-input-validation.integration-spec.ts index e5011396a0..38473fcfc7 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/morph-relation-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/morph-relation-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts index 7844599e3e..582efc4827 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts index 29fb05d107..12cf3803b1 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts @@ -1,13 +1,16 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util'; import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util'; import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; import { FieldMetadataType } from 'twenty-shared/types'; const FIELD_METADATA_TYPE = FieldMetadataType.NUMBER; -// const failingTestCases = -// failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; const successfulTestCases = successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; @@ -36,43 +39,39 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { ]); }); - // describe('Gql filter input - failure', () => { - // it.each( - // failingTestCases.map((testCase) => ({ - // ...testCase, - // stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), - // })), - // )( - // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - // async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { - // await testGqlFailingScenario( - // objectMetadataSingularName, - // objectMetadataPluralName, - // filter, - // errorMessage, - // ); - // }, - // ); - // }); + describe('Gql filter input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + ); + }, + ); + }); - // // TODO : Refacto-common - Uncomment this - // describe('Rest filter input - failure', () => { - // it.each( - // failingTestCases.map((testCase) => ({ - // ...testCase, - // stringifiedFilter: JSON.stringify(testCase.restFilterInput), - // })), - // )( - // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - // await testRestFailingScenario( - // objectMetadataPluralName, - // filter, - // errorMessage, - // ); - // }, - // ); - // }); + describe('Rest filter input - failure', () => { + it.each( + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); + }, + ); + }); describe('Gql filter input - success', () => { it.each( diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts index 753e8cd426..9c8f0c1dca 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts index f0fb2fb44e..bcfa49d21c 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts @@ -1,13 +1,16 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util'; import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util'; import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; import { FieldMetadataType } from 'twenty-shared/types'; const FIELD_METADATA_TYPE = FieldMetadataType.RAW_JSON; -// const failingTestCases = -// failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; const successfulTestCases = successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; @@ -36,43 +39,43 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { ]); }); - // describe('Gql filter input - failure', () => { - // it.each( - // failingTestCases.map((testCase) => ({ - // ...testCase, - // stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), - // })), - // )( - // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - // async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { - // await testGqlFailingScenario( - // objectMetadataSingularName, - // objectMetadataPluralName, - // filter, - // errorMessage, - // ); - // }, - // ); - // }); + describe('Gql filter input - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + ); + }, + ); + }); - // // TODO : Refacto-common - Uncomment this - // describe('Rest filter input - failure', () => { - // it.each( - // failingTestCases.map((testCase) => ({ - // ...testCase, - // stringifiedFilter: JSON.stringify(testCase.restFilterInput), - // })), - // )( - // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - // await testRestFailingScenario( - // objectMetadataPluralName, - // filter, - // errorMessage, - // ); - // }, - // ); - // }); + describe('Rest filter input - failure', () => { + const restFailingTestCases = failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })); + + if (restFailingTestCases.length === 0) { + it.skip('No REST failing test cases for this field type', () => {}); + } else { + it.each(restFailingTestCases)( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); + }, + ); + } + }); describe('Gql filter input - success', () => { it.each( diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts index 29f5b68ad6..4a61db9cdf 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts index fbb383c988..c8f561a4a1 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts @@ -2,6 +2,7 @@ import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/ import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; import { testGqlFailingScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util'; import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util'; import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util'; import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/inputs-validation/utils/destroy-many-objects-metadata'; import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; @@ -46,35 +47,31 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); }); - // TODO : Refacto-common - Uncomment this - // describe('Rest filter input - failure', () => { - // it.each( - // failingTestCases.map((testCase) => ({ - // ...testCase, - // stringifiedFilter: JSON.stringify(testCase.restFilterInput), - // })), - // )( - // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - // await testRestFailingScenario( - // objectMetadataPluralName, - // filter, - // errorMessage, - // ); - // }, - // ); - // }); + describe('Rest filter input - failure', () => { + it.each( + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); + }, + ); + }); describe('Gql filter input - success', () => { it.each( diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts index 72ea697052..8616b9bd78 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts @@ -40,40 +40,45 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { }); describe('Gql filter input - failure', () => { - it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), - })), - )( - `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { - await testGqlFailingScenario( - objectMetadataSingularName, - objectMetadataPluralName, - filter, - errorMessage, - ); - }, - ); + if (failingTestCases.length === 0) { + it.skip('No failing test cases for this field type', () => {}); + } else { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + ); + }, + ); + } }); describe('Rest filter input - failure', () => { - it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), - )( - `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); - }, - ); + if (failingTestCases.length === 0) { + it.skip('No failing test cases for this field type', () => {}); + } else { + it.each( + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); + }, + ); + } }); describe('Gql filter input - success', () => { @@ -97,10 +102,12 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - success', () => { it.each( - successfulTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + successfulTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, async ({ restFilterInput, validateFilter }) => { diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util.ts index ae99e45dcd..831d32251c 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-gql-failing-scenario.util.ts @@ -5,7 +5,6 @@ export const testGqlFailingScenario = async ( objectMetadataSingularName: string, objectMetadataPluralName: string, filter: any, - errorMessage: string, ) => { const graphqlOperation = findManyOperationFactory({ objectMetadataSingularName: objectMetadataSingularName, @@ -17,5 +16,5 @@ export const testGqlFailingScenario = async ( const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation); expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toContain(errorMessage); + expect(response.body.errors[0].message).toMatchSnapshot(); }; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util.ts index d4ebd64ebc..7aaeef47d9 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-failing-scenario.util.ts @@ -3,7 +3,6 @@ import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-re export const testRestFailingScenario = async ( objectMetadataPluralName: string, filter: any, - errorMessage: string, ) => { const encodedFilter = encodeURIComponent(filter); const response = await makeRestAPIRequest({ @@ -12,5 +11,5 @@ export const testRestFailingScenario = async ( }); expect(response.body.error).toBeDefined(); - expect(JSON.stringify(response.body.messages)).toContain(errorMessage); + expect(response.body.messages).toMatchSnapshot(); }; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util.ts index 7baf8d404b..de3a961a42 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/utils/test-rest-successful-scenario.util.ts @@ -11,6 +11,14 @@ export const testRestSuccessfulScenario = async ( path: `/${objectMetadataPluralName}?filter=${encodedFilter}`, }); + if (response.body.error || response.body.messages) { + throw new Error( + `REST API error: ${response.body.error} - ${JSON.stringify(response.body.messages)}`, + ); + } + + expect(response.body.data).toBeDefined(); + const records = response.body.data[objectMetadataPluralName]; expect(response.body.errors).toBeUndefined(); diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts index f63e39f75e..008f98e48b 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts @@ -47,12 +47,11 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + async ({ gqlFilterInput: filter }) => { await testGqlFailingScenario( objectMetadataSingularName, objectMetadataPluralName, filter, - errorMessage, ); }, ); @@ -60,18 +59,16 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - failure', () => { it.each( - failingTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + failingTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, - async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { - await testRestFailingScenario( - objectMetadataPluralName, - filter, - errorMessage, - ); + async ({ restFilterInput: filter }) => { + await testRestFailingScenario(objectMetadataPluralName, filter); }, ); }); @@ -97,10 +94,12 @@ describe(`Filter input validation - ${FIELD_METADATA_TYPE}`, () => { describe('Rest filter input - success', () => { it.each( - successfulTestCases.map((testCase) => ({ - ...testCase, - stringifiedFilter: JSON.stringify(testCase.restFilterInput), - })), + successfulTestCases + .filter((testCase) => testCase.restFilterInput) + .map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), )( `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, async ({ restFilterInput, validateFilter }) => {