Morph relation field v2 fixes (#14216)

Fixes, depending on the contextual current operation in order to
retrieve relation target and all morph related fields to be delete:
- DELETE Field,
- DELETE Object, 
- UPDATE Field, that affects all others related fields
This commit is contained in:
Paul Rastoin
2025-09-02 12:17:20 +00:00
committed by GitHub
parent c4f56156d6
commit 6d6ee939cf
10 changed files with 649 additions and 102 deletions
@@ -83,10 +83,14 @@ export class FieldMetadataServiceV2 {
const flatObjectMetadataMapsWithImpactedObject =
getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: flatFieldMetadatasToDelete.map(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId,
),
objectMetadataIds: [
...new Set(
flatFieldMetadatasToDelete.map(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId,
),
),
],
});
const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete.reduce(
@@ -145,20 +149,33 @@ export class FieldMetadataServiceV2 {
throw inputTranspilationResult.error;
}
const optimisticiallyUpdatedFlatFieldMetadata =
const optimisticallyUpdatedFlatFieldMetadatas =
inputTranspilationResult.result;
const objectMetadataIdWithRelatedObjectMetadataIds = [
...new Set(
optimisticallyUpdatedFlatFieldMetadatas.flatMap(
({ objectMetadataId, relationTargetObjectMetadataId }) =>
isDefined(relationTargetObjectMetadataId)
? [objectMetadataId, relationTargetObjectMetadataId]
: [objectMetadataId],
),
),
];
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [
optimisticiallyUpdatedFlatFieldMetadata.objectMetadataId,
],
objectMetadataIds: objectMetadataIdWithRelatedObjectMetadataIds,
});
const toFlatObjectMetadataMaps =
replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
flatFieldMetadata: optimisticiallyUpdatedFlatFieldMetadata,
});
optimisticallyUpdatedFlatFieldMetadatas.reduce(
(flatObjectMetadataMaps, flatFieldMetadata) =>
replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps,
flatFieldMetadata,
}),
fromFlatObjectMetadataMaps,
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -182,7 +199,7 @@ export class FieldMetadataServiceV2 {
return this.fieldMetadataRepository.findOneOrFail({
where: {
id: optimisticiallyUpdatedFlatFieldMetadata.id,
id: optimisticallyUpdatedFlatFieldMetadatas[0].id,
workspaceId,
},
});
@@ -133,7 +133,7 @@ export class FlatFieldMetadataValidatorService {
validationResult.errors.push(
...validateFlatFieldMetadataName(updatedFlatFieldMetadata.name),
...validateFlatFieldMetadataNameAvailability({
name: updatedFlatFieldMetadata.name,
flatFieldMetadata: updatedFlatFieldMetadata,
flatObjectMetadata: flatObjectMetadata,
}),
);
@@ -298,7 +298,7 @@ export class FlatFieldMetadataValidatorService {
validationResult.errors.push(
...validateFlatFieldMetadataNameAvailability({
name: flatFieldMetadataToValidate.name,
flatFieldMetadata: flatFieldMetadataToValidate,
flatObjectMetadata: parentFlatObjectMetadata,
}),
);
@@ -0,0 +1,43 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findFlatFieldMetadatasRelatedToMorphRelationOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
export const computeFlatFieldMetadataRelatedFlatFieldMetadata = ({
flatFieldMetadata,
flatObjectMetadataMaps,
}: {
flatFieldMetadata: FlatFieldMetadata;
flatObjectMetadataMaps: FlatObjectMetadataMaps;
}): FlatFieldMetadata[] => {
if (
isFlatFieldMetadataEntityOfType(
flatFieldMetadata,
FieldMetadataType.RELATION,
)
) {
return [
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
flatFieldMetadata,
flatObjectMetadataMaps,
}),
];
}
if (
isFlatFieldMetadataEntityOfType(
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
return findFlatFieldMetadatasRelatedToMorphRelationOrThrow({
flatFieldMetadata,
flatObjectMetadataMaps,
});
}
return [];
};
@@ -62,6 +62,10 @@ export const findFlatFieldMetadatasRelatedToMorphRelationOrThrow = ({
flatObjectMetadataMaps,
});
if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) {
return [relationTargetFlatFieldMetadata];
}
return [flatFieldMetadata, relationTargetFlatFieldMetadata];
},
);
@@ -1,4 +1,3 @@
import { FieldMetadataType } from 'twenty-shared/types';
import {
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
@@ -10,9 +9,7 @@ import {
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findFlatFieldMetadatasRelatedToMorphRelationOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util';
@@ -43,32 +40,11 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
);
}
if (
isFlatFieldMetadataEntityOfType(
flatFieldMetadataToDelete,
FieldMetadataType.MORPH_RELATION,
)
) {
return findFlatFieldMetadatasRelatedToMorphRelationOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
const relatedFlatFieldMetadataToDelete =
computeFlatFieldMetadataRelatedFlatFieldMetadata({
flatFieldMetadata: flatFieldMetadataToDelete,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
});
}
if (
isFlatFieldMetadataEntityOfType(
flatFieldMetadataToDelete,
FieldMetadataType.RELATION,
)
) {
const relationTargetFlatFieldMetadata =
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatFieldMetadata: flatFieldMetadataToDelete,
});
return [flatFieldMetadataToDelete, relationTargetFlatFieldMetadata];
}
return [flatFieldMetadataToDelete];
return [flatFieldMetadataToDelete, ...relatedFlatFieldMetadataToDelete];
};
@@ -13,11 +13,36 @@ import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-m
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import {} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util';
import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
const applyUpdatesToFlatFieldMetadata = ({
updatedEditableFieldProperties,
flatFieldMetadata,
}: {
updatedEditableFieldProperties: SanitizedUpdateFieldInput;
flatFieldMetadata: FlatFieldMetadata;
}) => {
return fieldMetadataEditableProperties.reduce((acc, property) => {
let newValue = updatedEditableFieldProperties[property];
if (property === 'options' && isDefined(newValue)) {
newValue = updatedEditableFieldProperties[property]?.map((option) => ({
id: v4(),
...option,
}));
}
return {
...acc,
...(newValue !== undefined ? { [property]: newValue } : {}),
};
}, flatFieldMetadata);
};
const fieldMetadataEditableProperties =
FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.filter(
(
@@ -28,6 +53,13 @@ const fieldMetadataEditableProperties =
> => property !== 'standardOverrides',
);
type SanitizedUpdateFieldInput = ReturnType<
typeof extractAndSanitizeObjectStringFields<
UpdateFieldInput,
(typeof fieldMetadataEditableProperties)[number][]
>
>;
type FromUpdateFieldInputToFlatFieldMetadataArgs = {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
updateFieldInput: UpdateFieldInput;
@@ -35,7 +67,9 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = {
export const fromUpdateFieldInputToFlatFieldMetadata = ({
existingFlatObjectMetadataMaps,
updateFieldInput: rawUpdateFieldInput,
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadata> => {
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<
FlatFieldMetadata[]
> => {
const updateFieldInputInformalProperties =
extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [
'objectMetadataId',
@@ -46,13 +80,13 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
fieldMetadataEditableProperties,
);
const relatedFlatFieldMetadata =
const existingFlatFieldMetadataToUpdate =
findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId({
fieldMetadataId: updateFieldInputInformalProperties.id,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
});
if (!isDefined(relatedFlatFieldMetadata)) {
if (!isDefined(existingFlatFieldMetadataToUpdate)) {
return {
status: 'fail',
error: {
@@ -65,7 +99,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
const flatObjectMetadataWithFlatFieldMaps =
existingFlatObjectMetadataMaps.byId[
relatedFlatFieldMetadata.objectMetadataId
existingFlatFieldMetadataToUpdate.objectMetadataId
];
if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) {
@@ -79,7 +113,12 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
};
}
if (flatObjectMetadataWithFlatFieldMaps.isRemote) {
const flatObjectMetadata =
fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata(
flatObjectMetadataWithFlatFieldMaps,
);
if (flatObjectMetadata.isRemote) {
return {
status: 'fail',
error: {
@@ -89,7 +128,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
};
}
if (isStandardMetadata(relatedFlatFieldMetadata)) {
if (isStandardMetadata(existingFlatFieldMetadataToUpdate)) {
const invalidUpdatedProperties = Object.keys(
updatedEditableFieldProperties,
).filter((property) =>
@@ -125,35 +164,35 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
: {}),
},
};
}, relatedFlatFieldMetadata);
}, existingFlatFieldMetadataToUpdate);
return {
status: 'success',
result: updatedStandardFlatFieldMetadata,
result: [updatedStandardFlatFieldMetadata],
};
}
const updatedFlatFieldMetadata = fieldMetadataEditableProperties.reduce(
(acc, property) => {
let newValue = updatedEditableFieldProperties[property];
const relatedFlatFieldMetadatasToUpdate =
computeFlatFieldMetadataRelatedFlatFieldMetadata({
flatFieldMetadata: existingFlatFieldMetadataToUpdate,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
});
if (property === 'options' && isDefined(newValue)) {
newValue = updatedEditableFieldProperties[property]?.map((option) => ({
id: v4(),
...option,
}));
}
const flatFieldMetadatasToUpdate = [
existingFlatFieldMetadataToUpdate,
...relatedFlatFieldMetadatasToUpdate,
];
return {
...acc,
...(newValue !== undefined ? { [property]: newValue } : {}),
};
},
relatedFlatFieldMetadata,
);
const optimisticiallyUpdatedFlatFieldMetadatas =
flatFieldMetadatasToUpdate.map((flatFieldMetadata) => {
return applyUpdatesToFlatFieldMetadata({
flatFieldMetadata,
updatedEditableFieldProperties,
});
});
return {
status: 'success',
result: updatedFlatFieldMetadata,
result: optimisticiallyUpdatedFlatFieldMetadatas,
};
};
@@ -6,6 +6,8 @@ import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-me
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
@@ -32,39 +34,43 @@ const getReservedCompositeFieldNames = (
});
};
// Should implement Morph relation nameObjectId col availability
export const validateFlatFieldMetadataNameAvailability = ({
name,
flatFieldMetadata,
flatObjectMetadata,
}: {
name: string;
flatFieldMetadata: FlatFieldMetadata;
flatObjectMetadata: FlatObjectMetadata;
}): FlatFieldMetadataValidationError[] => {
const errors: FlatFieldMetadataValidationError[] = [];
const reservedCompositeFieldsNames =
getReservedCompositeFieldNames(flatObjectMetadata);
const flatFieldMetadataName = flatFieldMetadata.name;
if (
!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata) &&
flatObjectMetadata.flatFieldMetadatas.some(
(field) =>
field.name === name ||
field.name === flatFieldMetadataName ||
(isMorphOrRelationFieldMetadataType(field.type) &&
`${field.name}Id` === name),
`${field.name}Id` === flatFieldMetadataName),
)
) {
errors.push({
code: FieldMetadataExceptionCode.NOT_AVAILABLE,
value: name,
message: `Name "${name}" is not available as it is already used by another field`,
userFriendlyMessage: t`Name "${name}" is not available as it is already used by another field`,
value: flatFieldMetadataName,
message: `Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
userFriendlyMessage: t`Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
});
}
if (reservedCompositeFieldsNames.includes(name)) {
if (reservedCompositeFieldsNames.includes(flatFieldMetadataName)) {
errors.push({
code: FieldMetadataExceptionCode.RESERVED_KEYWORD,
message: `Name "${name}" is reserved composite field name`,
value: name,
userFriendlyMessage: t`Name "${name}" is not available`,
message: `Name "${flatFieldMetadataName}" is reserved composite field name`,
value: flatFieldMetadataName,
userFriendlyMessage: t`Name "${flatFieldMetadataName}" is not available`,
});
}
@@ -1,12 +1,10 @@
import { FieldMetadataType } from 'twenty-shared/types';
import {
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -48,26 +46,13 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({
const flatFieldMetadatasToDelete =
flatObjectMetadataToDelete.flatFieldMetadatas.flatMap(
(flatFieldMetadata) => {
if (
isFlatFieldMetadataEntityOfType(
const relatedFlatFieldMetadata =
computeFlatFieldMetadataRelatedFlatFieldMetadata({
flatFieldMetadata,
FieldMetadataType.RELATION,
) ||
isFlatFieldMetadataEntityOfType(
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
const relationTargetFlatFieldMetadata =
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatFieldMetadata,
});
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
});
return [flatFieldMetadata, relationTargetFlatFieldMetadata];
}
return flatFieldMetadata;
return [flatFieldMetadata, ...relatedFlatFieldMetadata];
},
);
@@ -0,0 +1,294 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
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 { 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';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
describe('createOne FieldMetadataService morph relation fields', () => {
let createdObjectMetadataPersonId: string;
let createdObjectMetadataOpportunityId: string;
let createdObjectMetadataCompanyId: string;
let createdFieldMetadataId: string | undefined = undefined;
beforeAll(async () => {
await updateFeatureFlag({
expectToFail: false,
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
value: true,
workspaceId: SEED_APPLE_WORKSPACE_ID,
});
const {
data: {
createOneObject: { id: objectMetadataPersonId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'personForMorphRelationSecond',
namePlural: 'peopleForMorphRelationSecond',
labelSingular: 'Person For Morph Relation',
labelPlural: 'People For Morph Relation',
icon: 'IconPerson',
},
});
createdObjectMetadataPersonId = objectMetadataPersonId;
const {
data: {
createOneObject: { id: objectMetadataCompanyId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'companyForMorphRelationSecond',
namePlural: 'companiesForMorphRelationSecond',
labelSingular: 'Company For Morph Relation',
labelPlural: 'Companies For Morph Relation',
icon: 'IconCompany',
},
});
createdObjectMetadataCompanyId = objectMetadataCompanyId;
const {
data: {
createOneObject: { id: objectMetadataOpportunityId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'opportunityForMorphRelationSecond',
namePlural: 'opportunitiesForMorphRelationSecond',
labelSingular: 'Opportunity For Morph Relation',
labelPlural: 'Opportunities For Morph Relation',
icon: 'IconOpportunity',
},
});
createdObjectMetadataOpportunityId = objectMetadataOpportunityId;
});
afterAll(async () => {
await Promise.all(
[
createdObjectMetadataPersonId,
createdObjectMetadataOpportunityId,
createdObjectMetadataCompanyId,
].map(
async (objectMetadataId) =>
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: objectMetadataId,
updatePayload: { isActive: false },
},
}),
),
);
await Promise.all(
[
createdObjectMetadataPersonId,
createdObjectMetadataOpportunityId,
createdObjectMetadataCompanyId,
].map(
async (objectMetadataId) =>
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: objectMetadataId },
}),
),
);
await updateFeatureFlag({
expectToFail: false,
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
value: false,
workspaceId: SEED_APPLE_WORKSPACE_ID,
});
});
afterEach(async () => {
if (!isDefined(createdFieldMetadataId)) {
return;
}
await updateOneFieldMetadata({
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: {
isActive: false,
},
},
expectToFail: false,
});
await deleteOneFieldMetadata({
input: {
idToDelete: createdFieldMetadataId,
},
expectToFail: false,
});
createdFieldMetadataId = undefined;
});
type EachTestingContextArray = EachTestingContext<
(args: {
createdObjectMetadataPersonId: string;
createdObjectMetadataOpportunityId: string;
createdObjectMetadataCompanyId: string;
}) => Omit<
CreateFieldInput,
'morphRelationsCreationPayload' | 'workspaceId'
> &
Required<Pick<CreateFieldInput, 'morphRelationsCreationPayload'>>
>[];
const eachTestingContextArray: EachTestingContextArray = [
{
title: 'should create a MORPH_RELATION field type MANY_TO_ONE',
context: ({
createdObjectMetadataCompanyId,
createdObjectMetadataOpportunityId,
createdObjectMetadataPersonId,
}) => ({
label: 'field label',
name: 'fieldName',
objectMetadataId: createdObjectMetadataCompanyId,
type: FieldMetadataType.MORPH_RELATION,
morphRelationsCreationPayload: [
{
targetFieldIcon: 'Icon123',
targetFieldLabel: 'toto',
targetObjectMetadataId: createdObjectMetadataOpportunityId,
type: RelationType.MANY_TO_ONE,
},
{
targetFieldIcon: 'Icon123',
targetFieldLabel: 'tata',
targetObjectMetadataId: createdObjectMetadataPersonId,
type: RelationType.MANY_TO_ONE,
},
],
}),
},
{
title: 'should create a MORPH_RELATION field type ONE_TO_MANY',
context: ({
createdObjectMetadataCompanyId,
createdObjectMetadataOpportunityId,
createdObjectMetadataPersonId,
}) => ({
label: 'field label',
name: 'fieldName',
objectMetadataId: createdObjectMetadataCompanyId,
type: FieldMetadataType.MORPH_RELATION,
morphRelationsCreationPayload: [
{
targetFieldIcon: 'Icon123',
targetFieldLabel: 'toto',
targetObjectMetadataId: createdObjectMetadataOpportunityId,
type: RelationType.ONE_TO_MANY,
},
{
targetFieldIcon: 'Icon123',
targetFieldLabel: 'tata',
targetObjectMetadataId: createdObjectMetadataPersonId,
type: RelationType.ONE_TO_MANY,
},
],
}),
},
];
it.each(eachTestingContextFilter(eachTestingContextArray))(
'$title',
async ({ context }) => {
const contextPayload = context({
createdObjectMetadataOpportunityId,
createdObjectMetadataPersonId,
createdObjectMetadataCompanyId,
});
const {
data: { createOneField: rawCreateOneField },
} = await createOneFieldMetadata({
input: contextPayload,
expectToFail: false,
gqlFields: `
id
name
label
isLabelSyncedWithName
settings
object {
id
nameSingular
}
morphRelations {
type
targetFieldMetadata {
id
}
targetObjectMetadata {
id
}
sourceFieldMetadata {
id
}
sourceObjectMetadata {
id
}
}
`,
});
const createOneField = rawCreateOneField as FieldMetadataDTO & {
morphRelations: RelationDTO[];
};
createdFieldMetadataId = createOneField.id;
const expectedMorphRelations =
contextPayload.morphRelationsCreationPayload.map(
({ targetObjectMetadataId, type }) => ({
sourceFieldMetadata: {
id: expect.any(String),
},
sourceObjectMetadata: {
id: contextPayload.objectMetadataId,
},
targetFieldMetadata: {
id: expect.any(String),
},
targetObjectMetadata: {
id: targetObjectMetadataId,
},
type,
}),
);
expect(createOneField.morphRelations).toMatchObject(
expectedMorphRelations,
);
},
);
});
@@ -0,0 +1,183 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
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 { 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';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
describe('updateOne FieldMetadataService morph relation fields v2', () => {
let createdObjectMetadataPersonId: string;
let createdObjectMetadataOpportunityId: string;
let createdObjectMetadataCompanyId: string;
let createdFieldMetadataId: string;
beforeAll(async () => {
await updateFeatureFlag({
expectToFail: false,
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
value: true,
workspaceId: SEED_APPLE_WORKSPACE_ID,
});
const {
data: {
createOneObject: { id: objectMetadataPersonId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'personForMorphRelationSecond',
namePlural: 'peopleForMorphRelationSecond',
labelSingular: 'Person For Morph Relation',
labelPlural: 'People For Morph Relation',
icon: 'IconPerson',
},
});
createdObjectMetadataPersonId = objectMetadataPersonId;
const {
data: {
createOneObject: { id: objectMetadataCompanyId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'companyForMorphRelationSecond',
namePlural: 'companiesForMorphRelationSecond',
labelSingular: 'Company For Morph Relation',
labelPlural: 'Companies For Morph Relation',
icon: 'IconCompany',
},
});
createdObjectMetadataCompanyId = objectMetadataCompanyId;
const {
data: {
createOneObject: { id: objectMetadataOpportunityId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'opportunityForMorphRelationSecond',
namePlural: 'opportunitiesForMorphRelationSecond',
labelSingular: 'Opportunity For Morph Relation',
labelPlural: 'Opportunities For Morph Relation',
icon: 'IconOpportunity',
},
});
createdObjectMetadataOpportunityId = objectMetadataOpportunityId;
});
afterAll(async () => {
await Promise.all(
[
createdObjectMetadataPersonId,
createdObjectMetadataOpportunityId,
createdObjectMetadataCompanyId,
].map(
async (objectMetadataId) =>
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: objectMetadataId,
updatePayload: { isActive: false },
},
}),
),
);
await Promise.all(
[
createdObjectMetadataPersonId,
createdObjectMetadataOpportunityId,
createdObjectMetadataCompanyId,
].map(
async (objectMetadataId) =>
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: objectMetadataId },
}),
),
);
await updateFeatureFlag({
expectToFail: false,
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
value: false,
workspaceId: SEED_APPLE_WORKSPACE_ID,
});
});
beforeEach(async () => {
const {
data: { createOneField: rawCreateOneField },
} = await createOneFieldMetadata({
input: {
label: 'field label',
name: 'fieldName',
objectMetadataId: createdObjectMetadataCompanyId,
type: FieldMetadataType.MORPH_RELATION,
morphRelationsCreationPayload: [
{
targetFieldIcon: 'Icon123',
targetFieldLabel: 'toto',
targetObjectMetadataId: createdObjectMetadataOpportunityId,
type: RelationType.MANY_TO_ONE,
},
{
targetFieldIcon: 'Icon123',
targetFieldLabel: 'tata',
targetObjectMetadataId: createdObjectMetadataPersonId,
type: RelationType.MANY_TO_ONE,
},
],
},
expectToFail: false,
});
createdFieldMetadataId = rawCreateOneField.id;
});
it('It should update all morph related flat field metadata allowing its deletion', async () => {
const input = {
idToUpdate: createdFieldMetadataId,
updatePayload: {
isActive: false,
label: 'new label',
description: 'new description',
},
};
const {
data: { updateOneField },
} = await updateOneFieldMetadata({
expectToFail: false,
input,
gqlFields: `
id
isActive
description
name
label
`,
});
expect(updateOneField).toMatchObject(input.updatePayload);
await deleteOneFieldMetadata({
input: {
idToDelete: createdFieldMetadataId,
},
expectToFail: false,
});
});
});