From 2274a937bd8f63ae22401610ae02240df0c77771 Mon Sep 17 00:00:00 2001 From: Guillim Date: Fri, 7 Nov 2025 14:49:57 +0100 Subject: [PATCH] first step (#15687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TODO : -> for test integreation for this connect ✅ -> add validation in common API Fixes https://github.com/twentyhq/core-team-issues/issues/1278 --- ...ation-field-metadata-gql-type.generator.ts | 6 +- ...ute-relation-connect-query-configs.util.ts | 3 +- .../constants/rocket-data-seeds.constant.ts | 30 ++++ .../data/services/dev-seeder-data.service.ts | 9 + .../constants/test-rocket-ids.constants.ts | 3 + .../test-survey-result-ids.constants.ts | 2 + ...ested-relation-queries.integration-spec.ts | 155 ++++++++++++++++++ .../search-resolver.integration-spec.ts | 1 + 8 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/rocket-data-seeds.constant.ts create mode 100644 packages/twenty-server/test/integration/constants/test-rocket-ids.constants.ts create mode 100644 packages/twenty-server/test/integration/constants/test-survey-result-ids.constants.ts diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts index 7ca02b5848..1fe6b35033 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts @@ -180,11 +180,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { >; typeOptions: TypeOptions; }) { - if ( - fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY || - //TODO : Enable connect on morph relation - @guillim - fieldMetadata.type === FieldMetadataType.MORPH_RELATION - ) { + if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) { return {}; } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts index d2174789d4..1fc5390d0f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts @@ -133,7 +133,8 @@ const computeRecordToConnectCondition = ( objectMetadata.fieldsById[objectMetadata.fieldIdByName[connectFieldName]]; if ( - !isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) || + (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && + !isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) || field.settings?.relationType !== RelationType.MANY_TO_ONE ) { const objectMetadataNameSingular = objectMetadata.nameSingular; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/rocket-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/rocket-data-seeds.constant.ts new file mode 100644 index 0000000000..876483f7d3 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/rocket-data-seeds.constant.ts @@ -0,0 +1,30 @@ +type RocketDataSeed = { + id: string; + name: string; +}; + +export const ROCKET_DATA_SEED_COLUMNS: (keyof RocketDataSeed)[] = [ + 'id', + 'name', +]; + +export const ROCKET_DATA_SEED_IDS = { + ID_1: '20202020-77d2-4000-8ce4-6a70b9720b32', + ID_2: '20202020-ed89-413a-b31a-962986a3546f', + ID_3: '20202020-1f3b-4e2a-9c1b-8d9e0f1a2b3c', +}; + +export const ROCKET_DATA_SEEDS: RocketDataSeed[] = [ + { + id: ROCKET_DATA_SEED_IDS.ID_1, + name: 'Falcon 9', + }, + { + id: ROCKET_DATA_SEED_IDS.ID_2, + name: 'Starship', + }, + { + id: ROCKET_DATA_SEED_IDS.ID_3, + name: 'Falcon Heavy', + }, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts index 6cee4c4434..a66559d30e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts @@ -84,6 +84,10 @@ import { PET_DATA_SEED_COLUMNS, PET_DATA_SEEDS, } from 'src/engine/workspace-manager/dev-seeder/data/constants/pet-data-seeds.constant'; +import { + ROCKET_DATA_SEED_COLUMNS, + ROCKET_DATA_SEEDS, +} from 'src/engine/workspace-manager/dev-seeder/data/constants/rocket-data-seeds.constant'; import { SURVEY_RESULT_DATA_SEED_COLUMNS, SURVEY_RESULT_DATA_SEEDS, @@ -126,6 +130,11 @@ const getRecordSeedsBatches = ( pgColumns: SURVEY_RESULT_DATA_SEED_COLUMNS, recordSeeds: SURVEY_RESULT_DATA_SEEDS, }, + { + tableName: '_rocket', + pgColumns: ROCKET_DATA_SEED_COLUMNS, + recordSeeds: ROCKET_DATA_SEEDS, + }, ]; // Batch 2: Depends on workspaceMember diff --git a/packages/twenty-server/test/integration/constants/test-rocket-ids.constants.ts b/packages/twenty-server/test/integration/constants/test-rocket-ids.constants.ts new file mode 100644 index 0000000000..f33c76dcb2 --- /dev/null +++ b/packages/twenty-server/test/integration/constants/test-rocket-ids.constants.ts @@ -0,0 +1,3 @@ +export const TEST_ROCKET_ID_1 = '20202020-77d2-4000-8ce4-6a70b9720b32'; +export const TEST_ROCKET_ID_2 = '20202020-ed89-413a-b31a-962986a3546f'; +export const TEST_ROCKET_ID_3 = '20202020-1f3b-4e2a-9c1b-8d9e0f1a2b3c'; diff --git a/packages/twenty-server/test/integration/constants/test-survey-result-ids.constants.ts b/packages/twenty-server/test/integration/constants/test-survey-result-ids.constants.ts new file mode 100644 index 0000000000..82a2ec1ab8 --- /dev/null +++ b/packages/twenty-server/test/integration/constants/test-survey-result-ids.constants.ts @@ -0,0 +1,2 @@ +export const TEST_SURVEY_RESULT_1_ID = '20202020-3d5f-4e2a-9c1b-7f8e2d3c4b5a'; +export const TEST_SURVEY_RESULT_2_ID = '20202020-4e83-41ec-93e2-fd70ff09f68c'; diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts index 501bee5cc7..b94d10c06f 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts @@ -6,6 +6,13 @@ import { TEST_PERSON_1_ID, TEST_PERSON_2_ID, } from 'test/integration/constants/test-person-ids.constants'; +import { + TEST_PET_ID_1, + TEST_PET_ID_2, + TEST_PET_ID_3, +} from 'test/integration/constants/test-pet-ids.constants'; +import { TEST_ROCKET_ID_1 } from 'test/integration/constants/test-rocket-ids.constants'; +import { TEST_SURVEY_RESULT_1_ID } from 'test/integration/constants/test-survey-result-ids.constants'; import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util'; @@ -25,6 +32,21 @@ const PERSON_GQL_FIELDS_WITH_COMPANY = ` } `; +const PET_GQL_FIELDS_WITH_OWNER = ` + id + name + ownerSurveyResultId + ownerSurveyResult { + id + name + } + ownerRocketId + ownerRocket { + id + name + } +`; + describe('relation connect in workspace createOne/createMany resolvers (e2e)', () => { const [company1, company2] = [ { id: TEST_COMPANY_1_ID, domainName: { primaryLinkUrl: 'company1.com' } }, @@ -577,4 +599,137 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', expect(updatedPerson.company?.id).toBeUndefined(); expect(insertedPerson.company?.id).toBe(TEST_COMPANY_2_ID); }); + + it('should connect a morph relation ownerSurveyResult on pet via the connect feature', async () => { + const PET_OBJECT_NAME = 'pet'; + const TEST_PET_ID = TEST_PET_ID_1; + + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_PET_ID, + name: 'Test Pet', + }, + }), + ); + + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + recordId: TEST_PET_ID, + gqlFields: PET_GQL_FIELDS_WITH_OWNER, + data: { + ownerSurveyResult: { + connect: { + where: { id: TEST_SURVEY_RESULT_ID }, + }, + }, + }, + }); + + let response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); + + expect(response.body.data.updatePet).toBeDefined(); + expect(response.body.data.updatePet.ownerSurveyResult).toBeDefined(); + expect(response.body.data.updatePet.ownerSurveyResult.id).toBe( + TEST_SURVEY_RESULT_ID, + ); + expect(response.body.data.updatePet.ownerRocketId).toBeFalsy(); + }); + + it('should disconnect a morph relation successfully', async () => { + const PET_OBJECT_NAME = 'pet'; + const TEST_PET_ID = TEST_PET_ID_2; + + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_PET_ID, + name: 'Test Pet 2', + }, + }), + ); + + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + recordId: TEST_PET_ID, + gqlFields: PET_GQL_FIELDS_WITH_OWNER, + data: { + ownerSurveyResult: { + connect: { + where: { id: TEST_SURVEY_RESULT_ID }, + }, + }, + }, + }); + + let response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); + + expect(response.body.data.updatePet.ownerSurveyResult).toBeDefined(); + + const updatePetOwnerSurveyResultDisconnectOp = updateOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + recordId: TEST_PET_ID, + gqlFields: PET_GQL_FIELDS_WITH_OWNER, + data: { + ownerSurveyResult: { + disconnect: true, + }, + }, + }); + + response = await makeGraphqlAPIRequest( + updatePetOwnerSurveyResultDisconnectOp, + ); + expect(response.body.data.updatePet.ownerSurveyResult).toBeFalsy(); + }); + + // TODO: run this test when validations are implemented in commonAPI + xit('should fail to create a morph relation on both target objects', async () => { + const PET_OBJECT_NAME = 'pet'; + const TEST_PET_ID = TEST_PET_ID_3; + const TEST_ROCKET_ID = TEST_ROCKET_ID_1; + + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_PET_ID, + name: 'Test Pet 3', + }, + }), + ); + + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ + objectMetadataSingularName: PET_OBJECT_NAME, + recordId: TEST_PET_ID, + gqlFields: PET_GQL_FIELDS_WITH_OWNER, + data: { + ownerSurveyResult: { + connect: { + where: { id: TEST_SURVEY_RESULT_ID }, + }, + }, + ownerRocket: { + connect: { + where: { id: TEST_ROCKET_ID }, + }, + }, + }, + }); + + let response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); + + expect(response.body.errors).toBeTruthy(); + }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts index 7fa95d2f55..793c972460 100644 --- a/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts @@ -121,6 +121,7 @@ describe('SearchResolver', () => { await deleteAllRecords('dashboard'); await deleteAllRecords('_pet'); await deleteAllRecords('_surveyResult'); + await deleteAllRecords('_rocket'); try { await performCreateManyOperation(