From 3b992ed549680e0f84117eddf9cd675371f283aa Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:42:22 +0200 Subject: [PATCH] Rest/Gql - Filter input - Integration tests (#14836) --- ...ilter-input-validation.integration-spec.ts | 70 ++ ...ilter-input-validation.integration-spec.ts | 110 ++ ...ilter-input-validation.integration-spec.ts | 110 ++ ...r-input-by-field-metadata-type.constant.ts | 416 ++++++++ ...r-input-by-field-metadata-type.constant.ts | 938 ++++++++++++++++++ .../test-object-gql-fields.constant.ts | 49 + ...ilter-input-validation.integration-spec.ts | 70 ++ ...ilter-input-validation.integration-spec.ts | 113 +++ ...ilter-input-validation.integration-spec.ts | 113 +++ ...ilter-input-validation.integration-spec.ts | 70 ++ ...ilter-input-validation.integration-spec.ts | 70 ++ ...ilter-input-validation.integration-spec.ts | 70 ++ ...ilter-input-validation.integration-spec.ts | 113 +++ ...ilter-input-validation.integration-spec.ts | 110 ++ ...ilter-input-validation.integration-spec.ts | 70 ++ ...ilter-input-validation.integration-spec.ts | 113 +++ ...ilter-input-validation.integration-spec.ts | 110 ++ ...ilter-input-validation.integration-spec.ts | 113 +++ ...ilter-input-validation.integration-spec.ts | 112 +++ ...ilter-input-validation.integration-spec.ts | 113 +++ .../utils/test-gql-failing-scenario.util.ts | 21 + .../test-gql-successful-scenario.util.ts | 30 + .../utils/test-rest-failing-scenario.util.ts | 16 + .../test-rest-successful-scenario.util.ts | 23 + ...ilter-input-validation.integration-spec.ts | 113 +++ .../types/field-metadata-type-to-test.ts | 15 + .../utils/destroy-many-objects-metadata.ts | 22 + ...get-field-metadata-creation-inputs.util.ts | 178 ++++ ...-test-objects-with-all-field-types.util.ts | 148 +++ 29 files changed, 3619 insertions(+) create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/address-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/test-object-gql-fields.constant.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/currency-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/emails-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/full-name-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/links-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/phones-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/utils/get-field-metadata-creation-inputs.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util.ts diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/address-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/address-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..e2bcfa8e2d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/address-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,70 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.ADDRESS; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..66e475bfc8 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/array-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,110 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.ARRAY; +// const failingTestCases = +// failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + // describe('Gql filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + // await testGqlFailingScenario( + // objectMetadataSingularName, + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + // // TODO : Refacto-common - Uncomment this + // describe('Rest filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.restFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + // await testRestFailingScenario( + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..32ca1f47ef --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/boolean-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,110 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.BOOLEAN; +// const failingTestCases = +// failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + // describe('Gql filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + // await testGqlFailingScenario( + // objectMetadataSingularName, + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + // // TODO : Refacto-common - Uncomment this + // describe('Rest filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.restFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + // await testRestFailingScenario( + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts new file mode 100644 index 0000000000..bcee26c785 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts @@ -0,0 +1,416 @@ +import { type FieldMetadataTypeToTest } from 'test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; + +export const failingFilterInputByFieldMetadataType: { + [K in Exclude]: { + gqlFilterInput: any; + restFilterInput: any; + gqlErrorMessage: string; + restErrorMessage: string; + }[]; +} = { + [FieldMetadataType.RELATION]: [ + { + gqlFilterInput: { + manyToOneRelationField: { + eq: '6dd71a46-68fe-4420-82b3-0d5b00ad2642', + }, + }, + gqlErrorMessage: 'is not defined by type', + restFilterInput: + 'manyToOneRelationField[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"', + restErrorMessage: "'manyToOneRelationField' does not exist", + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + eq: 'invalid-uuid', + }, + }, + gqlErrorMessage: 'Invalid UUID', + restFilterInput: 'manyToOneRelationFieldId[eq]:"invalid-uuid"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { + oneToManyRelationField: { + eq: '6dd71a46-68fe-4420-82b3-0d5b00ad2642', + }, + }, + gqlErrorMessage: 'is not defined by type', + restFilterInput: + 'oneToManyRelationFieldId[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"', + restErrorMessage: "'oneToManyRelationFieldId' does not exist", + }, + // { + // gqlFilterInput: { + // oneToManyRelationFieldId: { + // eq: 'invalid-uuid', + // }, + // }, + // restFilterInput: 'oneToManyRelationFieldId[eq]:"invalid-uuid"', + // gqlErrorMessage: 'is not defined by type', + // restErrorMessage: "'oneToManyRelationFieldId' does not exist", + // }, + ], + [FieldMetadataType.UUID]: [ + { + gqlFilterInput: { uuidField: { eq: 'invalid-uuid' } }, + gqlErrorMessage: 'Invalid UUID', + restFilterInput: 'uuidField[eq]:"invalid-uuid"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { uuidField: { eq: undefined } }, + gqlErrorMessage: + 'undefined is not iterable (cannot read property Symbol(Symbol.iterator))', + restFilterInput: 'uuidField[eq]:"undefined"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { uuidField: { eq: 2 } }, + gqlErrorMessage: 'UUID must be a string', + restFilterInput: 'uuidField[eq]:2', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { uuidField: { eq: {} } }, + gqlErrorMessage: 'UUID must be a string', + restFilterInput: 'uuidField[eq]:"{}"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { uuidField: { eq: [] } }, + gqlErrorMessage: 'UUID must be a string', + restFilterInput: 'uuidField[eq]:"[]"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { uuidField: { eq: true } }, + gqlErrorMessage: 'UUID must be a string', + restFilterInput: 'uuidField[eq]:"true"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + { + gqlFilterInput: { uuidField: { eq: '2025-01-01' } }, + gqlErrorMessage: 'Invalid UUID', + restFilterInput: 'uuidField[eq]:"2025-01-01"', + restErrorMessage: 'invalid input syntax for type uuid', + }, + ], + [FieldMetadataType.TEXT]: [ + { + gqlFilterInput: { + textField: { regex: 'test' }, + }, + gqlErrorMessage: 'Operator "regex" is not supported', + restFilterInput: 'textField[regex]:test', + restErrorMessage: 'comparator regex not in', + }, + { + gqlFilterInput: { + textField: { iregex: 'test' }, + }, + gqlErrorMessage: 'Operator "iregex" is not supported', + restFilterInput: 'textField[iregex]:test', + restErrorMessage: 'comparator iregex not in', + }, + ], + [FieldMetadataType.DATE_TIME]: [ + { + gqlFilterInput: { dateTimeField: { eq: 'not-a-date-time' } }, + gqlErrorMessage: + 'invalid input syntax for type timestamp with time zone: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + restFilterInput: 'dateTimeField[eq]:"not-a-date-time"', + restErrorMessage: + 'invalid input syntax for type timestamp with time zone', + }, + { + gqlFilterInput: { dateTimeField: { eq: {} } }, + gqlErrorMessage: + 'invalid input syntax for type timestamp with time zone: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + restFilterInput: 'dateTimeField[eq]:"{}"', + restErrorMessage: + 'invalid input syntax for type timestamp with time zone', + }, + { + gqlFilterInput: { dateTimeField: { eq: [] } }, + gqlErrorMessage: + 'invalid input syntax for type timestamp with time zone: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + restFilterInput: 'dateTimeField[eq]:"[]"', + restErrorMessage: + 'invalid input syntax for type timestamp with time zone', + }, + // TODO - fix this, should throw an error + // { + // gqlFilterInput: { dateTimeField: { eq: true } }, + // gqlErrorMessage: + // 'invalid input syntax for type timestamp with time zone: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + // }, + ], + [FieldMetadataType.DATE]: [ + { + gqlFilterInput: { dateField: { eq: 'not-a-date' } }, + gqlErrorMessage: + 'invalid input syntax for type date: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + restFilterInput: 'dateField[eq]:"{}"', + restErrorMessage: 'invalid input syntax for type date', + }, + { + gqlFilterInput: { dateField: { eq: {} } }, + gqlErrorMessage: + 'invalid input syntax for type date: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + restFilterInput: 'dateField[eq]:"{}"', + restErrorMessage: 'invalid input syntax for type date', + }, + { + gqlFilterInput: { dateField: { eq: [] } }, + gqlErrorMessage: + 'invalid input syntax for type date: "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN"', + restFilterInput: 'dateField[eq]:"[]"', + restErrorMessage: 'invalid input syntax for type date', + }, + ], + [FieldMetadataType.BOOLEAN]: [ + // { + // gqlFilterInput: { booleanField: { eq: 'not-a-boolean' } }, + // gqlErrorMessage: + // 'Boolean cannot represent a non boolean value: "not-a-boolean"', + // // TODO - fix this, should throw an error + // // restFilterInput: 'booleanField[eq]:"not-a-boolean"', + // // restErrorMessage: 'invalid input syntax for type boolean', + // }, + // { + // gqlFilterInput: { booleanField: { eq: [] } }, + // gqlErrorMessage: 'Boolean cannot represent a non boolean value: []', + // // TODO - fix this, should throw an error + // // restFilterInput: 'booleanField[eq]:"[]"', + // // restErrorMessage: 'invalid input syntax for type boolean', + // }, + // { + // gqlFilterInput: { booleanField: { eq: 2 } }, + // gqlErrorMessage: 'Boolean cannot represent a non boolean value: 2', + // // TODO - fix this, should throw an error + // // restFilterInput: 'booleanField[eq]:2', + // // restErrorMessage: 'invalid input syntax for type boolean', + // }, + // TODO - fix this, should throw an error + // { + // gqlFilterInput: { booleanField: { eq: null } }, + // gqlErrorMessage: 'Boolean cannot represent a non boolean value: null', + // }, + ], + [FieldMetadataType.NUMBER]: [ + // { + // gqlFilterInput: { numberField: { eq: 'not-a-number' } }, + // gqlErrorMessage: + // 'Float cannot represent non numeric value: "not-a-number"', + // // TODO - fix this, should throw an error + // // restFilterInput: 'numberField[eq]:"not-a-number"', + // // restErrorMessage: 'invalid input syntax for type float', + // }, + // { + // gqlFilterInput: { numberField: { eq: {} } }, + // gqlErrorMessage: 'Float cannot represent non numeric value: {}', + // // TODO - fix this, should throw an error + // // restFilterInput: 'numberField[eq]:"{}"', + // // restErrorMessage: 'invalid input syntax for type float', + // }, + // { + // gqlFilterInput: { numberField: { eq: [] } }, + // gqlErrorMessage: 'Float cannot represent non numeric value: []', + // // TODO - fix this, should throw an error + // // restFilterInput: 'numberField[eq]:"[]"', + // // restErrorMessage: 'invalid input syntax for type float', + // }, + // { + // gqlFilterInput: { numberField: { eq: true } }, + // gqlErrorMessage: 'Float cannot represent non numeric value: true', + // // TODO - fix this, should throw an error + // // restFilterInput: 'numberField[eq]:"true"', + // // restErrorMessage: 'invalid input syntax for type float', + // }, + // TODO - ensure it should throw + // { + // gqlFilterInput: { numberField: { eq: null } }, + // gqlErrorMessage: 'Float cannot represent non numeric value: null', + // }, + ], + [FieldMetadataType.RATING]: [ + { + gqlFilterInput: { ratingField: { eq: 'not-a-rating' } }, + gqlErrorMessage: 'Value "not-a-rating" does not exist in ', + restFilterInput: 'ratingField[eq]:"not-a-rating"', + restErrorMessage: 'invalid input value for enum', + }, + { + gqlFilterInput: { ratingField: { eq: {} } }, + gqlErrorMessage: 'cannot represent non-string value: {}.', + restFilterInput: 'ratingField[eq]:"{}"', + restErrorMessage: 'invalid input value for enum', + }, + { + gqlFilterInput: { ratingField: { eq: [] } }, + gqlErrorMessage: 'cannot represent non-string value: [].', + restFilterInput: 'ratingField[eq]:"[]"', + restErrorMessage: 'invalid input value for enum', + }, + { + gqlFilterInput: { ratingField: { eq: true } }, + gqlErrorMessage: 'cannot represent non-string value: true.', + restFilterInput: 'ratingField[eq]:"true"', + restErrorMessage: 'invalid input value for enum', + }, + { + gqlFilterInput: { ratingField: { eq: 2 } }, + gqlErrorMessage: 'cannot represent non-string value: 2.', + restFilterInput: 'ratingField[eq]:2', + restErrorMessage: 'invalid input value for enum', + }, + // TODO - ensure it should throw + // { + // gqlFilterInput: { ratingField: { eq: null } }, + // gqlErrorMessage: 'cannot represent non-string value: null.', + // }, + ], + [FieldMetadataType.SELECT]: [ + { + gqlFilterInput: { selectField: { eq: 'not-a-select' } }, + gqlErrorMessage: 'Value "not-a-select" does not exist in', + restFilterInput: 'selectField[eq]:"not-a-select"', + restErrorMessage: "not available in 'selectField'", + }, + { + gqlFilterInput: { selectField: { eq: {} } }, + gqlErrorMessage: 'cannot represent non-string value: {}.', + restFilterInput: 'selectField[eq]:"{}"', + restErrorMessage: "not available in 'selectField'", + }, + { + gqlFilterInput: { selectField: { eq: [] } }, + gqlErrorMessage: 'cannot represent non-string value: [].', + restFilterInput: 'selectField[eq]:"[]"', + restErrorMessage: "not available in 'selectField'", + }, + { + gqlFilterInput: { selectField: { eq: true } }, + gqlErrorMessage: 'cannot represent non-string value: true.', + restFilterInput: 'selectField[eq]:"true"', + restErrorMessage: "not available in 'selectField'", + }, + // TODO - ensure it should throw + // { + // gqlFilterInput: { selectField: { eq: null } }, + // gqlErrorMessage: 'cannot represent non-string value: null.', + // }, + ], + [FieldMetadataType.MULTI_SELECT]: [ + { + gqlFilterInput: { multiSelectField: { eq: 'not-a-multi-select' } }, + gqlErrorMessage: 'Value "not-a-multi-select" does not exist ', + restFilterInput: 'multiSelectField[eq]:"not-a-multi-select"', + restErrorMessage: "not available in 'multiSelectField'", + }, + { + gqlFilterInput: { multiSelectField: { eq: {} } }, + gqlErrorMessage: 'cannot represent non-string value: {}.', + restFilterInput: 'multiSelectField[eq]:"{}"', + restErrorMessage: "not available in 'multiSelectField'", + }, + { + gqlFilterInput: { multiSelectField: { eq: [] } }, + gqlErrorMessage: 'cannot represent non-string value: [].', + restFilterInput: 'multiSelectField[eq]:"[]"', + restErrorMessage: "not available in 'multiSelectField'", + }, + { + gqlFilterInput: { multiSelectField: { eq: true } }, + gqlErrorMessage: 'cannot represent non-string value: true.', + restFilterInput: 'multiSelectField[eq]:"true"', + restErrorMessage: "not available in 'multiSelectField'", + }, + { + gqlFilterInput: { multiSelectField: { eq: 2 } }, + gqlErrorMessage: 'cannot represent non-string value: 2.', + restFilterInput: 'multiSelectField[eq]:2', + restErrorMessage: "enum value '2' not available in 'multiSelectField' ", + }, + // TODO - ensure it should throw + // { + // gqlFilterInput: { multiSelectField: { eq: null } }, + // gqlErrorMessage: 'cannot represent non-string value: null.', + // }, + ], + [FieldMetadataType.RAW_JSON]: [ + // { + // gqlFilterInput: { rawJsonField: { like: {} } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? for rest + // // restFilterInput: 'rawJsonField[like]:"{}"', + // // restErrorMessage: 'cannot represent a non string value', + // }, + // { + // gqlFilterInput: { rawJsonField: { like: [] } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? for rest + // // restFilterInput: 'rawJsonField[like]:"[]"', + // // restErrorMessage: 'cannot represent a non string value', + // }, + // { + // gqlFilterInput: { rawJsonField: { like: true } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? for rest + // // restFilterInput: 'rawJsonField[like]:"true"', + // // restErrorMessage: 'cannot represent a non string value', + // }, + // { + // gqlFilterInput: { rawJsonField: { like: 2 } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? for rest + // // restFilterInput: 'rawJsonField[like]:2', + // // restErrorMessage: 'cannot represent a non string value', + // }, + // TODO - ensure it should throw + // { + // gqlFilterInput: { rawJsonField: { like: null } }, + // gqlErrorMessage: 'cannot represent non-string value: null.', + // }, + ], + [FieldMetadataType.ARRAY]: [ + // { + // gqlFilterInput: { arrayField: { containsIlike: {} } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? containsIlike not existing for rest + // // restFilterInput: 'arrayField[containsIlike]:"{}"', + // // restErrorMessage: '', + // }, + // { + // gqlFilterInput: { arrayField: { containsIlike: [] } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? containsIlike not existing for rest + // // restFilterInput: 'arrayField[containsIlike]:"[]"', + // // restErrorMessage: '', + // }, + // { + // gqlFilterInput: { arrayField: { containsIlike: true } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? containsIlike not existing for rest + // // restFilterInput: 'arrayField[containsIlike]:"true"', + // // restErrorMessage: '', + // }, + // { + // gqlFilterInput: { arrayField: { containsIlike: 2 } }, + // gqlErrorMessage: 'cannot represent a non string value', + // // TODO - fix this ? containsIlike not existing for rest + // // restFilterInput: 'arrayField[containsIlike]:2', + // // restErrorMessage: '', + // }, + // TODO - ensure it should throw + // { + // gqlFilterInput: { arrayField: { containsIlike: null } }, + // gqlErrorMessage: 'cannot represent a non string value', + // }, + ], +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts new file mode 100644 index 0000000000..85fd83d60e --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts @@ -0,0 +1,938 @@ +import { isDefined } from 'class-validator'; +import { type FieldMetadataTypeToTest } from 'test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test'; +import { + TEST_TARGET_OBJECT_RECORD_ID, + TEST_UUID_FIELD_VALUE, +} from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +export const successfulFilterInputByFieldMetadataType: { + [K in FieldMetadataTypeToTest]: { + gqlFilterInput: any; + restFilterInput: string; + validateFilter: (record: Record) => boolean; + }[]; +} = { + [FieldMetadataType.RELATION]: [ + { + gqlFilterInput: { + manyToOneRelationFieldId: { + neq: '00000000-0000-4000-8000-000000000000', + }, + }, + restFilterInput: `manyToOneRelationFieldId[neq]:"00000000-0000-4000-8000-000000000000"`, + validateFilter: (record: Record) => { + return ( + record.manyToOneRelationFieldId !== + '00000000-0000-4000-8000-000000000000' + ); + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + eq: TEST_TARGET_OBJECT_RECORD_ID, + }, + }, + restFilterInput: `manyToOneRelationFieldId[eq]:"${TEST_TARGET_OBJECT_RECORD_ID}"`, + validateFilter: (record: Record) => { + return record.manyToOneRelationFieldId === TEST_TARGET_OBJECT_RECORD_ID; + }, + }, + // TODO - fix this, should be returning or not be allowed + // { + // gqlFilterInput: { + // manyToOneRelationFieldId: { + // eq: null, + // }, + // }, + // restFilterInput: 'manyToOneRelationFieldId[eq]=null', + // validateFilter: (record: Record) => { + // return record.manyToOneRelationField === null; + // }, + // }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + gt: '00000000-0000-4000-8000-000000000000', + }, + }, + restFilterInput: + 'manyToOneRelationFieldId[gt]:"00000000-0000-4000-8000-000000000000"', + validateFilter: (record: Record) => { + return ( + record.manyToOneRelationFieldId > + '00000000-0000-4000-8000-000000000000' + ); + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + gte: '00000000-0000-4000-8000-000000000000', + }, + }, + restFilterInput: `manyToOneRelationFieldId[gte]:"00000000-0000-4000-8000-000000000000"`, + validateFilter: (record: Record) => { + return ( + record.manyToOneRelationFieldId >= + '00000000-0000-4000-8000-000000000000' + ); + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + lt: 'ffffffff-ffff-4fff-bfff-ffffffffffff', + }, + }, + restFilterInput: `manyToOneRelationFieldId[lt]:"ffffffff-ffff-4fff-bfff-ffffffffffff"`, + validateFilter: (record: Record) => { + return ( + record.manyToOneRelationFieldId < + 'ffffffff-ffff-4fff-bfff-ffffffffffff' + ); + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + lte: 'ffffffff-ffff-4fff-bfff-ffffffffffff', + }, + }, + restFilterInput: `manyToOneRelationFieldId[lte]:"ffffffff-ffff-4fff-bfff-ffffffffffff"`, + validateFilter: (record: Record) => { + return ( + record.manyToOneRelationFieldId <= + 'ffffffff-ffff-4fff-bfff-ffffffffffff' + ); + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { + in: [TEST_TARGET_OBJECT_RECORD_ID], + }, + }, + restFilterInput: `manyToOneRelationFieldId[in]:["${TEST_TARGET_OBJECT_RECORD_ID}"]`, + validateFilter: (record: Record) => { + return record.manyToOneRelationFieldId === TEST_TARGET_OBJECT_RECORD_ID; + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { is: 'NULL' }, + }, + restFilterInput: 'manyToOneRelationFieldId[is]:NULL', + validateFilter: (record: Record) => { + return record.manyToOneRelationFieldId === null; + }, + }, + { + gqlFilterInput: { + manyToOneRelationFieldId: { is: 'NOT_NULL' }, + }, + restFilterInput: 'manyToOneRelationFieldId[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.manyToOneRelationFieldId); + }, + }, + ], + [FieldMetadataType.UUID]: [ + { + gqlFilterInput: { + uuidField: { neq: '00000000-0000-4000-8000-000000000000' }, + }, + restFilterInput: `uuidField[neq]:"00000000-0000-4000-8000-000000000000"`, + validateFilter: (record: Record) => { + return record.uuidField !== '00000000-0000-4000-8000-000000000000'; + }, + }, + { + gqlFilterInput: { + uuidField: { eq: TEST_UUID_FIELD_VALUE }, + }, + restFilterInput: `uuidField[eq]:"${TEST_UUID_FIELD_VALUE}"`, + validateFilter: (record: Record) => { + return record.uuidField === TEST_UUID_FIELD_VALUE; + }, + }, + // TODO - fix this, should be returning or not be allowed + // { + // gqlFilterInput: { uuidField: { eq: null } }, + // restFilterInput: 'uuidField[eq]=null', + // validateFilter: (record: Record) => { + // return record.uuidField === null; + // }, + // }, + { + gqlFilterInput: { + uuidField: { gt: '00000000-0000-4000-8000-000000000000' }, + }, + restFilterInput: `uuidField[gt]:"00000000-0000-4000-8000-000000000000"`, + validateFilter: (record: Record) => { + return record.uuidField > '00000000-0000-4000-8000-000000000000'; + }, + }, + { + gqlFilterInput: { + uuidField: { gte: '00000000-0000-4000-8000-000000000000' }, + }, + restFilterInput: `uuidField[gte]:"00000000-0000-4000-8000-000000000000"`, + validateFilter: (record: Record) => { + return record.uuidField >= '00000000-0000-4000-8000-000000000000'; + }, + }, + { + gqlFilterInput: { + uuidField: { lt: 'ffffffff-ffff-4fff-bfff-ffffffffffff' }, + }, + restFilterInput: `uuidField[lt]:"ffffffff-ffff-4fff-bfff-ffffffffffff"`, + validateFilter: (record: Record) => { + return record.uuidField < 'ffffffff-ffff-4fff-bfff-ffffffffffff'; + }, + }, + { + gqlFilterInput: { + uuidField: { lte: 'ffffffff-ffff-4fff-bfff-ffffffffffff' }, + }, + restFilterInput: `uuidField[lte]:"ffffffff-ffff-4fff-bfff-ffffffffffff"`, + validateFilter: (record: Record) => { + return record.uuidField <= 'ffffffff-ffff-4fff-bfff-ffffffffffff'; + }, + }, + { + gqlFilterInput: { + uuidField: { in: [TEST_UUID_FIELD_VALUE] }, + }, + restFilterInput: `uuidField[in]:["${TEST_UUID_FIELD_VALUE}"]`, + validateFilter: (record: Record) => { + return record.uuidField === TEST_UUID_FIELD_VALUE; + }, + }, + { + gqlFilterInput: { uuidField: { is: 'NULL' } }, + restFilterInput: 'uuidField[is]:NULL', + validateFilter: (record: Record) => { + return record.uuidField === null; + }, + }, + { + gqlFilterInput: { uuidField: { is: 'NOT_NULL' } }, + restFilterInput: 'uuidField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.uuidField); + }, + }, + ], + [FieldMetadataType.TEXT]: [ + { + gqlFilterInput: { textField: { neq: 'neqtest' } }, + restFilterInput: 'textField[neq]:"neqtest"', + validateFilter: (record: Record) => { + return record.textField !== 'neqtest'; + }, + }, + { + gqlFilterInput: { textField: { eq: 'test' } }, + restFilterInput: 'textField[eq]:"test"', + validateFilter: (record: Record) => { + return record.textField === 'test'; + }, + }, + // TODO - fix this ? not working + // { + // gqlFilterInput: { textField: { eq: null } }, + // restFilterInput: 'textField[eq]:"null"', + // validateFilter: (record: Record) => { + // return record.textField === ''; + // }, + // }, + { + gqlFilterInput: { textField: { gt: 'tess' } }, + restFilterInput: 'textField[gt]:"tess"', + validateFilter: (record: Record) => { + return record.textField > 'tess'; + }, + }, + { + gqlFilterInput: { textField: { gte: 'tess' } }, + restFilterInput: 'textField[gte]:"tess"', + validateFilter: (record: Record) => { + return record.textField >= 'tess'; + }, + }, + { + gqlFilterInput: { textField: { lt: 'tesu' } }, + restFilterInput: 'textField[lt]:"tesu"', + validateFilter: (record: Record) => { + return record.textField < 'tesu'; + }, + }, + { + gqlFilterInput: { textField: { lte: 'tesu' } }, + restFilterInput: 'textField[lte]:"tesu"', + validateFilter: (record: Record) => { + return record.textField <= 'tesu'; + }, + }, + { + gqlFilterInput: { textField: { in: ['test'] } }, + restFilterInput: 'textField[in]:["test"]', + validateFilter: (record: Record) => { + return record.textField === 'test'; + }, + }, + // TODO - fix this ? not working + // { + // gqlFilterInput: { textField: { is: 'NULL' } }, + // restFilterInput: 'textField[is]:"NULL"', + // validateFilter: (record: Record) => { + // return record.textField === ''; + // }, + // }, + { + gqlFilterInput: { textField: { is: 'NOT_NULL' } }, + restFilterInput: 'textField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.textField); + }, + }, + // TODO - fix this ? not working + // { + // gqlFilterInput: { textField: { startsWith: 'tes' } }, + // restFilterInput: 'textField[startsWith]:"tes"', + // validateFilter: (record: Record) => { + // return record.textField.startsWith('tes'); + // }, + // }, + { + gqlFilterInput: { textField: { like: '%es%' } }, + restFilterInput: 'textField[like]:"%es%"', + validateFilter: (record: Record) => { + return record.textField.includes('es'); + }, + }, + { + gqlFilterInput: { textField: { ilike: '%ES%' } }, + restFilterInput: 'textField[ilike]:"%ES%"', + validateFilter: (record: Record) => { + return record.textField.toLowerCase().includes('es'); + }, + }, + //TODO - regex, iregex not working ? to remove from gql schema ? + // { + // gqlFilterInput: { textField: { regex: '^test$' } }, + // restFilterInput: 'textField[regex]:"^test$"', + // validateFilter: (record: Record) => { + // return record.textField.includes('test'); + // }, + // }, + // { + // gqlFilterInput: { textField: { iregex: '^test$' } }, + // restFilterInput: 'textField[iregex]:"^test$"', + // validateFilter: (record: Record) => { + // return record.textField.toLowerCase().includes('test'); + // }, + // }, + ], + [FieldMetadataType.PHONES]: [ + { + gqlFilterInput: { + phonesField: { primaryPhoneNumber: { eq: '1234567890' } }, + }, + restFilterInput: 'phonesField.primaryPhoneNumber[eq]:"1234567890"', + validateFilter: (record: Record) => { + return record.phonesField.primaryPhoneNumber === '1234567890'; + }, + }, + { + gqlFilterInput: { + phonesField: { primaryPhoneCallingCode: { is: 'NOT_NULL' } }, + }, + restFilterInput: 'phonesField.primaryPhoneCallingCode[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return record.phonesField.primaryPhoneCallingCode !== null; + }, + }, + { + gqlFilterInput: { + phonesField: { + additionalPhones: { + is: 'NOT_NULL', + }, + }, + }, + restFilterInput: 'phonesField.additionalPhones[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return record.phonesField.additionalPhones !== null; + }, + }, + { + gqlFilterInput: { + phonesField: { primaryPhoneCountryCode: { like: 'FR' } }, + }, + restFilterInput: 'phonesField.primaryPhoneCountryCode[like]:"FR"', + validateFilter: (record: Record) => { + return record.phonesField.primaryPhoneCountryCode.includes('FR'); + }, + }, + // TODO - should be allowed ? two conditions simultaneously + // { + // gqlFilterInput: { + // phonesField: { + // primaryPhoneCountryCode: { is: 'NULL' }, + // primaryPhoneCallingCode: { is: 'NULL' }, + // }, + // }, + // restFilterInput: + // 'phonesField[primaryPhoneCountryCode][is]=NULL&phonesField[primaryPhoneCallingCode][is]=NULL', + // }, + ], + [FieldMetadataType.EMAILS]: [ + { + gqlFilterInput: { + emailsField: { primaryEmail: { eq: 'test@test.com' } }, + }, + restFilterInput: 'emailsField.primaryEmail[eq]:"test@test.com"', + validateFilter: (record: Record) => { + return record.emailsField.primaryEmail === 'test@test.com'; + }, + }, + { + gqlFilterInput: { + emailsField: { additionalEmails: { is: 'NOT_NULL' } }, + }, + restFilterInput: 'emailsField.additionalEmails[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return record.emailsField.additionalEmails !== null; + }, + }, + ], + [FieldMetadataType.DATE_TIME]: [ + // TODO - fix this ? not working + // { + // gqlFilterInput: { dateTimeField: { eq: '2025-01-01T00:00:00Z' } }, + // restFilterInput: 'dateTimeField[eq]=2025-01-01T00:00:00Z', + // validateFilter: (record: Record) => { + // return record.dateTimeField === '2025-01-01T00:00:00Z'; + // }, + // }, + { + gqlFilterInput: { dateTimeField: { gt: '2005-01-01T00:00:00Z' } }, + restFilterInput: 'dateTimeField[gt]:"2005-01-01T00:00:00Z"', + validateFilter: (record: Record) => { + return record.dateTimeField > '2005-01-01T00:00:00Z'; + }, + }, + { + gqlFilterInput: { dateTimeField: { gte: '2005-01-01T00:00:00Z' } }, + restFilterInput: 'dateTimeField[gte]:"2005-01-01T00:00:00Z"', + validateFilter: (record: Record) => { + return record.dateTimeField >= '2005-01-01T00:00:00Z'; + }, + }, + // TODO - fix this ? not working + // { + // gqlFilterInput: { dateTimeField: { in: ['2025-01-01T00:00:00Z'] } }, + // restFilterInput: 'dateTimeField[in]=2025-01-01T00:00:00Z', + // validateFilter: (record: Record) => { + // return record.dateTimeField === '2025-01-01T00:00:00Z'; + // }, + // }, + { + gqlFilterInput: { dateTimeField: { lt: '2125-01-01T00:00:00Z' } }, + restFilterInput: 'dateTimeField[lt]:"2125-01-01T00:00:00Z"', + validateFilter: (record: Record) => { + return record.dateTimeField < '2125-01-01T00:00:00Z'; + }, + }, + { + gqlFilterInput: { dateTimeField: { lte: '2125-01-01T00:00:00Z' } }, + restFilterInput: 'dateTimeField[lte]:"2125-01-01T00:00:00Z"', + validateFilter: (record: Record) => { + return record.dateTimeField <= '2125-01-01T00:00:00Z'; + }, + }, + { + gqlFilterInput: { dateTimeField: { neq: '2005-01-01T00:00:00Z' } }, + restFilterInput: 'dateTimeField[neq]:"2005-01-01T00:00:00Z"', + validateFilter: (record: Record) => { + return record.dateTimeField !== '2005-01-01T00:00:00Z'; + }, + }, + { + gqlFilterInput: { dateTimeField: { is: 'NOT_NULL' } }, + restFilterInput: 'dateTimeField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.dateTimeField); + }, + }, + // { + // gqlFilterInput: { dateTimeField: { is: 'NULL' } }, + // // TODO - fix this ? not working + // // restFilterInput: 'dateTimeField[is]:"NULL"', + // validateFilter: (record: Record) => { + // return record.dateTimeField === null; + // }, + // }, + // TODO - fix this, should be returning or not be allowed + // { + // gqlFilterInput: { dateTimeField: { eq: null } }, + // restFilterInput: 'dateTimeField[eq]=null', + // validateFilter: (record: Record) => { + // return record.dateTimeField === null; + // }, + // }, + // TODO - why float are allowed ? + // { + // gqlFilterInput: { dateTimeField: { eq: 2 } }, + // restFilterInput: 'dateTimeField[eq]=2', + // validateFilter: (record: Record) => { + // return record.dateTimeField === 2; + // }, + // }, + ], + [FieldMetadataType.DATE]: [ + // TODO - fix this ? not working + // { + // gqlFilterInput: { dateField: { eq: '2025-01-01' } }, + // restFilterInput: 'dateField[eq]=2025-01-01', + // validateFilter: (record: Record) => { + // return record.dateField === '2025-01-01'; + // }, + // }, + { + gqlFilterInput: { dateField: { neq: '2005-01-01' } }, + restFilterInput: 'dateField[neq]:"2005-01-01"', + validateFilter: (record: Record) => { + return record.dateField !== '2005-01-01'; + }, + }, + { + gqlFilterInput: { dateField: { gt: '2005-01-01' } }, + restFilterInput: 'dateField[gt]:"2005-01-01"', + validateFilter: (record: Record) => { + return record.dateField > '2005-01-01'; + }, + }, + { + gqlFilterInput: { dateField: { gte: '2005-01-01' } }, + restFilterInput: 'dateField[gte]:"2005-01-01"', + validateFilter: (record: Record) => { + return record.dateField >= '2005-01-01'; + }, + }, + // TODO - fix this ? not working + // { + // gqlFilterInput: { dateField: { in: ['2025-01-01'] } }, + // restFilterInput: 'dateField[in]=2025-01-01', + // validateFilter: (record: Record) => { + // return record.dateField === '2025-01-01'; + // }, + // }, + { + gqlFilterInput: { dateField: { lt: '2125-01-01' } }, + restFilterInput: 'dateField[lt]:"2125-01-01"', + validateFilter: (record: Record) => { + return record.dateField < '2125-01-01'; + }, + }, + { + gqlFilterInput: { dateField: { lte: '2125-01-01' } }, + restFilterInput: 'dateField[lte]:"2125-01-01"', + validateFilter: (record: Record) => { + return record.dateField <= '2125-01-01'; + }, + }, + { + gqlFilterInput: { dateField: { is: 'NOT_NULL' } }, + restFilterInput: 'dateField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.dateField); + }, + }, + { + gqlFilterInput: { dateField: { is: 'NULL' } }, + restFilterInput: 'dateField[is]:NULL', + validateFilter: (record: Record) => { + return record.dateField === null; + }, + }, + // TODO - fix this, should be returning or not be allowed + // { + // gqlFilterInput: { dateField: { eq: null } }, + // restFilterInput: 'dateField[eq]=null', + // validateFilter: (record: Record) => { + // return record.dateField === null; + // }, + // }, + // TODO - why float are allowed ? + // { + // gqlFilterInput: { dateField: { eq: 2 } }, + // restFilterInput: 'dateField[eq]=2', + // validateFilter: (record: Record) => { + // return record.dateField === 2; + // }, + // }, + ], + [FieldMetadataType.BOOLEAN]: [ + // { + // gqlFilterInput: { booleanField: { eq: true } }, + // // TODO - fix this ? not working + // // restFilterInput: 'booleanField[eq]:"true"', + // validateFilter: (record: Record) => { + // return record.booleanField === true; + // }, + // }, + { + gqlFilterInput: { booleanField: { is: 'NULL' } }, + restFilterInput: 'booleanField[is]:NULL', + validateFilter: (record: Record) => { + return record.booleanField === null; + }, + }, + { + gqlFilterInput: { booleanField: { is: 'NOT_NULL' } }, + restFilterInput: 'booleanField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.booleanField); + }, + }, + ], + [FieldMetadataType.NUMBER]: [ + { + gqlFilterInput: { numberField: { eq: 1 } }, + restFilterInput: 'numberField[eq]:1', + validateFilter: (record: Record) => { + return record.numberField === 1; + }, + }, + { + gqlFilterInput: { numberField: { gt: 0 } }, + restFilterInput: 'numberField[gt]:0', + validateFilter: (record: Record) => { + return record.numberField > 0; + }, + }, + { + gqlFilterInput: { numberField: { gte: 0 } }, + restFilterInput: 'numberField[gte]:0', + validateFilter: (record: Record) => { + return record.numberField >= 0; + }, + }, + { + gqlFilterInput: { numberField: { lt: 2 } }, + restFilterInput: 'numberField[lt]:2', + validateFilter: (record: Record) => { + return record.numberField < 2; + }, + }, + { + gqlFilterInput: { numberField: { lte: 1 } }, + restFilterInput: 'numberField[lte]:1', + validateFilter: (record: Record) => { + return record.numberField <= 1; + }, + }, + { + gqlFilterInput: { numberField: { neq: 2 } }, + restFilterInput: 'numberField[neq]:2', + validateFilter: (record: Record) => { + return record.numberField !== 2; + }, + }, + { + gqlFilterInput: { numberField: { is: 'NULL' } }, + restFilterInput: 'numberField[is]:NULL', + validateFilter: (record: Record) => { + return record.numberField === null; + }, + }, + { + gqlFilterInput: { numberField: { is: 'NOT_NULL' } }, + restFilterInput: 'numberField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.numberField); + }, + }, + ], + [FieldMetadataType.LINKS]: [ + { + gqlFilterInput: { + linksField: { primaryLinkUrl: { eq: 'twenty.com' } }, + }, + restFilterInput: 'linksField.primaryLinkUrl[eq]:"twenty.com"', + validateFilter: (record: Record) => { + return record.linksField.primaryLinkUrl === 'twenty.com'; + }, + }, + { + gqlFilterInput: { + linksField: { + primaryLinkLabel: { eq: 'twenty - #1 Open source CRM' }, + }, + }, + restFilterInput: + 'linksField.primaryLinkLabel[eq]:"twenty - #1 Open source CRM"', + validateFilter: (record: Record) => { + return ( + record.linksField.primaryLinkLabel === 'twenty - #1 Open source CRM' + ); + }, + }, + { + gqlFilterInput: { linksField: { secondaryLinks: { like: '%twenty%' } } }, + restFilterInput: 'linksField.secondaryLinks[like]:"%twenty%"', + validateFilter: (record: Record) => { + return JSON.stringify(record.linksField.secondaryLinks).includes( + 'twenty', + ); + }, + }, + ], + [FieldMetadataType.CURRENCY]: [ + { + gqlFilterInput: { + currencyField: { amountMicros: { eq: 1000000 } }, + }, + restFilterInput: 'currencyField.amountMicros[eq]:1000000', + validateFilter: (record: Record) => { + return Number(record.currencyField.amountMicros) === 1000000; + }, + }, + { + gqlFilterInput: { currencyField: { currencyCode: { eq: 'USD' } } }, + restFilterInput: 'currencyField.currencyCode[eq]:"USD"', + validateFilter: (record: Record) => { + return record.currencyField.currencyCode === 'USD'; + }, + }, + ], + [FieldMetadataType.FULL_NAME]: [ + { + gqlFilterInput: { fullNameField: { firstName: { eq: 'John' } } }, + restFilterInput: 'fullNameField.firstName[eq]:"John"', + validateFilter: (record: Record) => { + return record.fullNameField.firstName === 'John'; + }, + }, + { + gqlFilterInput: { fullNameField: { lastName: { eq: 'Doe' } } }, + restFilterInput: 'fullNameField.lastName[eq]:"Doe"', + validateFilter: (record: Record) => { + return record.fullNameField.lastName === 'Doe'; + }, + }, + ], + [FieldMetadataType.RATING]: [ + { + gqlFilterInput: { ratingField: { eq: 'RATING_5' } }, + restFilterInput: 'ratingField[eq]:"RATING_5"', + validateFilter: (record: Record) => { + return record.ratingField === 'RATING_5'; + }, + }, + { + gqlFilterInput: { ratingField: { neq: 'RATING_3' } }, + restFilterInput: 'ratingField[neq]:"RATING_3"', + validateFilter: (record: Record) => { + return record.ratingField !== 'RATING_3'; + }, + }, + { + gqlFilterInput: { ratingField: { is: 'NULL' } }, + restFilterInput: 'ratingField[is]:NULL', + validateFilter: (record: Record) => { + return record.ratingField === null; + }, + }, + { + gqlFilterInput: { ratingField: { in: ['RATING_5', 'RATING_4'] } }, + restFilterInput: 'ratingField[in]:["RATING_5","RATING_4"]', + validateFilter: (record: Record) => { + return ['RATING_5', 'RATING_4'].includes(record.ratingField); + }, + }, + { + gqlFilterInput: { ratingField: { is: 'NOT_NULL' } }, + restFilterInput: 'ratingField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.ratingField); + }, + }, + // TODO - fix this, should be ok + // { + // gqlFilterInput: { ratingField: { isEmptyArray: true } }, + // restFilterInput: 'ratingField[isEmptyArray]=true', + // }, + ], + [FieldMetadataType.SELECT]: [ + { + gqlFilterInput: { selectField: { in: ['OPTION_1'] } }, + restFilterInput: 'selectField[in]:[OPTION_1]', + validateFilter: (record: Record) => { + return ['OPTION_1'].includes(record.selectField); + }, + }, + { + gqlFilterInput: { selectField: { is: 'NULL' } }, + restFilterInput: 'selectField[is]:NULL', + validateFilter: (record: Record) => { + return record.selectField === null; + }, + }, + { + gqlFilterInput: { selectField: { is: 'NOT_NULL' } }, + restFilterInput: 'selectField[is]:NOT_NULL', + validateFilter: (record: Record) => { + return isDefined(record.selectField); + }, + }, + ], + [FieldMetadataType.MULTI_SELECT]: [ + { + gqlFilterInput: { multiSelectField: { containsAny: ['OPTION_1'] } }, + restFilterInput: 'multiSelectField[containsAny]:[OPTION_1]', + validateFilter: (record: Record) => { + return record.multiSelectField.includes('OPTION_1'); + }, + }, + { + gqlFilterInput: { multiSelectField: { is: 'NULL' } }, + restFilterInput: 'multiSelectField[is]:NULL', + validateFilter: (record: Record) => { + return record.multiSelectField === null; + }, + }, + { + gqlFilterInput: { multiSelectField: { is: 'NOT_NULL' } }, + restFilterInput: 'multiSelectField[is]:NOT_NULL', + validateFilter: (record: Record) => { + return isDefined(record.multiSelectField); + }, + }, + //TODO - null and empty array should be equivalent + // { + // gqlFilterInput: { multiSelectField: { isEmptyArray: true } }, + // restFilterInput: 'multiSelectField[isEmptyArray]=true', + // validateFilter: (record: Record) => { + // return record.multiSelectField.length === 0; + // }, + // }, + ], + [FieldMetadataType.ADDRESS]: [ + { + gqlFilterInput: { + addressField: { addressStreet1: { eq: 'address street 1' } }, + }, + restFilterInput: 'addressField.addressStreet1[eq]:"address street 1"', + validateFilter: (record: Record) => { + return record.addressField.addressStreet1 === 'address street 1'; + }, + }, + { + gqlFilterInput: { + addressField: { addressStreet2: { eq: 'address street 2' } }, + }, + restFilterInput: 'addressField.addressStreet2[eq]:"address street 2"', + validateFilter: (record: Record) => { + return record.addressField.addressStreet2 === 'address street 2'; + }, + }, + { + gqlFilterInput: { + addressField: { addressStreet2: { is: 'NOT_NULL' } }, + }, + restFilterInput: 'addressField.addressStreet2[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.addressField.addressStreet2); + }, + }, + { + gqlFilterInput: { + addressField: { addressCity: { eq: 'address city' } }, + }, + restFilterInput: 'addressField.addressCity[eq]:"address city"', + validateFilter: (record: Record) => { + return record.addressField.addressCity === 'address city'; + }, + }, + { + gqlFilterInput: { + addressField: { addressState: { eq: 'address state' } }, + }, + restFilterInput: 'addressField.addressState[eq]:"address state"', + validateFilter: (record: Record) => { + return record.addressField.addressState === 'address state'; + }, + }, + { + gqlFilterInput: { + addressField: { addressCountry: { eq: 'address country' } }, + }, + restFilterInput: 'addressField.addressCountry[eq]:"address country"', + validateFilter: (record: Record) => { + return record.addressField.addressCountry === 'address country'; + }, + }, + { + gqlFilterInput: { + addressField: { addressPostcode: { eq: 'address postcode' } }, + }, + restFilterInput: 'addressField.addressPostcode[eq]:"address postcode"', + validateFilter: (record: Record) => { + return record.addressField.addressPostcode === 'address postcode'; + }, + }, + ], + [FieldMetadataType.RAW_JSON]: [ + { + gqlFilterInput: { rawJsonField: { is: 'NULL' } }, + restFilterInput: 'rawJsonField[is]:NULL', + validateFilter: (record: Record) => { + return record.rawJsonField === null; + }, + }, + { + gqlFilterInput: { rawJsonField: { is: 'NOT_NULL' } }, + restFilterInput: 'rawJsonField[is]:"NOT_NULL"', + validateFilter: (record: Record) => { + return isDefined(record.rawJsonField); + }, + }, + { + gqlFilterInput: { rawJsonField: { like: '%test%' } }, + restFilterInput: 'rawJsonField[like]:"%test%"', + validateFilter: (record: Record) => { + return JSON.stringify(record.rawJsonField).includes('test'); + }, + }, + ], + [FieldMetadataType.ARRAY]: [ + // { + // gqlFilterInput: { arrayField: { containsIlike: 'test' } }, + // // TODO - fix this ? not existing for rest + // // restFilterInput: 'arrayField[containsIlike]:"test"', + // validateFilter: (record: Record) => { + // return JSON.stringify(record.arrayField).includes('test'); + // }, + // }, + { + gqlFilterInput: { arrayField: { is: 'NULL' } }, + restFilterInput: 'arrayField[is]:NULL', + validateFilter: (record: Record) => { + return record.arrayField === null; + }, + }, + //TODO - null and empty array should be equivalent + // { + // gqlFilterInput: { arrayField: { isEmptyArray: true } }, + // restFilterInput: 'arrayField[isEmptyArray]=true', + // validateFilter: (record: Record) => { + // return record.arrayField.length === 0; + // }, + // }, + ], +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/test-object-gql-fields.constant.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/test-object-gql-fields.constant.ts new file mode 100644 index 0000000000..843ce83a28 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/constants/test-object-gql-fields.constant.ts @@ -0,0 +1,49 @@ +export const TEST_OBJECT_GQL_FIELDS = ` + id + manyToOneRelationFieldId + manyToOneRelationField { + id + } + uuidField + textField + phonesField { + primaryPhoneNumber + primaryPhoneCallingCode + primaryPhoneCountryCode + additionalPhones + } + emailsField { + primaryEmail + additionalEmails + } + dateTimeField + dateField + booleanField + numberField + linksField { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + currencyField { + amountMicros + currencyCode + } + fullNameField { + firstName + lastName + } + ratingField + selectField + multiSelectField + addressField { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + } + rawJsonField + arrayField +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/currency-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/currency-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..bdbd5f85a9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/currency-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,70 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.CURRENCY; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..4688d6e704 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.DATE; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..4dad9612c7 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/date-time-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.DATE_TIME; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/emails-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/emails-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..af9d0e5174 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/emails-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,70 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.EMAILS; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/full-name-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/full-name-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..734c62f2fa --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/full-name-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,70 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.FULL_NAME; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/links-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/links-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..1cf2769183 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/links-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,70 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.LINKS; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..176a52d0bc --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/multi-select-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.MULTI_SELECT; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..3f6da632b4 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/number-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,110 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.NUMBER; +// const failingTestCases = +// failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + // describe('Gql filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + // await testGqlFailingScenario( + // objectMetadataSingularName, + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + // // TODO : Refacto-common - Uncomment this + // describe('Rest filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.restFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + // await testRestFailingScenario( + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/phones-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/phones-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..7fbd3a73b4 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/phones-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,70 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.PHONES; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..11747a12be --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/rating-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.RATING; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..959b513b46 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/raw-json-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,110 @@ +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.RAW_JSON; +// const failingTestCases = +// failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + // describe('Gql filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + // await testGqlFailingScenario( + // objectMetadataSingularName, + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + // // TODO : Refacto-common - Uncomment this + // describe('Rest filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.restFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + // await testRestFailingScenario( + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..d6cc72e996 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/relation-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.RELATION; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..4b6bed57a3 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/select-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,112 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.SELECT; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + // describe('Rest filterInput - failure', () => { + // it.each( + // failingTestCases.map((testCase) => ({ + // ...testCase, + // stringifiedFilter: JSON.stringify(testCase.restFilterInput), + // })), + // )( + // `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + // async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + // await testRestFailingScenario( + // objectMetadataPluralName, + // filter, + // errorMessage, + // ); + // }, + // ); + // }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..f9f8bd72ef --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/text-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.TEXT; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util.ts new file mode 100644 index 0000000000..ae99e45dcd --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util.ts @@ -0,0 +1,21 @@ +import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util'; +import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util'; + +export const testGqlFailingScenario = async ( + objectMetadataSingularName: string, + objectMetadataPluralName: string, + filter: any, + errorMessage: string, +) => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: objectMetadataSingularName, + objectMetadataPluralName: objectMetadataPluralName, + gqlFields: 'id', + filter, + }); + + const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation); + + expect(response.body.errors).toBeDefined(); + expect(response.body.errors[0].message).toContain(errorMessage); +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util.ts new file mode 100644 index 0000000000..016ecc5914 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util.ts @@ -0,0 +1,30 @@ +import { TEST_OBJECT_GQL_FIELDS } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/test-object-gql-fields.constant'; +import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util'; +import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util'; + +export const testGqlSuccessfulScenario = async ( + objectMetadataSingularName: string, + objectMetadataPluralName: string, + filter: any, + validateFilter: (record: Record) => boolean, +) => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: objectMetadataSingularName, + objectMetadataPluralName: objectMetadataPluralName, + gqlFields: TEST_OBJECT_GQL_FIELDS, + filter, + }); + + const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation); + + const records: Record[] = response.body.data[ + objectMetadataPluralName + ].edges.map((edge: any) => edge.node); + + expect(response.body.errors).toBeUndefined(); + + expect(records.length).toBeGreaterThan(0); + expect( + records.every((record: Record) => validateFilter?.(record)), + ).toBe(true); +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util.ts new file mode 100644 index 0000000000..d4ebd64ebc --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util.ts @@ -0,0 +1,16 @@ +import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; + +export const testRestFailingScenario = async ( + objectMetadataPluralName: string, + filter: any, + errorMessage: string, +) => { + const encodedFilter = encodeURIComponent(filter); + const response = await makeRestAPIRequest({ + method: 'get', + path: `/${objectMetadataPluralName}?filter=${encodedFilter}`, + }); + + expect(response.body.error).toBeDefined(); + expect(JSON.stringify(response.body.messages)).toContain(errorMessage); +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util.ts new file mode 100644 index 0000000000..7baf8d404b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util.ts @@ -0,0 +1,23 @@ +import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; + +export const testRestSuccessfulScenario = async ( + objectMetadataPluralName: string, + filter: any, + validateFilter: (record: Record) => boolean, +) => { + const encodedFilter = encodeURIComponent(filter); + const response = await makeRestAPIRequest({ + method: 'get', + path: `/${objectMetadataPluralName}?filter=${encodedFilter}`, + }); + + const records = response.body.data[objectMetadataPluralName]; + + expect(response.body.errors).toBeUndefined(); + + expect(records.length).toBeGreaterThan(0); + + expect( + records.every((record: Record) => validateFilter?.(record)), + ).toBe(true); +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts new file mode 100644 index 0000000000..8c012a467d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/filter-validation/uuid-field-filter-input-validation.integration-spec.ts @@ -0,0 +1,113 @@ +import { failingFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant'; +import { successfulFilterInputByFieldMetadataType } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant'; +import { testGqlFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-failing-scenario.util'; +import { testGqlSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-gql-successful-scenario.util'; +import { testRestFailingScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-failing-scenario.util'; +import { testRestSuccessfulScenario } from 'test/integration/graphql/suites/args-validation/filter-validation/utils/test-rest-successful-scenario.util'; +import { destroyManyObjectsMetadata } from 'test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata'; +import { setupTestObjectsWithAllFieldTypes } from 'test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const FIELD_METADATA_TYPE = FieldMetadataType.UUID; +const failingTestCases = + failingFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; +const successfulTestCases = + successfulFilterInputByFieldMetadataType[FIELD_METADATA_TYPE]; + +describe(`Filter args validation - ${FIELD_METADATA_TYPE}`, () => { + let objectMetadataId: string; + let objectMetadataSingularName: string; + let objectMetadataPluralName: string; + let targetObjectMetadataId: string; + + beforeAll(async () => { + const setupTest = await setupTestObjectsWithAllFieldTypes(); + + objectMetadataId = setupTest.objectMetadataId; + objectMetadataSingularName = setupTest.objectMetadataSingularName; + objectMetadataPluralName = setupTest.objectMetadataPluralName; + targetObjectMetadataId = setupTest.targetObjectMetadataId; + }); + + afterAll(async () => { + await destroyManyObjectsMetadata([ + objectMetadataId, + targetObjectMetadataId, + ]); + }); + + describe('Gql filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, gqlErrorMessage: errorMessage }) => { + await testGqlFailingScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + // TODO : Refacto-common - Uncomment this + describe('Rest filterInput - failure', () => { + it.each( + failingTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should fail with filter : $stringifiedFilter`, + async ({ restFilterInput: filter, restErrorMessage: errorMessage }) => { + await testRestFailingScenario( + objectMetadataPluralName, + filter, + errorMessage, + ); + }, + ); + }); + + describe('Gql filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.gqlFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ gqlFilterInput: filter, validateFilter }) => { + await testGqlSuccessfulScenario( + objectMetadataSingularName, + objectMetadataPluralName, + filter, + validateFilter, + ); + }, + ); + }); + + describe('Rest filterInput - success', () => { + it.each( + successfulTestCases.map((testCase) => ({ + ...testCase, + stringifiedFilter: JSON.stringify(testCase.restFilterInput), + })), + )( + `${FIELD_METADATA_TYPE} field type - should succeed with filter : $stringifiedFilter`, + async ({ restFilterInput, validateFilter }) => { + await testRestSuccessfulScenario( + objectMetadataPluralName, + restFilterInput, + validateFilter, + ); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test.ts new file mode 100644 index 0000000000..a21d1a31a6 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test.ts @@ -0,0 +1,15 @@ +import { type FieldMetadataType } from 'twenty-shared/types'; + +type FieldMetadataTypesNotTested = + | 'TS_VECTOR' + | 'RICH_TEXT' + | 'POSITION' + | 'ACTOR' + | 'MORPH_RELATION' + | 'NUMERIC' + | 'RICH_TEXT_V2'; + +export type FieldMetadataTypeToTest = Exclude< + FieldMetadataType, + FieldMetadataTypesNotTested +>; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata.ts new file mode 100644 index 0000000000..f050c7f620 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/destroy-many-objects-metadata.ts @@ -0,0 +1,22 @@ +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; + +export const destroyManyObjectsMetadata = async ( + objectMetadataIds: string[], +) => { + for (const objectMetadataId of objectMetadataIds) { + await updateOneObjectMetadata({ + input: { + idToUpdate: objectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + input: { + idToDelete: objectMetadataId, + }, + }); + } +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/get-field-metadata-creation-inputs.util.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/get-field-metadata-creation-inputs.util.ts new file mode 100644 index 0000000000..148c05f5ab --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/get-field-metadata-creation-inputs.util.ts @@ -0,0 +1,178 @@ +import { type FieldMetadataTypeToTest } from 'test/integration/graphql/suites/args-validation/types/field-metadata-type-to-test'; +import { + FieldMetadataType, + RelationType, + type RelationCreationPayload, +} from 'twenty-shared/types'; + +import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; + +type FieldMetadataCreationInput = { + name: string; + label: string; + type: FieldMetadataType; + objectMetadataId: string; + options?: FieldMetadataComplexOption[]; + relationCreationPayload?: RelationCreationPayload; +}; + +export const getFieldMetadataCreationInputs = ( + objectMetadataId: string, + targetObjectMetadataId: string, +) => { + const fieldInputsMap: { + [K in FieldMetadataTypeToTest]: + | FieldMetadataCreationInput + | FieldMetadataCreationInput[]; + } = { + [FieldMetadataType.TEXT]: { + name: 'textField', + label: 'textField', + type: FieldMetadataType.TEXT, + objectMetadataId, + }, + [FieldMetadataType.PHONES]: { + name: 'phonesField', + label: 'phonesField', + type: FieldMetadataType.PHONES, + objectMetadataId, + }, + [FieldMetadataType.EMAILS]: { + name: 'emailsField', + label: 'emailsField', + type: FieldMetadataType.EMAILS, + objectMetadataId, + }, + [FieldMetadataType.DATE_TIME]: { + name: 'dateTimeField', + label: 'dateTimeField', + type: FieldMetadataType.DATE_TIME, + objectMetadataId, + }, + [FieldMetadataType.DATE]: { + name: 'dateField', + label: 'dateField', + type: FieldMetadataType.DATE, + objectMetadataId, + }, + [FieldMetadataType.BOOLEAN]: { + name: 'booleanField', + label: 'booleanField', + type: FieldMetadataType.BOOLEAN, + objectMetadataId, + }, + [FieldMetadataType.NUMBER]: { + name: 'numberField', + label: 'numberField', + type: FieldMetadataType.NUMBER, + objectMetadataId, + }, + [FieldMetadataType.LINKS]: { + name: 'linksField', + label: 'linksField', + type: FieldMetadataType.LINKS, + objectMetadataId, + }, + [FieldMetadataType.CURRENCY]: { + name: 'currencyField', + label: 'currencyField', + type: FieldMetadataType.CURRENCY, + objectMetadataId, + }, + [FieldMetadataType.FULL_NAME]: { + name: 'fullNameField', + label: 'fullNameField', + type: FieldMetadataType.FULL_NAME, + objectMetadataId, + }, + [FieldMetadataType.RATING]: { + name: 'ratingField', + label: 'ratingField', + type: FieldMetadataType.RATING, + objectMetadataId, + }, + [FieldMetadataType.SELECT]: { + name: 'selectField', + label: 'selectField', + type: FieldMetadataType.SELECT, + options: [ + { + label: 'Option 1', + value: 'OPTION_1', + color: 'green', + position: 1, + id: '26c602c3-cba9-4d83-92d4-4ba7dbae2f31', + }, + ], + objectMetadataId, + }, + [FieldMetadataType.MULTI_SELECT]: { + name: 'multiSelectField', + label: 'multiSelectField', + type: FieldMetadataType.MULTI_SELECT, + options: [ + { + label: 'Option 1', + value: 'OPTION_1', + color: 'green', + position: 1, + id: '26c602c3-cba9-4d83-92d4-4ba7dbae2f31', + }, + ], + objectMetadataId, + }, + + [FieldMetadataType.ADDRESS]: { + name: 'addressField', + label: 'addressField', + type: FieldMetadataType.ADDRESS, + objectMetadataId, + }, + [FieldMetadataType.RAW_JSON]: { + name: 'rawJsonField', + label: 'rawJsonField', + type: FieldMetadataType.RAW_JSON, + objectMetadataId, + }, + [FieldMetadataType.ARRAY]: { + name: 'arrayField', + label: 'arrayField', + type: FieldMetadataType.ARRAY, + objectMetadataId, + }, + [FieldMetadataType.UUID]: { + name: 'uuidField', + label: 'uuidField', + type: FieldMetadataType.UUID, + objectMetadataId, + }, + [FieldMetadataType.RELATION]: [ + { + name: 'manyToOneRelationField', + label: 'manyToOneRelationField', + type: FieldMetadataType.RELATION, + objectMetadataId, + relationCreationPayload: { + targetObjectMetadataId: targetObjectMetadataId, + targetFieldLabel: 'oneToManyTargetRelationField', + targetFieldIcon: 'IconListOpportunity', + type: RelationType.MANY_TO_ONE, + }, + }, + { + name: 'oneToManyRelationField', + label: 'oneToManyRelationField', + type: FieldMetadataType.RELATION, + objectMetadataId, + relationCreationPayload: { + targetObjectMetadataId: targetObjectMetadataId, + targetFieldLabel: 'manyToOneTargetRelationField', + targetFieldIcon: 'IconListOpportunity', + type: RelationType.ONE_TO_MANY, + }, + }, + ], + }; + + return Object.values(fieldInputsMap).flat(); +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util.ts b/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util.ts new file mode 100644 index 0000000000..5392f9de1b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/args-validation/utils/setup-test-objects-with-all-field-types.util.ts @@ -0,0 +1,148 @@ +import { randomUUID } from 'crypto'; + +import { TEST_OBJECT_GQL_FIELDS } from 'test/integration/graphql/suites/args-validation/filter-validation/constants/test-object-gql-fields.constant'; +import { getFieldMetadataCreationInputs } from 'test/integration/graphql/suites/args-validation/utils/get-field-metadata-creation-inputs.util'; +import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; + +const TEST_OBJECT_METADATA_NAME_SINGULAR = 'apiFilterValidationTestObject'; +const TEST_OBJECT_METADATA_NAME_PLURAL = 'apiFilterValidationTestObjects'; +const TEST_TARGET_OBJECT_METADATA_NAME_SINGULAR = + 'apiFilterValidationTargetTestObject'; +const TEST_TARGET_OBJECT_METADATA_NAME_PLURAL = + 'apiFilterValidationTargetTestObjects'; + +export const TEST_TARGET_OBJECT_RECORD_ID = + '20202020-a21e-4ec2-873b-de4264d89025'; + +export const TEST_UUID_FIELD_VALUE = '20202020-b21e-4ec2-873b-de4264d89025'; + +export const setupTestObjectsWithAllFieldTypes = async () => { + const createdObjectMetadata = await createOneObjectMetadata({ + input: { + nameSingular: TEST_OBJECT_METADATA_NAME_SINGULAR, + namePlural: TEST_OBJECT_METADATA_NAME_PLURAL, + labelSingular: TEST_OBJECT_METADATA_NAME_SINGULAR, + labelPlural: TEST_OBJECT_METADATA_NAME_PLURAL, + }, + }); + + const objectMetadataId = createdObjectMetadata.data.createOneObject.id; + + const createdTargetObjectMetadata = await createOneObjectMetadata({ + input: { + nameSingular: TEST_TARGET_OBJECT_METADATA_NAME_SINGULAR, + namePlural: TEST_TARGET_OBJECT_METADATA_NAME_PLURAL, + labelSingular: TEST_TARGET_OBJECT_METADATA_NAME_SINGULAR, + labelPlural: TEST_TARGET_OBJECT_METADATA_NAME_PLURAL, + }, + }); + + const targetObjectMetadataId = + createdTargetObjectMetadata.data.createOneObject.id; + + const fieldMetadataCreationInputs = getFieldMetadataCreationInputs( + objectMetadataId, + targetObjectMetadataId, + ); + + for (const input of fieldMetadataCreationInputs) { + await createOneFieldMetadata({ + input, + }); + } + + await makeGraphqlAPIRequest( + createManyOperationFactory({ + objectMetadataSingularName: TEST_TARGET_OBJECT_METADATA_NAME_SINGULAR, + objectMetadataPluralName: TEST_TARGET_OBJECT_METADATA_NAME_PLURAL, + gqlFields: `id`, + data: [ + { + id: TEST_TARGET_OBJECT_RECORD_ID, + }, + ], + }), + ); + + await makeGraphqlAPIRequest( + createManyOperationFactory({ + objectMetadataSingularName: TEST_OBJECT_METADATA_NAME_SINGULAR, + objectMetadataPluralName: TEST_OBJECT_METADATA_NAME_PLURAL, + gqlFields: TEST_OBJECT_GQL_FIELDS, + data: [ + { + id: randomUUID(), + manyToOneRelationFieldId: TEST_TARGET_OBJECT_RECORD_ID, + uuidField: TEST_UUID_FIELD_VALUE, + textField: 'test', + phonesField: { + primaryPhoneNumber: '01234567890', + primaryPhoneCallingCode: '+33', + primaryPhoneCountryCode: 'FR', + additionalPhones: [ + { + number: '01234567890', + callingCode: '+33', + countryCode: 'FR', + }, + ], + }, + emailsField: { + primaryEmail: 'test@test.com', + additionalEmails: ['test@test.com'], + }, + dateTimeField: '2025-01-01T00:00:00Z', + dateField: '2025-01-01', + booleanField: true, + numberField: 1, + linksField: { + primaryLinkUrl: 'twenty.com', + primaryLinkLabel: 'twenty - #1 Open source CRM', + secondaryLinks: [ + { + url: 'twenty.com', + label: 'twenty - #1 Open source CRM', + }, + ], + }, + currencyField: { + amountMicros: 1000000, + currencyCode: 'USD', + }, + fullNameField: { + firstName: 'John', + lastName: 'Doe', + }, + ratingField: 'RATING_5', + selectField: 'OPTION_1', + multiSelectField: ['OPTION_1'], + addressField: { + addressStreet1: 'address street 1', + addressStreet2: 'address street 2', + addressCity: 'address city', + addressState: 'address state', + addressCountry: 'address country', + addressPostcode: 'address postcode', + }, + rawJsonField: { + test: 'test', + }, + arrayField: ['test'], + }, + { + id: randomUUID(), + }, + ], + }), + ); + + return { + objectMetadataId, + targetObjectMetadataId, + objectMetadataSingularName: TEST_OBJECT_METADATA_NAME_SINGULAR, + objectMetadataPluralName: TEST_OBJECT_METADATA_NAME_PLURAL, + }; +};