FieldMetadataServiceV2.updateOne (#13784)

# Introduction
Implementing the `updateOne` service handler following workspace
migration v2 style
This commit is contained in:
Paul Rastoin
2025-08-12 12:05:53 +02:00
committed by GitHub
parent 37a0f2726e
commit d4b5ed93a3
14 changed files with 565 additions and 93 deletions
@@ -1,11 +1,16 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
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 {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
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';
@@ -14,7 +19,7 @@ import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/f
export class FlatFieldMetadataTypeValidatorService {
constructor(private readonly featureFlagService: FeatureFlagService) {}
public readonly FIELD_METADATA_TYPE_VALIDATOR_HASHMAP: FlatFieldMetadataTypeValidator =
private readonly FIELD_METADATA_TYPE_VALIDATOR_HASHMAP: FlatFieldMetadataTypeValidator =
{
ACTOR: async (_args) => {
return [];
@@ -126,4 +131,36 @@ export class FlatFieldMetadataTypeValidatorService {
return [];
},
};
public async validateFlatFieldMetadataTypeSpecificities<
T extends FieldMetadataType = FieldMetadataType,
>({
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate,
workspaceId,
otherFlatObjectMetadataMapsToValidate,
}: ValidateOneFieldMetadataArgs<T>): Promise<
FailedFlatFieldMetadataValidationExceptions[]
> {
const fieldMetadataTypeValidator =
this.FIELD_METADATA_TYPE_VALIDATOR_HASHMAP[
flatFieldMetadataToValidate.type
];
if (!isDefined(fieldMetadataTypeValidator)) {
return [
new FieldMetadataException(
'Unsupported field metadata type',
FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION,
),
];
}
return await fieldMetadataTypeValidator({
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate,
workspaceId,
otherFlatObjectMetadataMapsToValidate,
});
}
}
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { type FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -11,8 +11,14 @@ import {
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 { 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 { 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 { 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 {
ObjectMetadataException,
ObjectMetadataExceptionCode,
@@ -21,7 +27,7 @@ import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
export type ValidateOneFieldMetadataArgs<
@@ -39,6 +45,143 @@ export class FlatFieldMetadataValidatorService {
private readonly flatFieldMetadataTypeValidatorService: FlatFieldMetadataTypeValidatorService,
) {}
async validateFlatFieldMetadataUpdate<T extends FieldMetadataType>({
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: updatedFlatFieldMetadata,
workspaceId,
}: ValidateOneFieldMetadataArgs<T>): Promise<
FailedFlatFieldMetadataValidationExceptions[]
> {
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
const flatObjectMetadataWithFlatFieldMaps =
existingFlatObjectMetadataMaps.byId[
updatedFlatFieldMetadata.objectMetadataId
];
if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) {
return [
new FieldMetadataException(
'field metadata to update object metadata not found',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
),
];
}
if (
!isDefined(
flatObjectMetadataWithFlatFieldMaps.labelIdentifierFieldMetadataId,
)
) {
errors.push(
new FieldMetadataException(
'Label identifier field metadata id does not exist',
FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND,
),
);
}
const existingFlatFieldMetadataToUpdate =
flatObjectMetadataWithFlatFieldMaps.fieldsById[
updatedFlatFieldMetadata.id
];
if (!isDefined(existingFlatFieldMetadataToUpdate)) {
errors.push(
new FieldMetadataException(
'field metadata to update not found',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
),
);
return errors;
}
const updates = compareTwoFlatFieldMetadata({
from: existingFlatFieldMetadataToUpdate,
to: updatedFlatFieldMetadata,
});
if (
isFlatFieldMetadataEntityOfType(
updatedFlatFieldMetadata,
FieldMetadataType.RELATION,
) ||
isFlatFieldMetadataEntityOfType(
updatedFlatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
const relationEditableFields = [
'label',
'description',
'isActive',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
const relationNonEditableUpdatedProperties = updates.flatMap(
({ property }) =>
!relationEditableFields.includes(
property as (typeof relationEditableFields)[number],
)
? property
: [],
);
if (relationNonEditableUpdatedProperties.length > 0) {
errors.push(
new FieldMetadataException(
`Forbidden updated properties for relation field metadata: ${relationNonEditableUpdatedProperties.join(', ')}`,
FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
),
);
}
}
const flatObjectMetadata =
fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata(
flatObjectMetadataWithFlatFieldMaps,
);
if (updates.some((update) => update.property === 'name')) {
errors.push(
...[
validateMetadataName(updatedFlatFieldMetadata.name),
validateFlatFieldMetadataNameAvailability({
name: updatedFlatFieldMetadata.name,
flatObjectMetadata: flatObjectMetadata,
}),
].filter(isDefined),
);
}
if (updatedFlatFieldMetadata.isLabelSyncedWithName) {
const computedName = computeMetadataNameFromLabel(
updatedFlatFieldMetadata.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 =
await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities(
{
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: updatedFlatFieldMetadata,
workspaceId,
},
);
if (fieldMetadataTypeValidationErrors.length > 0) {
errors.push(...fieldMetadataTypeValidationErrors);
}
return errors;
}
validateFlatFieldMetadataDeletion({
existingFlatObjectMetadataMaps,
flatFieldMetadataToDelete,
@@ -98,7 +241,7 @@ export class FlatFieldMetadataValidatorService {
return errors;
}
async validateOneFlatFieldMetadata<
async validateOneFlatFieldMetadataCreation<
T extends FieldMetadataType = FieldMetadataType,
>({
existingFlatObjectMetadataMaps,
@@ -140,7 +283,7 @@ export class FlatFieldMetadataValidatorService {
const failedNameAvailabilityValidation =
validateFlatFieldMetadataNameAvailability({
name: flatFieldMetadataToValidate.name,
objectMetadata: parentFlatObjectMetadata,
flatObjectMetadata: parentFlatObjectMetadata,
});
if (isDefined(failedNameAvailabilityValidation)) {
@@ -163,43 +306,26 @@ export class FlatFieldMetadataValidatorService {
}
}
try {
validateMetadataNameOrThrow(flatFieldMetadataToValidate.name);
} catch (error) {
errors.push(
new FieldMetadataException(
error.message,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: error.userFriendlyMessage,
},
),
);
const nameValidationResult = validateMetadataName(
flatFieldMetadataToValidate.name,
);
if (isDefined(nameValidationResult)) {
errors.push(nameValidationResult);
}
const fieldMetadataTypeValidator =
this.flatFieldMetadataTypeValidatorService
.FIELD_METADATA_TYPE_VALIDATOR_HASHMAP[
flatFieldMetadataToValidate.type
];
if (!isDefined(fieldMetadataTypeValidator)) {
errors.push(
new FieldMetadataException(
'Unsupported field metadata type',
FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION,
),
);
} else {
const fieldMetadataTypeValidatorExceptions =
await fieldMetadataTypeValidator({
const fieldMetadataTypeValidationErrors =
await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities(
{
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate,
workspaceId,
otherFlatObjectMetadataMapsToValidate,
});
},
);
errors.push(...fieldMetadataTypeValidatorExceptions);
if (fieldMetadataTypeValidationErrors.length > 0) {
errors.push(...fieldMetadataTypeValidationErrors);
}
return errors;