diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index c6d69fd2c..d42c0aca5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -4,7 +4,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { @@ -175,7 +174,7 @@ export class ProcessNestedRelationsV2Helper { targetRelation, FieldMetadataType.MORPH_RELATION, ) - ? `${(targetRelation?.settings as FieldMetadataRelationSettings)?.joinColumnName}` + ? `${targetRelation.settings?.joinColumnName}` : `${targetRelationName}Id`; const { relationResults, relationAggregatedFieldsResult } = diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 446cdae95..ba5e646f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -5,7 +5,13 @@ import { t } from '@lingui/core/macro'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { DataSource, FindOneOptions, In, Repository } from 'typeorm'; +import { + DataSource, + FindOneOptions, + In, + QueryRunner, + Repository, +} from 'typeorm'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -24,6 +30,7 @@ import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/f import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service'; +import { areFieldMetadatasTypeRelationOrMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util'; import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util'; import { buildUpdatableStandardFieldInput } from 'src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util'; import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw'; @@ -35,6 +42,8 @@ import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/ import { createMigrationActions } from 'src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util'; import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; +import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; @@ -59,6 +68,14 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { ViewService } from 'src/modules/view/services/view.service'; +type GenerateMigrationArgs = { + fieldMetadata: FieldMetadataEntity< + FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION + >; + workspaceId: string; + queryRunner: QueryRunner; +}; + @Injectable() export class FieldMetadataService extends TypeOrmQueryService { constructor( @@ -352,48 +369,85 @@ export class FieldMetadataService extends TypeOrmQueryService) - .settings?.relationType === RelationType.MANY_TO_ONE; + if (isFieldMetadataTypeRelation(fieldMetadata)) { + const fieldMetadataIdsToDelete: string[] = []; + const isRelationTargetMorphRelation = isFieldMetadataTypeMorphRelation( + fieldMetadata.relationTargetFieldMetadata, + ); - if (!isDefined(fieldMetadata.relationTargetFieldMetadata)) { - throw new FieldMetadataException( - 'Target field metadata does not exist', - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } + if (isRelationTargetMorphRelation) { + const morphRelationsWithSameName = + await this.getMorphRelationsWithSameName({ + fieldMetadataName: fieldMetadata.relationTargetFieldMetadata.name, + objectMetadataId: + fieldMetadata.relationTargetFieldMetadata.objectMetadataId, + workspaceId, + fieldMetadataRepository, + }); - await fieldMetadataRepository.delete({ - id: In([ + morphRelationsWithSameName.forEach((morphRelation) => { + fieldMetadataIdsToDelete.push( + morphRelation.id, + morphRelation.relationTargetFieldMetadataId, + ); + }); + + await fieldMetadataRepository.delete({ + id: In(fieldMetadataIdsToDelete), + }); + + for (const morphRelation of morphRelationsWithSameName) { + await this.generateDeleteRelationMigration({ + fieldMetadata: morphRelation, + workspaceId, + queryRunner, + }); + } + } else { + fieldMetadataIdsToDelete.push( fieldMetadata.id, - fieldMetadata.relationTargetFieldMetadata.id, - ]), + fieldMetadata.relationTargetFieldMetadataId, + ); + + await fieldMetadataRepository.delete({ + id: In(fieldMetadataIdsToDelete), + }); + + await this.generateDeleteRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }); + } + } else if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { + const fieldMetadataIdsToDelete: string[] = []; + + const morphRelationsWithSameName = + await this.getMorphRelationsWithSameName({ + fieldMetadataName: fieldMetadata.name, + objectMetadataId: fieldMetadata.objectMetadataId, + workspaceId, + fieldMetadataRepository, + }); + + morphRelationsWithSameName.forEach((morphRelation) => { + fieldMetadataIdsToDelete.push( + morphRelation.id, + morphRelation.relationTargetFieldMetadataId, + ); }); - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`delete-${fieldMetadata.name}`), - workspaceId, - [ - { - name: isManyToOneRelation - ? computeObjectTargetTable(fieldMetadata.object) - : computeObjectTargetTable( - fieldMetadata.relationTargetObjectMetadata as ObjectMetadataEntity, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: isManyToOneRelation - ? `${(fieldMetadata as FieldMetadataEntity).settings?.joinColumnName}` - : `${(fieldMetadata.relationTargetFieldMetadata as FieldMetadataEntity).settings?.joinColumnName}`, - } satisfies WorkspaceMigrationColumnDrop, - ], - } satisfies WorkspaceMigrationTableAction, - ], - queryRunner, - ); + await fieldMetadataRepository.delete({ + id: In(fieldMetadataIdsToDelete), + }); + + for (const morphRelation of morphRelationsWithSameName) { + await this.generateDeleteRelationMigration({ + fieldMetadata: morphRelation, + workspaceId, + queryRunner, + }); + } } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { await fieldMetadataRepository.delete(fieldMetadata.id); const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); @@ -710,4 +764,126 @@ export class FieldMetadataService extends TypeOrmQueryService; + }): Promise< + FieldMetadataEntity< + FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION + >[] + > { + const fieldMetadatas = await fieldMetadataRepository.find({ + where: { + name: fieldMetadataName, + objectMetadataId, + workspaceId, + }, + relations: [ + 'object', + 'relationTargetFieldMetadata', + 'relationTargetObjectMetadata', + ], + }); + + if (!areFieldMetadatasTypeRelationOrMorphRelation(fieldMetadatas)) { + throw new FieldMetadataException( + 'At least one field metadata is not a relation or morph relation', + FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + return fieldMetadatas; + } + + private async generateDeleteRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }: GenerateMigrationArgs) { + if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) { + await this.generateDeleteOneToManyRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }); + } else { + await this.generateDeleteManyToOneRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }); + } + } + + private async generateDeleteManyToOneRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }: GenerateMigrationArgs) { + if (fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE) { + throw new FieldMetadataException( + 'Field metadata is not a many to one relation', + FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`delete-${fieldMetadata.name}`), + workspaceId, + [ + { + name: computeObjectTargetTable(fieldMetadata.object), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP, + columnName: `${fieldMetadata.settings?.joinColumnName}`, + } satisfies WorkspaceMigrationColumnDrop, + ], + } satisfies WorkspaceMigrationTableAction, + ], + queryRunner, + ); + } + + private async generateDeleteOneToManyRelationMigration({ + fieldMetadata, + workspaceId, + queryRunner, + }: GenerateMigrationArgs) { + if (fieldMetadata.settings?.relationType !== RelationType.ONE_TO_MANY) { + throw new FieldMetadataException( + 'Field metadata is not a one to many relation', + FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`delete-${fieldMetadata.name}`), + workspaceId, + [ + { + name: computeObjectTargetTable( + fieldMetadata.relationTargetObjectMetadata as ObjectMetadataEntity, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.DROP, + columnName: `${(fieldMetadata.relationTargetFieldMetadata as FieldMetadataEntity).settings?.joinColumnName}`, + } satisfies WorkspaceMigrationColumnDrop, + ], + } satisfies WorkspaceMigrationTableAction, + ], + queryRunner, + ); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts new file mode 100644 index 000000000..c3a333801 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-type-relation-or-morph-relation.util.ts @@ -0,0 +1,20 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; +import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; + +export const areFieldMetadatasTypeRelationOrMorphRelation = ( + fieldMetadatas: FieldMetadataEntity[], +): fieldMetadatas is Array< + FieldMetadataEntity & + FieldMetadataEntity< + FieldMetadataType.MORPH_RELATION | FieldMetadataType.RELATION + > +> => { + return fieldMetadatas.every( + (fieldMetadata) => + isFieldMetadataTypeRelation(fieldMetadata) || + isFieldMetadataTypeMorphRelation(fieldMetadata), + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util.ts new file mode 100644 index 000000000..a99d91cbc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util.ts @@ -0,0 +1,10 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +export const isFieldMetadataTypeMorphRelation = ( + fieldMetadata: FieldMetadataEntity, +): fieldMetadata is FieldMetadataEntity & + FieldMetadataEntity => { + return fieldMetadata.type === FieldMetadataType.MORPH_RELATION; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util.ts new file mode 100644 index 000000000..280e46b0f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util.ts @@ -0,0 +1,10 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +export const isFieldMetadataTypeRelation = ( + fieldMetadata: FieldMetadataEntity, +): fieldMetadata is FieldMetadataEntity & + FieldMetadataEntity => { + return fieldMetadata.type === FieldMetadataType.RELATION; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts index 8c7896f38..6d7b1ffb4 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/create-one-field-metadata-morph-relation.integration-spec.ts @@ -138,11 +138,12 @@ describe('createOne FieldMetadataService morph relation fields', () => { expect(createdField.id).toBeDefined(); expect(createdField.name).toBe('owner'); - // expect(createdField.relation).toBeUndefined(); - // expect(createdField.morphRelations[0].type).toBe( - // contextPayload.relationType, - // ); - // expect(createdField.morphRelations[0].targetFieldMetadata.id).toBeDefined(); + expect(createdField.morphRelations[0].targetObjectMetadata.id).toBe( + contextPayload.firstTargetObjectMetadataId, + ); + expect(createdField.morphRelations[1].targetObjectMetadata.id).toBe( + contextPayload.secondTargetObjectMetadataId, + ); const isManyToOne = contextPayload.relationType === RelationType.MANY_TO_ONE; @@ -155,8 +156,6 @@ describe('createOne FieldMetadataService morph relation fields', () => { expect(createdField.settings?.joinColumnName).toBeUndefined(); } - // TODO: check the morphrelation targets are created correctly (wait for Query Morph Relations) - await deleteOneFieldMetadata({ input: { idToDelete: createdField.id }, }).catch(); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/delete-one-field-metadata-morph-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/delete-one-field-metadata-morph-relation.integration-spec.ts new file mode 100644 index 000000000..13983c5b8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/delete-one-field-metadata-morph-relation.integration-spec.ts @@ -0,0 +1,236 @@ +import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { EachTestingContext } from 'twenty-shared/testing'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +describe('deleteOne FieldMetadataService morph relation fields', () => { + let createdObjectMetadataPersonId = ''; + let createdObjectMetadataOpportunityId = ''; + let createdObjectMetadataCompanyId = ''; + + beforeEach(async () => { + const { + data: { + createOneObject: { id: objectMetadataPersonId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'personForMorphRelation', + namePlural: 'peopleForMorphRelation', + labelSingular: 'Person For Morph Relation', + labelPlural: 'People For Morph Relation', + icon: 'IconPerson', + }, + }); + + createdObjectMetadataPersonId = objectMetadataPersonId; + + const { + data: { + createOneObject: { id: objectMetadataCompanyId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'companyForMorphRelation', + namePlural: 'companiesForMorphRelation', + labelSingular: 'Company For Morph Relation', + labelPlural: 'Companies For Morph Relation', + icon: 'IconCompany', + }, + }); + + createdObjectMetadataCompanyId = objectMetadataCompanyId; + + const { + data: { + createOneObject: { id: objectMetadataOpportunityId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'opportunityForMorphRelation', + namePlural: 'opportunitiesForMorphRelation', + labelSingular: 'Opportunity For Morph Relation', + labelPlural: 'Opportunities For Morph Relation', + icon: 'IconOpportunity', + }, + }); + + createdObjectMetadataOpportunityId = objectMetadataOpportunityId; + }); + afterEach(async () => { + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataPersonId }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataOpportunityId }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataCompanyId }, + }); + }); + + type EachTestingContextArray = EachTestingContext< + (args: { + objectMetadataId: string; + firstTargetObjectMetadataId: string; + secondTargetObjectMetadataId: string; + }) => { + relationType: RelationType; + objectMetadataId: string; + firstTargetObjectMetadataId: string; + secondTargetObjectMetadataId: string; + type: FieldMetadataType; + } + >[]; + + const eachTestingContextArray: EachTestingContextArray = [ + { + title: 'should delete a MORPH_RELATION field type MANY_TO_ONE', + context: ({ + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + }) => ({ + relationType: RelationType.MANY_TO_ONE, + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + type: FieldMetadataType.MORPH_RELATION, + }), + }, + { + title: 'should delete a MORPH_RELATION field type ONE_TO_MANY', + context: ({ + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + }) => ({ + relationType: RelationType.ONE_TO_MANY, + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + type: FieldMetadataType.MORPH_RELATION, + }), + }, + ]; + + const eachTestingContextForRelationWithMorphRelationTargetArray: EachTestingContextArray = + [ + { + title: + 'should delete a ONE_TO_MANY RELATION that has a MORPH_RELATION field as target', + context: ({ + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + }) => ({ + relationType: RelationType.MANY_TO_ONE, + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + type: FieldMetadataType.MORPH_RELATION, + }), + }, + { + title: + 'should delete a MANY_TO_ONE RELATION that has a MORPH_RELATION field as target', + context: ({ + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + }) => ({ + relationType: RelationType.ONE_TO_MANY, + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + type: FieldMetadataType.MORPH_RELATION, + }), + }, + ]; + + it.each(eachTestingContextArray)('$title', async ({ context }) => { + const contextPayload = context({ + objectMetadataId: createdObjectMetadataOpportunityId, + firstTargetObjectMetadataId: createdObjectMetadataPersonId, + secondTargetObjectMetadataId: createdObjectMetadataCompanyId, + }); + + const createdField = await createMorphRelationBetweenObjects({ + objectMetadataId: contextPayload.objectMetadataId, + firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId, + secondTargetObjectMetadataId: contextPayload.secondTargetObjectMetadataId, + type: contextPayload.type, + relationType: contextPayload.relationType, + }); + + const deactivatedField = await updateOneFieldMetadata({ + input: { + idToUpdate: createdField.id, + updatePayload: { isActive: false }, + }, + gqlFields: ` + id + isActive + `, + }); + + expect(deactivatedField.data.updateOneField.id).toBe(createdField.id); + + const { errors } = await deleteOneFieldMetadata({ + input: { idToDelete: createdField.id }, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + }); + + it.each(eachTestingContextForRelationWithMorphRelationTargetArray)( + '$title', + async ({ context }) => { + const contextPayload = context({ + objectMetadataId: createdObjectMetadataOpportunityId, + firstTargetObjectMetadataId: createdObjectMetadataPersonId, + secondTargetObjectMetadataId: createdObjectMetadataCompanyId, + }); + + const createdField = await createMorphRelationBetweenObjects({ + objectMetadataId: contextPayload.objectMetadataId, + firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId, + secondTargetObjectMetadataId: + contextPayload.secondTargetObjectMetadataId, + type: contextPayload.type, + relationType: contextPayload.relationType, + }); + + const targetRelationField = + createdField.morphRelations[0].targetFieldMetadata; + + const deactivatedTargetRelationField = await updateOneFieldMetadata({ + input: { + idToUpdate: targetRelationField.id, + updatePayload: { isActive: false }, + }, + gqlFields: ` + id + isActive + `, + }); + + expect(deactivatedTargetRelationField.data.updateOneField.id).toBe( + targetRelationField.id, + ); + + const { errors } = await deleteOneFieldMetadata({ + input: { idToDelete: targetRelationField.id }, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/delete-one-object-metadata-with-morph-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/delete-one-object-metadata-with-morph-relation.integration-spec.ts index 40b84f44c..7409948bd 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/delete-one-object-metadata-with-morph-relation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/delete-one-object-metadata-with-morph-relation.integration-spec.ts @@ -1,5 +1,110 @@ +import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util'; +import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + describe('Delete Object metadata with morph relation should succeed', () => { - it('should succeed', () => { - // 'TODO guillim : once delete is implemented' + let opportunityId = ''; + let personId = ''; + let companyId = ''; + let morphRelationField: { id: string }; + + beforeEach(async () => { + const { + data: { + createOneObject: { id: aId }, + }, + } = await forceCreateOneObjectMetadata({ + input: { + nameSingular: 'opportunityForDelete', + namePlural: 'opportunitiesForDelete', + labelSingular: 'Opportunity For Delete', + labelPlural: 'Opportunities For Delete', + icon: 'IconOpportunity', + }, + }); + + opportunityId = aId; + const { + data: { + createOneObject: { id: bId }, + }, + } = await forceCreateOneObjectMetadata({ + input: { + nameSingular: 'personForDelete', + namePlural: 'peopleForDelete', + labelSingular: 'Person For Delete', + labelPlural: 'People For Delete', + icon: 'IconPerson', + }, + }); + + personId = bId; + const { + data: { + createOneObject: { id: cId }, + }, + } = await forceCreateOneObjectMetadata({ + input: { + nameSingular: 'companyForDelete', + namePlural: 'companiesForDelete', + labelSingular: 'Company For Delete', + labelPlural: 'Companies For Delete', + icon: 'IconCompany', + }, + }); + + companyId = cId; + }); + + afterEach(async () => { + await deleteOneObjectMetadata({ input: { idToDelete: opportunityId } }); + await deleteOneObjectMetadata({ input: { idToDelete: personId } }); + await deleteOneObjectMetadata({ input: { idToDelete: companyId } }); + }); + + it('When deleting source object, the relation on the target should be deleted', async () => { + morphRelationField = await createMorphRelationBetweenObjects({ + objectMetadataId: opportunityId, + firstTargetObjectMetadataId: personId, + secondTargetObjectMetadataId: companyId, + type: FieldMetadataType.MORPH_RELATION, + relationType: RelationType.MANY_TO_ONE, + }); + + await deleteOneObjectMetadata({ input: { idToDelete: opportunityId } }); + const fieldAfterDeletion = await findFieldMetadata({ + fieldMetadataId: morphRelationField.id, + }); + + expect(fieldAfterDeletion).toBeUndefined(); }); }); + +const findFieldMetadata = async ({ + fieldMetadataId, +}: { + fieldMetadataId: string; +}) => { + const operation = findManyFieldsMetadataQueryFactory({ + gqlFields: ` + id + name + object { id nameSingular } + relation { type targetFieldMetadata { id } targetObjectMetadata { id } } + settings + `, + input: { + filter: { id: { eq: fieldMetadataId } }, + paging: { first: 1 }, + }, + }); + const fields = await makeMetadataAPIRequest(operation); + const field = fields.body.data.fields.edges?.[0]?.node; + + return field; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts index d1dae1455..1893bfaab 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util.ts @@ -4,6 +4,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export const createMorphRelationBetweenObjects = async ({ @@ -51,13 +52,6 @@ export const createMorphRelationBetweenObjects = async ({ ], }; - // TODO: add morphRelations to the query once available - // morphRelations { - // type - // targetFieldMetadata { - // id - // } - // } const { data: { createOneField: createdFieldPerson }, } = await createOneFieldMetadata({ @@ -72,9 +66,20 @@ export const createMorphRelationBetweenObjects = async ({ id nameSingular } + morphRelations { + type + targetFieldMetadata { + id + } + targetObjectMetadata { + id + } + } `, expectToFail: false, }); - return createdFieldPerson as FieldMetadataEntity; + return createdFieldPerson as FieldMetadataEntity & { + morphRelations: RelationDTO[]; + }; };