mirror of
https://github.com/twentyhq/twenty.git
synced 2026-04-19 22:39:30 -04:00
Fix v2 self relation field creation (#15382)
# Introduction Fixing self relation field creation in v2 - When computing related flat field to delete on object metadata deletion that has self relation fields - On self relation creation validation name availability not searching for the relation target field of the current object field if it's the field being validated ## Coverage - added CUD integration testing on self relation fields close https://github.com/twentyhq/twenty/issues/15153
This commit is contained in:
@@ -5,4 +5,5 @@ export const FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE = [
|
||||
'description',
|
||||
'isActive',
|
||||
'standardOverrides',
|
||||
'icon',
|
||||
] as const satisfies (typeof ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY.fieldMetadata.propertiesToCompare)[number][];
|
||||
|
||||
@@ -79,13 +79,17 @@ export const validateFlatFieldMetadataNameAvailability = ({
|
||||
}
|
||||
|
||||
const targetFlatFieldMetadata =
|
||||
remainingFlatEntityMapsToValidate?.byId[
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId
|
||||
] ??
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: existingFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
flatFieldMetadata.id ===
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId
|
||||
? flatFieldMetadata
|
||||
: (remainingFlatEntityMapsToValidate?.byId[
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId
|
||||
] ??
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId:
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
}));
|
||||
|
||||
if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) {
|
||||
return false;
|
||||
|
||||
@@ -59,7 +59,11 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({
|
||||
});
|
||||
const flatFieldMetadatasToDelete = objectFlatFieldMetadatas.flatMap(
|
||||
(flatFieldMetadata) => {
|
||||
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
|
||||
if (
|
||||
isMorphOrRelationFlatFieldMetadata(flatFieldMetadata) &&
|
||||
flatFieldMetadata.relationTargetObjectMetadataId !==
|
||||
objectMetadataToDeleteId
|
||||
) {
|
||||
const relationTargetFlatFieldMetadata =
|
||||
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
|
||||
flatFieldMetadata,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -86,13 +87,18 @@ describe('createOne FieldMetadataService relation fields', () => {
|
||||
name: 'person',
|
||||
});
|
||||
|
||||
expect(createdField).toMatchObject({
|
||||
name: 'person',
|
||||
relation: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
},
|
||||
settings: {
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
expect(createdField.id).toBeDefined();
|
||||
expect(createdField.name).toBe('person');
|
||||
expect(createdField.relation?.type).toBe(RelationType.MANY_TO_ONE);
|
||||
expect(createdField.relation?.targetFieldMetadata.id).toBeDefined();
|
||||
|
||||
expect(createdField.settings?.joinColumnName).toBe('personId');
|
||||
|
||||
if (!isDefined(createdField.relation?.targetFieldMetadata?.id)) {
|
||||
throw new Error('targetFieldMetadata.id is not defined');
|
||||
}
|
||||
@@ -101,12 +107,14 @@ describe('createOne FieldMetadataService relation fields', () => {
|
||||
fieldMetadataId: createdField.relation.targetFieldMetadata.id,
|
||||
});
|
||||
|
||||
expect(opportunityFieldOnPerson.object.nameSingular).toBe(
|
||||
'personForRelation',
|
||||
);
|
||||
expect(opportunityFieldOnPerson.relation.type).toBe(
|
||||
RelationType.ONE_TO_MANY,
|
||||
);
|
||||
expect(opportunityFieldOnPerson).toMatchObject({
|
||||
object: {
|
||||
nameSingular: 'personForRelation',
|
||||
},
|
||||
relation: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
opportunityFieldOnPerson.relation.targetFieldMetadata.id,
|
||||
).toBeDefined();
|
||||
@@ -125,11 +133,14 @@ describe('createOne FieldMetadataService relation fields', () => {
|
||||
name: 'person',
|
||||
});
|
||||
|
||||
expect(createdField).toMatchObject({
|
||||
name: 'person',
|
||||
relation: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
expect(createdField.id).toBeDefined();
|
||||
expect(createdField.name).toBe('person');
|
||||
expect(createdField.relation?.type).toBe(RelationType.ONE_TO_MANY);
|
||||
expect(createdField.relation?.targetFieldMetadata.id).toBeDefined();
|
||||
|
||||
expect(createdField.settings?.joinColumnName).toBeUndefined();
|
||||
|
||||
if (!isDefined(createdField.relation?.targetFieldMetadata?.id)) {
|
||||
@@ -140,22 +151,114 @@ describe('createOne FieldMetadataService relation fields', () => {
|
||||
fieldMetadataId: createdField.relation.targetFieldMetadata.id,
|
||||
});
|
||||
|
||||
expect(opportunityFieldOnPerson.object.nameSingular).toBe(
|
||||
'personForRelation',
|
||||
);
|
||||
expect(opportunityFieldOnPerson.relation.type).toBe(
|
||||
RelationType.MANY_TO_ONE,
|
||||
);
|
||||
expect(opportunityFieldOnPerson).toMatchObject({
|
||||
object: {
|
||||
nameSingular: 'personForRelation',
|
||||
},
|
||||
relation: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
},
|
||||
settings: {
|
||||
joinColumnName: 'opportunityId',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
opportunityFieldOnPerson.relation.targetFieldMetadata.id,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
opportunityFieldOnPerson.relation.targetObjectMetadata.id,
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
expect(opportunityFieldOnPerson.settings?.joinColumnName).toBe(
|
||||
'opportunityId',
|
||||
);
|
||||
it('MANY TO ONE self-relation field creation', async () => {
|
||||
const createdField = await createRelationBetweenObjects({
|
||||
objectMetadataId: createdObjectMetadataPersonId,
|
||||
targetObjectMetadataId: createdObjectMetadataPersonId,
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'manager',
|
||||
});
|
||||
|
||||
expect(createdField).toMatchObject({
|
||||
name: 'manager',
|
||||
relation: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadata: {
|
||||
id: createdObjectMetadataPersonId,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
joinColumnName: 'managerId',
|
||||
},
|
||||
});
|
||||
jestExpectToBeDefined(createdField.relation?.targetFieldMetadata.id);
|
||||
|
||||
const reportsFieldOnPerson = await findFieldMetadata({
|
||||
fieldMetadataId: createdField.relation.targetFieldMetadata.id,
|
||||
});
|
||||
|
||||
expect(reportsFieldOnPerson).toMatchObject({
|
||||
object: {
|
||||
nameSingular: 'personForRelation',
|
||||
id: createdObjectMetadataPersonId,
|
||||
},
|
||||
relation: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadata: {
|
||||
id: createdObjectMetadataPersonId,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(reportsFieldOnPerson.relation.targetFieldMetadata.id).toBeDefined();
|
||||
expect(reportsFieldOnPerson.settings?.joinColumnName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ONE TO MANY self-relation field creation', async () => {
|
||||
const createdField = await createRelationBetweenObjects({
|
||||
objectMetadataId: createdObjectMetadataPersonId,
|
||||
targetObjectMetadataId: createdObjectMetadataPersonId,
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'reports',
|
||||
targetFieldLabel: 'reportTarget',
|
||||
});
|
||||
|
||||
expect(createdField).toMatchObject({
|
||||
name: 'reports',
|
||||
relation: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadata: {
|
||||
id: createdObjectMetadataPersonId,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(createdField.relation?.targetFieldMetadata.id).toBeDefined();
|
||||
expect(createdField.settings?.joinColumnName).toBeUndefined();
|
||||
|
||||
if (!isDefined(createdField.relation?.targetFieldMetadata?.id)) {
|
||||
throw new Error('targetFieldMetadata.id is not defined');
|
||||
}
|
||||
|
||||
const managerFieldOnPerson = await findFieldMetadata({
|
||||
fieldMetadataId: createdField.relation.targetFieldMetadata.id,
|
||||
});
|
||||
|
||||
expect(managerFieldOnPerson).toMatchObject({
|
||||
object: {
|
||||
nameSingular: 'personForRelation',
|
||||
id: createdObjectMetadataPersonId,
|
||||
},
|
||||
relation: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadata: {
|
||||
id: createdObjectMetadataPersonId,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
joinColumnName: 'reporttargetId',
|
||||
},
|
||||
});
|
||||
expect(managerFieldOnPerson.relation.targetFieldMetadata.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -322,10 +323,57 @@ describe('Field metadata relation creation should fail', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(errors),
|
||||
);
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should fail when creating MANY_TO_ONE self-relation with same field name and label', async () => {
|
||||
const { errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: 'manager',
|
||||
label: 'Manager',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
relationCreationPayload: {
|
||||
targetFieldLabel: 'Manager',
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
targetFieldIcon: 'IconUser',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when creating ONE_TO_MANY self-relation with same field name and label', async () => {
|
||||
const { errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: 'manager',
|
||||
label: 'Manager',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
relationCreationPayload: {
|
||||
targetFieldLabel: 'Manager',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
targetFieldIcon: 'IconUser',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
@@ -103,3 +105,173 @@ describe('Field metadata relation update should succeed', () => {
|
||||
expect(data.updateOneField.isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Field metadata self-relation update should succeed', () => {
|
||||
let personObjectId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: getMockCreateObjectInput({
|
||||
namePlural: 'peopleForUpdate',
|
||||
nameSingular: 'personForUpdate',
|
||||
}),
|
||||
});
|
||||
|
||||
personObjectId = objectId;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: personObjectId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: personObjectId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully update MANY_TO_ONE self-relation field label', async () => {
|
||||
const createdField = await createRelationBetweenObjects({
|
||||
objectMetadataId: personObjectId,
|
||||
targetObjectMetadataId: personObjectId,
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'supervisor',
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(createdField.id);
|
||||
|
||||
const { data } = await updateOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdField.id,
|
||||
updatePayload: {
|
||||
label: 'Direct Supervisor',
|
||||
description: 'The direct supervisor of this person',
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
label
|
||||
description
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data.updateOneField).toMatchObject({
|
||||
id: createdField.id,
|
||||
label: 'Direct Supervisor',
|
||||
description: 'The direct supervisor of this person',
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully update ONE_TO_MANY self-relation field label', async () => {
|
||||
const createdField = await createRelationBetweenObjects({
|
||||
objectMetadataId: personObjectId,
|
||||
targetObjectMetadataId: personObjectId,
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'directReports',
|
||||
targetFieldLabel: 'supervisor',
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(createdField.id);
|
||||
|
||||
const { data } = await updateOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdField.id,
|
||||
updatePayload: {
|
||||
label: 'Direct Reports',
|
||||
description: 'All direct reports under this person',
|
||||
icon: 'IconUsers',
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
label
|
||||
description
|
||||
icon
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data.updateOneField).toMatchObject({
|
||||
id: createdField.id,
|
||||
label: 'Direct Reports',
|
||||
description: 'All direct reports under this person',
|
||||
icon: 'IconUsers',
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully update both sides of self-relation independently', async () => {
|
||||
const createdField = await createRelationBetweenObjects({
|
||||
objectMetadataId: personObjectId,
|
||||
targetObjectMetadataId: personObjectId,
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'mentor',
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(createdField.id);
|
||||
jestExpectToBeDefined(createdField.relation?.targetFieldMetadata.id);
|
||||
|
||||
const mentorFieldId = createdField.id;
|
||||
const menteesFieldId = createdField.relation.targetFieldMetadata.id;
|
||||
|
||||
// Update the MANY_TO_ONE side
|
||||
const { data: mentorUpdateData } = await updateOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: mentorFieldId,
|
||||
updatePayload: {
|
||||
label: 'Mentor',
|
||||
description: 'The mentor of this person',
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
label
|
||||
description
|
||||
`,
|
||||
});
|
||||
|
||||
expect(mentorUpdateData.updateOneField).toMatchObject({
|
||||
id: mentorFieldId,
|
||||
label: 'Mentor',
|
||||
description: 'The mentor of this person',
|
||||
});
|
||||
|
||||
// Update the ONE_TO_MANY side
|
||||
const { data: menteesUpdateData } = await updateOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: menteesFieldId,
|
||||
updatePayload: {
|
||||
label: 'Mentees',
|
||||
description: 'All mentees under this person',
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
label
|
||||
description
|
||||
`,
|
||||
});
|
||||
|
||||
expect(menteesUpdateData.updateOneField).toMatchObject({
|
||||
id: menteesFieldId,
|
||||
label: 'Mentees',
|
||||
description: 'All mentees under this person',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user