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 14:17:20 +02: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];
},
);