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
@@ -4,8 +4,20 @@ 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';
export const fieldMetadataStandardOverridesProperties = [
'label',
'description',
'icon',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
export type FieldMetadataStandardOverridesProperties =
(typeof fieldMetadataStandardOverridesProperties)[number];
@ObjectType('StandardOverrides')
export class FieldStandardOverridesDTO {
export class FieldStandardOverridesDTO
implements Partial<Record<FieldMetadataStandardOverridesProperties, unknown>>
{
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
@@ -87,6 +87,23 @@ export class FieldMetadataResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
@Context() context: I18nContext,
) {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
try {
return await this.fieldMetadataServiceV2.updateOne({
updateFieldInput: { ...input.update, id: input.id },
workspaceId,
});
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
}
}
try {
const updatedInput = (await this.beforeUpdateOneField.run(input, {
workspaceId,
@@ -26,7 +26,7 @@ import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-module
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@@ -33,7 +33,7 @@ import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metad
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils';
type ValidateFieldMetadataArgs = {
fieldMetadataType: FieldMetadataType;
@@ -11,6 +11,7 @@ import { In, Repository } from 'typeorm';
import { MultipleMetadataValidationErrors } from 'src/engine/core-modules/error/multiple-metadata-validation-errors';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
@@ -21,6 +22,7 @@ import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/met
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
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 { fromUpdateFieldInputToFlatFieldMetadataToUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata-to-update.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';
@@ -29,6 +31,7 @@ import { addFlatFieldMetadataInFlatObjectMetadataMaps } from 'src/engine/metadat
import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-field-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 { getSubFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps.util';
import { replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
@@ -105,33 +108,40 @@ export class FieldMetadataServiceV2 {
);
}
const flatObjectMetadataMapsWithImpactedObject =
getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: flatFieldMetadatasToDelete.map(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId,
),
try {
const flatObjectMetadataMapsWithImpactedObject =
getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: flatFieldMetadatasToDelete.map(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId,
),
});
const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete.reduce(
(flatObjectMetadataMaps, flatFieldMetadataToDelete) =>
deleteFieldFromFlatObjectMetadataMapsOrThrow({
fieldMetadataId: flatFieldMetadataToDelete.id,
flatObjectMetadataMaps,
objectMetadataId: flatFieldMetadataToDelete.objectMetadataId,
}),
flatObjectMetadataMapsWithImpactedObject,
);
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: true,
workspaceId,
});
const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete.reduce(
(flatObjectMetadataMaps, flatFieldMetadataToDelete) =>
deleteFieldFromFlatObjectMetadataMapsOrThrow({
fieldMetadataId: flatFieldMetadataToDelete.id,
flatObjectMetadataMaps,
objectMetadataId: flatFieldMetadataToDelete.objectMetadataId,
}),
flatObjectMetadataMapsWithImpactedObject,
);
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: true,
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
} catch {
throw new FieldMetadataException(
'Workspace migration failed to run',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
// TODO to be discussed with coco regarding storing dates in flat in order to build dtos
// return flatFieldMetadatasToDelete[0];
@@ -188,6 +198,82 @@ export class FieldMetadataServiceV2 {
});
}
async updateOne({
updateFieldInput,
workspaceId,
}: {
updateFieldInput: UpdateFieldInput;
workspaceId: string;
}): Promise<FieldMetadataEntity> {
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{ workspaceId },
);
const inputTranspilationResult =
fromUpdateFieldInputToFlatFieldMetadataToUpdate({
existingFlatObjectMetadataMaps,
updateFieldInput,
});
if (inputTranspilationResult.status === 'fail') {
throw inputTranspilationResult.error;
}
const optimisticiallyUpdatedFlatFieldMetadata =
inputTranspilationResult.result;
const validationErrors =
await this.flatFieldMetadataValidatorService.validateFlatFieldMetadataUpdate(
{
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: optimisticiallyUpdatedFlatFieldMetadata,
workspaceId,
},
);
if (validationErrors.length > 0) {
throw new MultipleMetadataValidationErrors(
validationErrors,
'Multiple validation errors occurred while updating field',
);
}
try {
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [
optimisticiallyUpdatedFlatFieldMetadata.objectMetadataId,
],
});
const toFlatObjectMetadataMaps =
replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
flatFieldMetadata: optimisticiallyUpdatedFlatFieldMetadata,
});
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: false,
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
return this.fieldMetadataRepository.findOneOrFail({
where: {
id: optimisticiallyUpdatedFlatFieldMetadata.id,
workspaceId,
},
});
} catch {
throw new FieldMetadataException(
'Workspace migration failed to run',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
}
async createMany({
fieldMetadataInputs,
workspaceId,
@@ -240,7 +326,7 @@ export class FieldMetadataServiceV2 {
});
const validationErrors =
await this.flatFieldMetadataValidatorService.validateOneFlatFieldMetadata(
await this.flatFieldMetadataValidatorService.validateOneFlatFieldMetadataCreation(
{
existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatFieldMetadataToValidate: flatFieldMetadataToCreate,