ObjectMetadataServiceV2.updateOne (#13891)

# Introduction
Last service to get migrated, `updateOne` object metadata service v2
This commit is contained in:
Paul Rastoin
2025-08-14 11:37:52 +02:00
committed by GitHub
parent 3c034c4ab4
commit ad77d2f462
42 changed files with 798 additions and 282 deletions
@@ -0,0 +1,5 @@
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const FLAT_OBJECT_METADATA_JSONB_PROPERTIES = [
'standardOverrides',
] as const satisfies (keyof FlatObjectMetadata)[];
@@ -0,0 +1,13 @@
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE = [
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'labelPlural',
'labelSingular',
'namePlural',
'nameSingular',
'standardOverrides', // Only if standard
] as const satisfies (keyof FlatObjectMetadata)[];
@@ -10,13 +10,14 @@ import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/
import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util';
import { FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator';
import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator';
import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util';
import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util';
import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util';
import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
export type ValidateOneFlatObjectMetadataArgs = {
@@ -32,6 +33,40 @@ export class FlatObjectMetadataValidatorService {
private readonly flatFieldMetadataValidatorService: FlatFieldMetadataValidatorService,
) {}
public validateFlatObjectMetadataUpdate({
existingFlatObjectMetadataMaps,
updatedFlatObjectMetadata,
}: {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
updatedFlatObjectMetadata: FlatObjectMetadata;
}) {
const existingFlatObjectMetadata =
existingFlatObjectMetadataMaps.byId[updatedFlatObjectMetadata.id];
if (!isDefined(existingFlatObjectMetadata)) {
return [
new ObjectMetadataException(
t`Object to update not found`,
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
),
];
}
const errors: FailedFlatObjectMetadataValidationExceptions[] = [];
errors.push(
...this.validateFlatObjectMetadataNameAndLabels({
existingFlatObjectMetadataMaps,
flatObjectMetadataToValidate: updatedFlatObjectMetadata,
}),
);
errors.push(
...validateFlatObjectMetadataIdentifiers(existingFlatObjectMetadata),
);
return errors;
}
public validateFlatObjectMetadataDeletion({
existingFlatObjectMetadataMaps,
objectMetadataToDeleteId,
@@ -47,7 +82,7 @@ export class FlatObjectMetadataValidatorService {
if (!isDefined(flatObjectMetadataToDelete)) {
errors.push(
new ObjectMetadataException(
t`Object does not exist`,
t`Object to delete not found`,
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
),
);
@@ -93,20 +128,6 @@ export class FlatObjectMetadataValidatorService {
}: ValidateOneFlatObjectMetadataArgs) {
const errors: FailedFlatObjectMetadataValidationExceptions[] = [];
errors.push(
...validateFlatObjectMetadataNames({
namePlural: flatObjectMetadataToValidate.namePlural,
nameSingular: flatObjectMetadataToValidate.nameSingular,
}),
);
errors.push(
...validateFlatObjectMetadataLabel({
labelPlural: flatObjectMetadataToValidate.labelPlural,
labelSingular: flatObjectMetadataToValidate.labelSingular,
}),
);
if (flatObjectMetadataToValidate.isRemote) {
errors.push(
new ObjectMetadataException(
@@ -116,51 +137,12 @@ export class FlatObjectMetadataValidatorService {
);
}
if (flatObjectMetadataToValidate.isLabelSyncedWithName === true) {
const computedNameSingular = computeMetadataNameFromLabel(
flatObjectMetadataToValidate.labelSingular,
);
if (computedNameSingular !== flatObjectMetadataToValidate.nameSingular) {
errors.push(
new ObjectMetadataException(
t`Singular name is not synced with singular label`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
),
);
}
const computedNamePlural = computeMetadataNameFromLabel(
flatObjectMetadataToValidate.labelPlural,
);
if (computedNamePlural !== flatObjectMetadataToValidate.namePlural) {
errors.push(
new ObjectMetadataException(
t`Plural name is not synced with plural label`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
),
);
}
}
if (
doesOtherObjectWithSameNameExists({
objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural,
objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular,
objectMetadataMaps: existingFlatObjectMetadataMaps,
})
) {
errors.push(
new ObjectMetadataException(
'Object already exists',
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
{
userFriendlyMessage: t`Object already exists`,
},
),
);
}
errors.push(
...this.validateFlatObjectMetadataNameAndLabels({
existingFlatObjectMetadataMaps,
flatObjectMetadataToValidate,
}),
);
const allFlatFieldMetadatasValidationErrors: FailedFlatFieldMetadataValidationExceptions[] =
[];
@@ -206,4 +188,61 @@ export class FlatObjectMetadataValidatorService {
return errors;
}
private validateFlatObjectMetadataNameAndLabels({
existingFlatObjectMetadataMaps,
flatObjectMetadataToValidate,
}: {
flatObjectMetadataToValidate: FlatObjectMetadata;
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
}) {
const errors: FailedFlatObjectMetadataValidationExceptions[] = [];
errors.push(
...validateFlatObjectMetadataNames({
namePlural: flatObjectMetadataToValidate.namePlural,
nameSingular: flatObjectMetadataToValidate.nameSingular,
}),
);
errors.push(
...validateFlatObjectMetadataLabel({
labelPlural: flatObjectMetadataToValidate.labelPlural,
labelSingular: flatObjectMetadataToValidate.labelSingular,
}),
);
if (
flatObjectMetadataToValidate.isLabelSyncedWithName &&
!areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate)
) {
errors.push(
new ObjectMetadataException(
t`Names are not synced with labels`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
),
);
}
if (
doesOtherObjectWithSameNameExists({
objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural,
objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular,
objectMetadataMaps: existingFlatObjectMetadataMaps,
existingObjectMetadataId: flatObjectMetadataToValidate.id,
})
) {
errors.push(
new ObjectMetadataException(
'Object already exists',
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
{
userFriendlyMessage: t`Object already exists`,
},
),
);
}
return errors;
}
}
@@ -0,0 +1,4 @@
import { type FLAT_OBJECT_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant';
export type FlatObjectMetadataEntityJsonbProperties =
(typeof FLAT_OBJECT_METADATA_JSONB_PROPERTIES)[number];
@@ -0,0 +1,4 @@
import { type FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
export type FlatObjectMetadataPropertiesToCompare =
(typeof FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,19 @@
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
export const areFlatObjectMetadataNamesSyncedWithLabels = (
flatObjectdMetadata: Pick<
FlatObjectMetadata,
'namePlural' | 'nameSingular' | 'labelPlural' | 'labelSingular'
>,
) => {
const [computedSingularName, computedPluralName] = [
flatObjectdMetadata.labelSingular,
flatObjectdMetadata.labelPlural,
].map(computeMetadataNameFromLabel);
return (
flatObjectdMetadata.nameSingular === computedSingularName &&
flatObjectdMetadata.namePlural === computedPluralName
);
};
@@ -1,36 +1,53 @@
import omit from 'lodash.omit';
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { FLAT_OBJECT_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant';
import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
import { type FlatObjectMetadataEntityJsonbProperties } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-jsonb-properties.type';
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const flatObjectMetadataPropertiesToCompare = [
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'labelPlural',
'labelSingular',
'namePlural',
'nameSingular',
'standardOverrides', // Only if standard
] as const satisfies (keyof FlatObjectMetadata)[];
export type FlatObjectMetadataPropertiesToCompare =
(typeof flatObjectMetadataPropertiesToCompare)[number];
/**
* This comparator handles update on colliding uniqueIdentifier flatObjectMetadata
*/
export const compareTwoFlatObjectMetadata = ({
from,
to,
}: FromTo<FlatObjectMetadata>) => {
const fromCompare = transformMetadataForComparison(from, {});
const toCompare = transformMetadataForComparison(to, {});
const transformMetadataForComparisonParameters = {
propertiesToStringify: FLAT_OBJECT_METADATA_JSONB_PROPERTIES,
shouldIgnoreProperty: (
property: string,
flatObjectMetadata: FlatObjectMetadata,
) => {
if (
!FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.includes(
property as FlatObjectMetadataPropertiesToCompare,
)
) {
return true;
}
if (
isStandardMetadata(flatObjectMetadata) &&
property !== 'standardOverrides'
) {
return true;
}
return false;
},
};
const fromCompare = transformMetadataForComparison(
from,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
to,
transformMetadataForComparisonParameters,
);
const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields'));
return objectMetadataDifference.flatMap<
@@ -38,32 +55,24 @@ export const compareTwoFlatObjectMetadata = ({
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
if (
difference.oldValue === null &&
(difference.value === null || difference.value === undefined)
) {
return [];
}
const property = difference.path[0];
const { oldValue, path, value } = difference;
const property = path[0] as FlatObjectMetadataPropertiesToCompare;
const isJsonb = FLAT_OBJECT_METADATA_JSONB_PROPERTIES.includes(
property as FlatObjectMetadataEntityJsonbProperties,
);
// TODO investigate why it would be a number, in case of array I guess ?
if (typeof property === 'number') {
return [];
}
// Could be handled directly from the diff we do above
if (
!flatObjectMetadataPropertiesToCompare.includes(
property as FlatObjectMetadataPropertiesToCompare,
)
) {
return [];
if (isJsonb) {
return {
from: isDefined(oldValue) ? JSON.parse(oldValue) : oldValue,
to: isDefined(value) ? JSON.parse(value) : value,
property,
};
}
return {
property: property as FlatObjectMetadataPropertiesToCompare,
from: difference.oldValue,
to: difference.value,
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
@@ -0,0 +1,115 @@
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
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 { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { type ObjectMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/object-metadata/types/object-metadata-standard-overrides-properties.types';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
type FromUpdateObjectInputToFlatObjectMetadataArgs = {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
updateObjectInput: UpdateOneObjectInput;
};
const objectMetadataEditableProperties =
FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.filter(
(
property,
): property is Exclude<
FlatObjectMetadataPropertiesToCompare,
'standardOverrides'
> => property !== 'standardOverrides',
);
export const fromUpdateObjectInputToFlatObjectMetadata = ({
existingFlatObjectMetadataMaps,
updateObjectInput: rawUpdateObjectInput,
}: FromUpdateObjectInputToFlatObjectMetadataArgs): FlatObjectMetadata => {
const { id: objectMetadataIdToUpdate } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateObjectInput,
['id'],
);
const updatedEditableObjectProperties = extractAndSanitizeObjectStringFields(
rawUpdateObjectInput.update,
objectMetadataEditableProperties,
);
const flatObjectMetadataToUpdate =
findFlatObjectMetadataInFlatObjectMetadataMaps({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataId: objectMetadataIdToUpdate,
});
if (!isDefined(flatObjectMetadataToUpdate)) {
throw new ObjectMetadataException(
'Object to update not found',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
if (isStandardMetadata(flatObjectMetadataToUpdate)) {
const invalidUpdatedProperties = Object.keys(
updatedEditableObjectProperties,
).filter(
(property) =>
!OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes(
property as ObjectMetadataStandardOverridesProperties,
),
);
if (invalidUpdatedProperties.length > 0) {
throw new ObjectMetadataException(
`Cannot edit standard object metadata properties: ${invalidUpdatedProperties.join(', ')}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
}
const updatedStandardFlatObjectdMetadata =
OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce((acc, property) => {
const isPropertyUpdated =
updatedEditableObjectProperties[property] !== undefined;
return {
...acc,
standardOverrides: {
...acc.standardOverrides,
...(isPropertyUpdated
? { [property]: updatedEditableObjectProperties[property] }
: {}),
},
};
}, flatObjectMetadataToUpdate);
return updatedStandardFlatObjectdMetadata;
}
const updatedFlatObjectMetadata = objectMetadataEditableProperties.reduce(
(acc, property) => {
const isPropertyUpdated =
updatedEditableObjectProperties[property] !== undefined;
return {
...acc,
...(isPropertyUpdated
? { [property]: updatedEditableObjectProperties[property] }
: {}),
};
},
flatObjectMetadataToUpdate,
);
return updatedFlatObjectMetadata;
};
@@ -0,0 +1,72 @@
import { t } from '@lingui/core/macro';
import {
isDefined,
isLabelIdentifierFieldMetadataTypes,
} from 'twenty-shared/utils';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
export const validateFlatObjectMetadataIdentifiers = (
flatObjectMetadata: FlatObjectMetadata,
) => {
const errors: ObjectMetadataException[] = [];
const { labelIdentifierFieldMetadataId, imageIdentifierFieldMetadataId } =
flatObjectMetadata;
if (isDefined(labelIdentifierFieldMetadataId)) {
const relatedFlatFieldMetadata = flatObjectMetadata.flatFieldMetadatas.find(
(flatFieldMetadata) =>
flatFieldMetadata.id === labelIdentifierFieldMetadataId,
);
if (!isDefined(relatedFlatFieldMetadata)) {
errors.push(
new ObjectMetadataException(
'labelIdentifierFieldMetadataId validation failed: related field metadata not found',
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
{
userFriendlyMessage: t`Field declared as label identifier not found`,
},
),
);
} else if (
!isLabelIdentifierFieldMetadataTypes(relatedFlatFieldMetadata.type)
) {
errors.push(
new ObjectMetadataException(
'labelIdentifierFieldMetadataId validation failed: field type not compatible',
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
{
userFriendlyMessage: t`Field cannot be used as label identifier`,
},
),
);
}
}
if (isDefined(imageIdentifierFieldMetadataId)) {
const relatedFlatFieldMetadata = flatObjectMetadata.flatFieldMetadatas.find(
(flatFieldMetadata) =>
flatFieldMetadata.id === imageIdentifierFieldMetadataId,
);
if (!isDefined(relatedFlatFieldMetadata)) {
errors.push(
new ObjectMetadataException(
'imageIdentifierFieldMetadataId validation failed: related field metadata not found',
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
{
userFriendlyMessage: t`Field declared as image identifier not found`,
},
),
);
}
}
return errors;
};