Refactor builder to embed Object validation create/delete/update (#13934)

# Introduction
Moving validation directly in the builder that has the perfect
granularity to do it.
When importing we won't have to infer and dispatch on the operation
nature ( update delete create ) and validate accordingly

## Objects
Only migrated object validation for the moment even though create object
involves a validate flat field metadata creation call too

## TODO
- improve `otherFlatObjectMetadataMapsToValidate` naming too vague

## Next
- handle fields validation within fields actions build
- Unit test coverage validation issue on builder and validate
- integration test plugging with new feature flag

## Manual tested
- Update
- Delete
- Create TODO
This commit is contained in:
Paul Rastoin
2025-08-18 11:16:16 +02:00
committed by GitHub
parent 18b2a68b69
commit 6629092645
51 changed files with 3800 additions and 617 deletions
@@ -2,14 +2,13 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { MultipleMetadataValidationErrors } from 'src/engine/core-modules/error/multiple-metadata-validation-errors';
import { EMPTY_FLAT_OBJECT_METADATA_MAPS } from 'src/engine/metadata-modules/flat-object-metadata-maps/constant/empty-flat-object-metadata-maps.constant';
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 { 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 { getSubFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps.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';
@@ -24,16 +23,13 @@ import {
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
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';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ObjectMetadataServiceV2 {
constructor(
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly workspaceMigrationBuilderV2: WorkspaceMigrationBuilderV2Service,
private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service,
private readonly flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async updateOne({
@@ -56,43 +52,29 @@ export class ObjectMetadataServiceV2 {
updateObjectInput,
});
const validationErrors =
this.flatObjectMetadataValidatorService.validateFlatObjectMetadataUpdate({
existingFlatObjectMetadataMaps,
updatedFlatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata,
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [optimisticallyUpdatedFlatObjectMetadata.id],
});
const toFlatObjectMetadataMaps =
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata,
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
});
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({
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: false,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
},
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
} catch {
throw new ObjectMetadataException(
'Workspace migration failed to run',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
errorMessage:
'Multiple validation errors occurred while updating object',
},
);
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
@@ -137,67 +119,51 @@ export class ObjectMetadataServiceV2 {
});
const { id: objectMetadataToDeleteId } = flatObjectMetadataToDelete;
const flatObjectDeleteValidationErrors =
this.flatObjectMetadataValidatorService.validateFlatObjectMetadataDeletion(
{
existingFlatObjectMetadataMaps,
objectMetadataToDeleteId,
},
);
if (flatObjectDeleteValidationErrors.length > 0) {
throw new MultipleMetadataValidationErrors(
flatObjectDeleteValidationErrors,
'Multiple validation errors occurred while deleting object',
);
}
try {
const impactedObjectMetadataIds = Array.from(
new Set(
flatFieldMetadatasToDelete.map(
(flatFieldMetadata) => flatFieldMetadata.objectMetadataId,
),
const impactedObjectMetadataIds = Array.from(
new Set(
flatFieldMetadatasToDelete.map(
(flatFieldMetadata) => flatFieldMetadata.objectMetadataId,
),
),
);
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: impactedObjectMetadataIds,
});
const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete
.filter(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId !==
objectMetadataToDeleteId,
)
.reduce(
(flatObjectMetadataMaps, flatFieldMetadata) =>
deleteFieldFromFlatObjectMetadataMapsOrThrow({
fieldMetadataId: flatFieldMetadata.id,
objectMetadataId: flatFieldMetadata.objectMetadataId,
flatObjectMetadataMaps,
}),
deleteObjectFromFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
objectMetadataId: objectMetadataToDeleteId,
}),
);
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: impactedObjectMetadataIds,
});
const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete
.filter(
(flatFieldMetadataToDelete) =>
flatFieldMetadataToDelete.objectMetadataId !==
objectMetadataToDeleteId,
)
.reduce(
(flatObjectMetadataMaps, flatFieldMetadata) =>
deleteFieldFromFlatObjectMetadataMapsOrThrow({
fieldMetadataId: flatFieldMetadata.id,
objectMetadataId: flatFieldMetadata.objectMetadataId,
flatObjectMetadataMaps,
}),
deleteObjectFromFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
objectMetadataId: objectMetadataToDeleteId,
}),
);
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: true,
buildOptions: {
inferDeletionFromMissingObjectFieldIndex: true,
isSystemBuild: false,
},
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
} catch {
throw new ObjectMetadataException(
'Workspace migration failed to run',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
errorMessage:
'Multiple validation errors occurred while deleting object',
},
);
return fromFlatObjectMetadataToObjectMetadataDto(
flatObjectMetadataToDelete,
@@ -224,41 +190,29 @@ export class ObjectMetadataServiceV2 {
workspaceId,
});
const validationErrors =
await this.flatObjectMetadataValidatorService.validateFlatObjectMetadataCreation(
{
existingFlatObjectMetadataMaps,
flatObjectMetadataToValidate: flatObjectMetadataToCreate,
workspaceId,
},
);
const fromFlatObjectMetadataMaps =
getSubFlatObjectMetadataMaps({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [flatObjectMetadataToCreate.id],
}) ?? EMPTY_FLAT_OBJECT_METADATA_MAPS;
if (validationErrors.length > 0) {
throw new MultipleMetadataValidationErrors(
validationErrors,
'Multiple validation errors occurred while creating object',
);
}
try {
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
fromFlatObjectMetadataMaps: EMPTY_FLAT_OBJECT_METADATA_MAPS,
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps:
addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: EMPTY_FLAT_OBJECT_METADATA_MAPS,
flatObjectMetadata: flatObjectMetadataToCreate,
}),
inferDeletionFromMissingObjectFieldIndex: false,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
},
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
} catch {
throw new ObjectMetadataException(
'Workspace migration failed to run',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
errorMessage:
'Multiple validation errors occurred while creating object',
},
);
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
@@ -43,8 +43,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module';
import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
@@ -69,8 +68,7 @@ import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/w
WorkspaceMetadataCacheModule,
WorkspaceDataSourceModule,
FeatureFlagModule,
WorkspaceMigrationBuilderV2Module,
WorkspaceMigrationRunnerV2Module,
WorkspaceMigrationV2Module,
],
services: [
ObjectMetadataService,