Morph Relations : deleteOneField (#13349)

This PR adapts teh deleteOneField to make sure morph relations can be
deleted, either
- from a relation type, 
- or from a morph relation type (which is the most obvious case).

This PR covers 
- the deletion of fieldMetadata 
- and the migrationof workspace schemas, by using the already existing
definition of relation migrations

This PR implements a new test suite: "Delete Object metadata with morph
relation" and completes the other test suites for FieldMetadata and
ObjectMetadata creation/deletion.

Last, we added a nitpick from @paul I forgot on a previous PR on
process-nested-realtion-v2

Fixes https://github.com/twentyhq/core-team-issues/issues/1197
This commit is contained in:
Guillim
2025-07-24 14:40:14 +02:00
committed by GitHub
parent 15e13b4267
commit 1ea451c8be
9 changed files with 617 additions and 57 deletions

View File

@ -4,7 +4,6 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; 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 { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { import {
@ -175,7 +174,7 @@ export class ProcessNestedRelationsV2Helper {
targetRelation, targetRelation,
FieldMetadataType.MORPH_RELATION, FieldMetadataType.MORPH_RELATION,
) )
? `${(targetRelation?.settings as FieldMetadataRelationSettings)?.joinColumnName}` ? `${targetRelation.settings?.joinColumnName}`
: `${targetRelationName}Id`; : `${targetRelationName}Id`;
const { relationResults, relationAggregatedFieldsResult } = const { relationResults, relationAggregatedFieldsResult } =

View File

@ -5,7 +5,13 @@ import { t } from '@lingui/core/macro';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; 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'; 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 { 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 { 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 { 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 { 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 { 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'; 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 { 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 { 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 { 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 { 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 { 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'; 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 { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { ViewService } from 'src/modules/view/services/view.service'; import { ViewService } from 'src/modules/view/services/view.service';
type GenerateMigrationArgs = {
fieldMetadata: FieldMetadataEntity<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>;
workspaceId: string;
queryRunner: QueryRunner;
};
@Injectable() @Injectable()
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntity> { export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntity> {
constructor( constructor(
@ -352,48 +369,85 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
} }
if (fieldMetadata.type === FieldMetadataType.RELATION) { if (isFieldMetadataTypeRelation(fieldMetadata)) {
const isManyToOneRelation = const fieldMetadataIdsToDelete: string[] = [];
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>) const isRelationTargetMorphRelation = isFieldMetadataTypeMorphRelation(
.settings?.relationType === RelationType.MANY_TO_ONE; fieldMetadata.relationTargetFieldMetadata,
);
if (!isDefined(fieldMetadata.relationTargetFieldMetadata)) { if (isRelationTargetMorphRelation) {
throw new FieldMetadataException( const morphRelationsWithSameName =
'Target field metadata does not exist', await this.getMorphRelationsWithSameName({
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, fieldMetadataName: fieldMetadata.relationTargetFieldMetadata.name,
); objectMetadataId:
} fieldMetadata.relationTargetFieldMetadata.objectMetadataId,
workspaceId,
fieldMetadataRepository,
});
await fieldMetadataRepository.delete({ morphRelationsWithSameName.forEach((morphRelation) => {
id: In([ 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.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( await fieldMetadataRepository.delete({
generateMigrationName(`delete-${fieldMetadata.name}`), id: In(fieldMetadataIdsToDelete),
workspaceId, });
[
{ for (const morphRelation of morphRelationsWithSameName) {
name: isManyToOneRelation await this.generateDeleteRelationMigration({
? computeObjectTargetTable(fieldMetadata.object) fieldMetadata: morphRelation,
: computeObjectTargetTable( workspaceId,
fieldMetadata.relationTargetObjectMetadata as ObjectMetadataEntity, queryRunner,
), });
action: WorkspaceMigrationTableActionType.ALTER, }
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: isManyToOneRelation
? `${(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`
: `${(fieldMetadata.relationTargetFieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`,
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
],
queryRunner,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) { } else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
await fieldMetadataRepository.delete(fieldMetadata.id); await fieldMetadataRepository.delete(fieldMetadata.id);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
@ -710,4 +764,126 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}, },
); );
} }
private async getMorphRelationsWithSameName({
fieldMetadataName,
objectMetadataId,
workspaceId,
fieldMetadataRepository,
}: {
fieldMetadataName: string;
objectMetadataId: string;
workspaceId: string;
fieldMetadataRepository: Repository<FieldMetadataEntity>;
}): 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<FieldMetadataType.RELATION>).settings?.joinColumnName}`,
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
],
queryRunner,
);
}
} }

View File

@ -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),
);
};

View File

@ -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<FieldMetadataType.MORPH_RELATION> => {
return fieldMetadata.type === FieldMetadataType.MORPH_RELATION;
};

View File

@ -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<FieldMetadataType.RELATION> => {
return fieldMetadata.type === FieldMetadataType.RELATION;
};

View File

@ -138,11 +138,12 @@ describe('createOne FieldMetadataService morph relation fields', () => {
expect(createdField.id).toBeDefined(); expect(createdField.id).toBeDefined();
expect(createdField.name).toBe('owner'); expect(createdField.name).toBe('owner');
// expect(createdField.relation).toBeUndefined(); expect(createdField.morphRelations[0].targetObjectMetadata.id).toBe(
// expect(createdField.morphRelations[0].type).toBe( contextPayload.firstTargetObjectMetadataId,
// contextPayload.relationType, );
// ); expect(createdField.morphRelations[1].targetObjectMetadata.id).toBe(
// expect(createdField.morphRelations[0].targetFieldMetadata.id).toBeDefined(); contextPayload.secondTargetObjectMetadataId,
);
const isManyToOne = const isManyToOne =
contextPayload.relationType === RelationType.MANY_TO_ONE; contextPayload.relationType === RelationType.MANY_TO_ONE;
@ -155,8 +156,6 @@ describe('createOne FieldMetadataService morph relation fields', () => {
expect(createdField.settings?.joinColumnName).toBeUndefined(); expect(createdField.settings?.joinColumnName).toBeUndefined();
} }
// TODO: check the morphrelation targets are created correctly (wait for Query Morph Relations)
await deleteOneFieldMetadata({ await deleteOneFieldMetadata({
input: { idToDelete: createdField.id }, input: { idToDelete: createdField.id },
}).catch(); }).catch();

View File

@ -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();
},
);
});

View File

@ -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', () => { describe('Delete Object metadata with morph relation should succeed', () => {
it('should succeed', () => { let opportunityId = '';
// 'TODO guillim : once delete is implemented' 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;
};

View File

@ -4,6 +4,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; 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'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const createMorphRelationBetweenObjects = async ({ 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 { const {
data: { createOneField: createdFieldPerson }, data: { createOneField: createdFieldPerson },
} = await createOneFieldMetadata({ } = await createOneFieldMetadata({
@ -72,9 +66,20 @@ export const createMorphRelationBetweenObjects = async ({
id id
nameSingular nameSingular
} }
morphRelations {
type
targetFieldMetadata {
id
}
targetObjectMetadata {
id
}
}
`, `,
expectToFail: false, expectToFail: false,
}); });
return createdFieldPerson as FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>; return createdFieldPerson as FieldMetadataEntity<FieldMetadataType.MORPH_RELATION> & {
morphRelations: RelationDTO[];
};
}; };