Common API - Filter validation layer (#18187)

Closes https://github.com/twentyhq/core-team-issues/issues/1627

**FilterArgProcessor consolidation:**
Refactored to both validate AND transform filter values in a single pass
Coerced string inputs to native types (e.g., "1" → 1, "true" → true -
useful for Rest input)
Returns transformed filter instead of just validating
Removed overrideFilterByFieldMetadata calls from all computeArgs methods
**QueryRunnerArgsFactory cleanup**
**Testing:**
Add unit testing
uncomment integration tests
This commit is contained in:
Etienne
2026-03-04 13:10:44 +01:00
committed by GitHub
parent 80d054563e
commit 906a0aed38
82 changed files with 3068 additions and 1015 deletions
@@ -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
@@ -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""`;
@@ -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""`;
@@ -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<RecordPositionService>;
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>(DataArgProcessor);
dataArgProcessorService = module.get<DataArgProcessorService>(
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,
@@ -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({
@@ -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(
@@ -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);
@@ -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(
@@ -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"`;
@@ -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<string, unknown>;
}[];
} = {
[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' } } } },
],
};
@@ -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<string, unknown>;
expected?: Record<string, unknown>;
}[];
} = {
[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' } } } },
],
};
@@ -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<FlatFieldMetadata> => {
const byUniversalIdentifier: Record<string, FlatFieldMetadata> = {};
const universalIdentifierById: Record<string, string> = {};
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<FlatFieldMetadata>;
};
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>(
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] } });
});
});
});
@@ -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'];
@@ -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<T extends ObjectRecordFilter | undefined>({
filter,
flatObjectMetadata,
flatFieldMetadataMaps,
}: {
filter: T;
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): 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<FlatFieldMetadata>,
fieldIdByName: Record<string, string>,
fieldIdByJoinColumnName: Record<string, string>,
): 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<string, unknown>,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
fieldIdByName: Record<string, string>,
fieldIdByJoinColumnName: Record<string, string>,
): Record<string, unknown> {
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<FlatFieldMetadata>(
{
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<string, unknown>,
): Record<string, unknown> {
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<string, unknown> = {};
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<string, unknown>,
{
...fieldMetadata,
type: subFieldMetadata.type as FieldMetadataType,
},
);
}
return transformedFilter;
}
}
@@ -0,0 +1,17 @@
export type FilterOperator =
| 'eq'
| 'neq'
| 'gt'
| 'gte'
| 'lt'
| 'lte'
| 'in'
| 'is'
| 'like'
| 'ilike'
| 'startsWith'
| 'endsWith'
| 'containsAny'
| 'containsIlike'
| 'isEmptyArray'
| 'search';
@@ -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');
});
});
@@ -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,
);
});
});
@@ -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<string, unknown>,
fieldMetadata,
),
).toThrow(CommonQueryRunnerException);
});
it('should throw when filter has no operators', () => {
const fieldMetadata = createFieldMetadata(FieldMetadataType.TEXT);
expect(() =>
validateAndTransformOperatorAndValue('testField', {}, fieldMetadata),
).toThrow(CommonQueryRunnerException);
});
});
@@ -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);
});
});
@@ -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);
});
});
@@ -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,
);
});
});
@@ -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);
});
});
@@ -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);
}
};
@@ -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);
}
};
@@ -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);
});
};
@@ -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<string, unknown>,
fieldMetadata: FlatFieldMetadata,
): Record<string, unknown> => {
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 };
};
@@ -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;
}
};
@@ -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;
}
};
@@ -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`,
},
);
}
};
@@ -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`,
},
);
}
};
@@ -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` },
);
}
};
@@ -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`,
},
);
}
};
@@ -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;
};
@@ -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'`,
},
);
};
@@ -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'`,
},
);
};
@@ -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;
};
@@ -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;
};
@@ -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<FlatFieldMetadata>,
): 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<string, string>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
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
@@ -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()
@@ -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,
),
}),
};
}
@@ -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,
),
}),
};
}
@@ -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,
),
}),
};
}
@@ -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,
),
}),
};
}
@@ -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,
),
}),
};
}
@@ -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,
),
}),
};
}
@@ -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],
@@ -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',
@@ -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:
@@ -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: