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,7 @@
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
export const FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES = [
'label',
'description',
'icon',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
@@ -4,16 +4,8 @@ import { IsJSON, IsOptional, IsString } from 'class-validator';
import { GraphQLJSON } from 'graphql-type-json';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import { FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type';
export const fieldMetadataStandardOverridesProperties = [
'label',
'description',
'icon',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
export type FieldMetadataStandardOverridesProperties =
(typeof fieldMetadataStandardOverridesProperties)[number];
@ObjectType('StandardOverrides')
export class FieldStandardOverridesDTO
implements Partial<Record<FieldMetadataStandardOverridesProperties, unknown>>
@@ -24,7 +24,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util';
import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util';
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
import { fromUpdateFieldInputToFlatFieldMetadataToUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata-to-update.util';
import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
@@ -213,11 +213,10 @@ export class FieldMetadataServiceV2 {
{ workspaceId },
);
const inputTranspilationResult =
fromUpdateFieldInputToFlatFieldMetadataToUpdate({
existingFlatObjectMetadataMaps,
updateFieldInput,
});
const inputTranspilationResult = fromUpdateFieldInputToFlatFieldMetadata({
existingFlatObjectMetadataMaps,
updateFieldInput,
});
if (inputTranspilationResult.status === 'fail') {
throw inputTranspilationResult.error;
@@ -0,0 +1,4 @@
import { type FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant';
export type FieldMetadataStandardOverridesProperties =
(typeof FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES)[number];
@@ -0,0 +1,8 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const FLAT_FIELD_METADATA_JSONB_PROPERTIES = [
'defaultValue',
'options',
'settings',
'standardOverrides',
] as const satisfies (keyof FlatFieldMetadata)[];
@@ -0,0 +1,15 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE = [
'defaultValue',
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'isUnique',
'label',
'name',
'options',
'standardOverrides',
'settings',
] as const satisfies (keyof FlatFieldMetadata)[];
@@ -0,0 +1,7 @@
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
export const FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE = [
'label',
'description',
'isActive',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
@@ -13,8 +13,8 @@ import { ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-f
import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
import { isEnumValidateOneFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util';
import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator';
import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-relation-flat-field-metadata.validator';
import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util';
import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util';
@Injectable()
export class FlatFieldMetadataTypeValidatorService {
constructor(private readonly featureFlagService: FeatureFlagService) {}
@@ -8,16 +8,16 @@ import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import {
compareTwoFlatFieldMetadata,
type FlatFieldMetadataPropertiesToCompare,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import { compareTwoFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import { isFlatFieldMetadataNameSyncedWithLabel } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator';
import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator';
import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util';
import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
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 {
@@ -28,7 +28,6 @@ import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
export type ValidateOneFieldMetadataArgs<
T extends FieldMetadataType = FieldMetadataType,
@@ -110,15 +109,10 @@ export class FlatFieldMetadataValidatorService {
FieldMetadataType.MORPH_RELATION,
)
) {
const relationEditableFields = [
'label',
'description',
'isActive',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
const relationNonEditableUpdatedProperties = updates.flatMap(
({ property }) =>
!relationEditableFields.includes(
property as (typeof relationEditableFields)[number],
!FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE.includes(
property as FlatFieldMetadataRelationPropertiesToCompare,
)
? property
: [],
@@ -155,19 +149,16 @@ export class FlatFieldMetadataValidatorService {
}
}
if (updatedFlatFieldMetadata.isLabelSyncedWithName) {
const computedName = computeMetadataNameFromLabel(
updatedFlatFieldMetadata.label,
if (
updatedFlatFieldMetadata.isLabelSyncedWithName &&
!isFlatFieldMetadataNameSyncedWithLabel(updatedFlatFieldMetadata)
) {
errors.push(
new InvalidMetadataException(
t`Name is not synced with label.`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
),
);
if (updatedFlatFieldMetadata.name !== computedName) {
errors.push(
new InvalidMetadataException(
`Name is not synced with label. Expected name: "${computedName}", got ${updatedFlatFieldMetadata.name}`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
),
);
}
}
const fieldMetadataTypeValidationErrors =
@@ -295,19 +286,16 @@ export class FlatFieldMetadataValidatorService {
}
}
if (flatFieldMetadataToValidate.isLabelSyncedWithName) {
const computedName = computeMetadataNameFromLabel(
flatFieldMetadataToValidate.label,
if (
flatFieldMetadataToValidate.isLabelSyncedWithName &&
!isFlatFieldMetadataNameSyncedWithLabel(flatFieldMetadataToValidate)
) {
errors.push(
new InvalidMetadataException(
t`Name is not synced with label.`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
),
);
if (flatFieldMetadataToValidate.name !== computedName) {
errors.push(
new InvalidMetadataException(
`Name is not synced with label. Expected name: "${computedName}", got ${flatFieldMetadataToValidate.name}`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
),
);
}
}
errors.push(
@@ -0,0 +1,4 @@
import { type FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant';
export type FlatFieldMetadataEntityJsonbProperties =
(typeof FLAT_FIELD_METADATA_JSONB_PROPERTIES)[number];
@@ -0,0 +1,4 @@
import { type FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant';
export type FlatFieldMetadataPropertiesToCompare =
(typeof FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,4 @@
import { type FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant';
export type FlatFieldMetadataRelationPropertiesToCompare =
(typeof FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE)[number];
@@ -2,36 +2,17 @@ import diff from 'microdiff';
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant';
import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant';
import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant';
import { type FlatFieldMetadataEntityJsonbProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type';
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
import { type FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
export const flatFieldMetadataPropertiesToCompare = [
'defaultValue',
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'isUnique',
'label',
'name',
'options',
'standardOverrides',
'settings',
] as const satisfies (keyof FlatFieldMetadata)[];
export type FlatFieldMetadataPropertiesToCompare =
(typeof flatFieldMetadataPropertiesToCompare)[number];
export const flatFieldMetadataEntityJsonbProperties = [
'defaultValue',
'options',
'settings',
'standardOverrides',
] as const satisfies (keyof FlatFieldMetadata)[];
export type FlatFieldMetadataEntityJsonbProperties =
(typeof flatFieldMetadataEntityJsonbProperties)[number];
const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
return [
FieldMetadataType.BOOLEAN,
@@ -51,13 +32,13 @@ export const compareTwoFlatFieldMetadata = ({
from,
to,
}: GetWorkspaceMigrationUpdateFieldActionArgs) => {
const compareFieldMetadataOptions = {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (
property: string,
fieldMetadata: FlatFieldMetadata,
) => {
if (
!flatFieldMetadataPropertiesToCompare.includes(
!FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.includes(
property as FlatFieldMetadataPropertiesToCompare,
)
) {
@@ -73,26 +54,38 @@ export const compareTwoFlatFieldMetadata = ({
}
// Remove below assertion when we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b
if (
const isRelationFieldType =
isDefined(fieldMetadata.type) &&
(fieldMetadata.type === FieldMetadataType.RELATION ||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION) &&
!['label', 'description', 'isActive'].includes(property)
fieldMetadata.type === FieldMetadataType.MORPH_RELATION);
if (
isRelationFieldType &&
!FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE.includes(
property as FlatFieldMetadataRelationPropertiesToCompare,
)
) {
return true;
}
if (
isStandardMetadata(fieldMetadata) &&
property !== 'standardOverrides'
) {
return true;
}
return false;
},
propertiesToStringify: flatFieldMetadataEntityJsonbProperties,
propertiesToStringify: FLAT_FIELD_METADATA_JSONB_PROPERTIES,
};
const fromCompare = transformMetadataForComparison(
from,
compareFieldMetadataOptions,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
to,
compareFieldMetadataOptions,
transformMetadataForComparisonParameters,
);
const flatFieldMetadataDifferences = diff(fromCompare, toCompare);
@@ -104,7 +97,7 @@ export const compareTwoFlatFieldMetadata = ({
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatFieldMetadataPropertiesToCompare;
const isJsonb = flatFieldMetadataEntityJsonbProperties.includes(
const isJsonb = FLAT_FIELD_METADATA_JSONB_PROPERTIES.includes(
property as FlatFieldMetadataEntityJsonbProperties,
);
@@ -4,30 +4,28 @@ import {
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import {
fieldMetadataStandardOverridesProperties,
type FieldMetadataStandardOverridesProperties,
} from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type';
import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant';
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 {
flatFieldMetadataPropertiesToCompare,
type FlatFieldMetadataPropertiesToCompare,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import {} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-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 {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
const fieldMetadataEditableProperties =
flatFieldMetadataPropertiesToCompare.filter(
FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.filter(
(
property,
): property is Exclude<
@@ -36,20 +34,20 @@ const fieldMetadataEditableProperties =
> => property !== 'standardOverrides',
);
type FromUpdateFieldInputToFlatFieldMetadataToUpdateArgs = {
type FromUpdateFieldInputToFlatFieldMetadataArgs = {
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
updateFieldInput: UpdateFieldInput;
};
export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({
export const fromUpdateFieldInputToFlatFieldMetadata = ({
existingFlatObjectMetadataMaps,
updateFieldInput: rawUpdateFieldInput,
}: FromUpdateFieldInputToFlatFieldMetadataToUpdateArgs): FieldInputTranspilationResult<FlatFieldMetadata> => {
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadata> => {
const updateFieldInputInformalProperties =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateFieldInput,
['objectMetadataId', 'id'],
);
const updatedEditableFields = extractAndSanitizeObjectStringFields(
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdateFieldInput,
fieldMetadataEditableProperties,
);
@@ -94,16 +92,14 @@ export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({
),
};
}
const isStandardField =
relatedFlatFieldMetadata.standardId !== null &&
!relatedFlatFieldMetadata.isCustom;
if (isStandardField) {
const invalidUpdatedProperties = Object.keys(updatedEditableFields).filter(
(property) =>
fieldMetadataStandardOverridesProperties.includes(
property as FieldMetadataStandardOverridesProperties,
),
if (isStandardMetadata(relatedFlatFieldMetadata)) {
const invalidUpdatedProperties = Object.keys(
updatedEditableFieldProperties,
).filter((property) =>
FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes(
property as FieldMetadataStandardOverridesProperties,
),
);
if (invalidUpdatedProperties.length > 0) {
@@ -117,15 +113,16 @@ export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({
}
const updatedStandardFlatFieldMetadata =
fieldMetadataStandardOverridesProperties.reduce((acc, property) => {
const isPropertyUpdated = updatedEditableFields[property] !== undefined;
FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce((acc, property) => {
const isPropertyUpdated =
updatedEditableFieldProperties[property] !== undefined;
return {
...acc,
standardOverrides: {
...acc.standardOverrides,
...(isPropertyUpdated
? { [property]: updatedEditableFields[property] }
? { [property]: updatedEditableFieldProperties[property] }
: {}),
},
};
@@ -139,12 +136,13 @@ export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({
const updatedFlatFieldMetadata = fieldMetadataEditableProperties.reduce(
(acc, property) => {
const isPropertyUpdated = updatedEditableFields[property] !== undefined;
const isPropertyUpdated =
updatedEditableFieldProperties[property] !== undefined;
return {
...acc,
...(isPropertyUpdated
? { [property]: updatedEditableFields[property] }
? { [property]: updatedEditableFieldProperties[property] }
: {}),
};
},
@@ -0,0 +1,13 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
export const isFlatFieldMetadataNameSyncedWithLabel = (
flatFieldMetadata: Pick<
FlatFieldMetadata,
'name' | 'isLabelSyncedWithName' | 'label'
>,
) => {
const computedName = computeMetadataNameFromLabel(flatFieldMetadata.label);
return flatFieldMetadata.name === computedName;
};
@@ -8,8 +8,6 @@ import {
import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
// Remark: This is duplicated with api metadata transpilers exceptions handlers
// We might wanna have an NotValidatedFlatFieldMetadata and NotValidatedFlatObjectMetadata, in order to avoid
export const validateRelationFlatFieldMetadata = async ({
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: {
@@ -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;
};
@@ -0,0 +1,8 @@
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
export const OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES = [
'labelSingular',
'labelPlural',
'description',
'icon',
] as const satisfies FlatObjectMetadataPropertiesToCompare[];
@@ -8,14 +8,17 @@ import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine
import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-field-from-flat-object-metadata-maps-or-throw.util';
import { deleteObjectFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-object-from-flat-object-metadata-maps-or-throw.util';
import { getSubFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-or-throw.util';
import { replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util';
import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromCreateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util';
import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util';
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
import { fromUpdateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
@@ -33,6 +36,86 @@ export class ObjectMetadataServiceV2 {
private readonly flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService,
) {}
async updateOne({
updateObjectInput,
workspaceId,
}: {
workspaceId: string;
updateObjectInput: UpdateOneObjectInput;
}): Promise<ObjectMetadataDTO> {
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const optimisticallyUpdatedFlatObjectMetadata =
fromUpdateObjectInputToFlatObjectMetadata({
existingFlatObjectMetadataMaps,
updateObjectInput,
});
const validationErrors =
this.flatObjectMetadataValidatorService.validateFlatObjectMetadataUpdate({
existingFlatObjectMetadataMaps,
updatedFlatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata,
});
if (validationErrors.length > 0) {
throw new MultipleMetadataValidationErrors(
validationErrors,
'Multiple validation errors occurred while updating object',
);
}
try {
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [optimisticallyUpdatedFlatObjectMetadata.id],
});
const toFlatObjectMetadataMaps =
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata,
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
});
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: false,
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
} catch {
throw new ObjectMetadataException(
'Workspace migration failed to run',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const updatedFlatObjectMetadata =
recomputedFlatObjectMetadataMaps.byId[
optimisticallyUpdatedFlatObjectMetadata.id
];
if (!isDefined(updatedFlatObjectMetadata)) {
throw new ObjectMetadataException(
'Updated object metadata not found in recomputed cache',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
return fromFlatObjectMetadataToObjectMetadataDto(updatedFlatObjectMetadata);
}
async deleteOne({
deleteObjectInput,
workspaceId,
@@ -189,7 +272,7 @@ export class ObjectMetadataServiceV2 {
if (!isDefined(createdFlatObjectMetadata)) {
throw new ObjectMetadataException(
'Fail to find just created object metadata',
'Created object metadata not found in recomputed cache',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
@@ -8,6 +8,8 @@ import {
Resolver,
} from '@nestjs/graphql';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
@@ -25,6 +27,7 @@ import {
type UpdateObjectPayload,
} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
@@ -42,6 +45,8 @@ export class ObjectMetadataResolver {
constructor(
private readonly objectMetadataService: ObjectMetadataService,
private readonly beforeUpdateOneObject: BeforeUpdateOneObject<UpdateObjectPayload>,
private readonly featureFlagService: FeatureFlagService,
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
) {}
@ResolveField(() => String, { nullable: true })
@@ -112,15 +117,35 @@ export class ObjectMetadataResolver {
@UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL))
@Mutation(() => ObjectMetadataDTO)
async updateOneObject(
@Args('input') input: UpdateOneObjectInput,
@Args('input') updateObjectInput: UpdateOneObjectInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
@Context() context: I18nContext,
) {
try {
const updatedInput = (await this.beforeUpdateOneObject.run(input, {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
locale: context.req.locale,
})) as UpdateOneObjectInput;
);
if (isWorkspaceMigrationV2Enabled) {
try {
return await this.objectMetadataServiceV2.updateOne({
updateObjectInput,
workspaceId,
});
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
}
}
try {
const updatedInput = (await this.beforeUpdateOneObject.run(
updateObjectInput,
{
workspaceId,
locale: context.req.locale,
},
)) as UpdateOneObjectInput;
return await this.objectMetadataService.updateOneObject(
updatedInput,
@@ -110,7 +110,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: createObjectInput.workspaceId,
});
// Since V2 returns FlatObjectMetadata, we need to fetch the created entity
const createdObjectMetadata = await this.objectMetadataRepository.findOne(
{
where: {
@@ -288,7 +287,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}
public async updateOneObject(
input: UpdateOneObjectInput,
updateObjectInput: UpdateOneObjectInput,
workspaceId: string,
): Promise<ObjectMetadataEntity> {
const mainDataSource =
@@ -306,14 +305,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{ workspaceId },
);
const inputId = input.id;
const inputId = updateObjectInput.id;
const inputPayload = {
...input.update,
...(isDefined(input.update.labelSingular)
? { labelSingular: capitalize(input.update.labelSingular) }
...updateObjectInput.update,
...(isDefined(updateObjectInput.update.labelSingular)
? {
labelSingular: capitalize(updateObjectInput.update.labelSingular),
}
: {}),
...(isDefined(input.update.labelPlural)
? { labelPlural: capitalize(input.update.labelPlural) }
...(isDefined(updateObjectInput.update.labelPlural)
? { labelPlural: capitalize(updateObjectInput.update.labelPlural) }
: {}),
};
@@ -0,0 +1,4 @@
import { type OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
export type ObjectMetadataStandardOverridesProperties =
(typeof OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES)[number];
@@ -0,0 +1,10 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const isStandardMetadata = (
metadata:
| Pick<FlatObjectMetadata, 'standardId' | 'isCustom'>
| Pick<FlatFieldMetadata, 'standardId' | 'isCustom'>,
) => !metadata.isCustom && isDefined(metadata.standardId);