Workspace migration v2 builder embed field metadata validation (#13960)

# Introduction
Following https://github.com/twentyhq/twenty/pull/13934
Finalizing object migration
Also refactored existing to follow same for const loop pattern
This commit is contained in:
Paul Rastoin
2025-08-18 16:00:58 +02:00
committed by GitHub
parent 578a2f4e6b
commit 3d3191425e
24 changed files with 771 additions and 571 deletions
@@ -8,7 +8,6 @@ import {
} from 'twenty-shared/utils';
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 { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
@@ -18,8 +17,6 @@ import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FlatFieldMetadataValidatorService } 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';
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';
@@ -28,7 +25,6 @@ import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-mod
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';
import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { addFlatFieldMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps.util';
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';
@@ -44,7 +40,6 @@ export class FieldMetadataServiceV2 {
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly flatFieldMetadataValidatorService: FlatFieldMetadataValidatorService,
) {}
async createOne({
@@ -89,23 +84,6 @@ export class FieldMetadataServiceV2 {
existingFlatObjectMetadataMaps,
});
const validationErrors = flatFieldMetadatasToDelete.flatMap(
(flatFieldMetadataToDelete) =>
this.flatFieldMetadataValidatorService.validateFlatFieldMetadataDeletion(
{
existingFlatObjectMetadataMaps,
flatFieldMetadataToDelete,
},
),
);
if (validationErrors.length > 0) {
throw new MultipleMetadataValidationErrors(
validationErrors,
'Multiple validation errors occurred while deleting field',
);
}
const flatObjectMetadataMapsWithImpactedObject =
getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
@@ -219,22 +197,6 @@ export class FieldMetadataServiceV2 {
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',
);
}
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [
@@ -306,56 +268,6 @@ export class FieldMetadataServiceV2 {
({ result }) => result,
);
const allValidationErrors: FailedFlatFieldMetadataValidationExceptions[] =
[];
let optimisticFlatObjectMetadataMaps = structuredClone(
existingFlatObjectMetadataMaps,
);
for (const flatFieldMetadataToCreate of flatFieldMetadatasToCreate) {
const otherFlatObjectMetadataMapsToValidate =
this.computeOtherFlatObjectMetadataMapsToValidate({
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatFieldMetadatasToCreate,
flatFieldMetadataToCreate,
});
const validationErrors =
await this.flatFieldMetadataValidatorService.validateFlatFieldMetadataCreation(
{
existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatFieldMetadataToValidate: flatFieldMetadataToCreate,
workspaceId,
otherFlatObjectMetadataMapsToValidate,
},
);
if (validationErrors.length > 0) {
allValidationErrors.push(...validationErrors);
continue;
}
try {
optimisticFlatObjectMetadataMaps =
addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatFieldMetadata: flatFieldMetadataToCreate,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
});
} catch {
throw new FieldMetadataException(
'Optimistic cache manipulation failed, should never occur',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
}
if (allValidationErrors.length > 0) {
throw new MultipleMetadataValidationErrors(
allValidationErrors,
'Multiple validation errors occurred while creating field',
);
}
const impactedObjectMetadataIds = Array.from(
new Set(
flatFieldMetadatasToCreate.map(
@@ -369,7 +281,7 @@ export class FieldMetadataServiceV2 {
objectMetadataIds: impactedObjectMetadataIds,
});
const toFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: impactedObjectMetadataIds,
});
@@ -387,7 +299,6 @@ export class FieldMetadataServiceV2 {
},
);
// In the best of the world could consume runner returned value instead of searching in db here
return this.fieldMetadataRepository.find({
where: {
name: In(
@@ -6,17 +6,15 @@ import { isDefined } from 'twenty-shared/utils';
import { FlatFieldMetadataValidatorService } 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 { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
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 { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util';
import { getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-out-of-flat-field-metadatas.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 { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util';
import { computeRelationTargetFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/compute-relation-target-flat-object-metadata-maps.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';
@@ -26,43 +24,7 @@ import {
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
import { WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
const computeRelationTargetFlatObjectMetadataMapsForValidation = ({
flatFieldMetadata,
flatObjectMetadataMaps,
}: {
flatFieldMetadata: FlatFieldMetadata;
flatObjectMetadataMaps: FlatObjectMetadataMaps;
}): FlatObjectMetadataMaps | undefined => {
if (
!isFlatFieldMetadataEntityOfType(
flatFieldMetadata,
FieldMetadataType.RELATION,
) &&
!isFlatFieldMetadataEntityOfType(
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
return undefined;
}
const relationTargetFlatFieldMetadata =
findRelationFlatFieldMetadataTargetFlatFieldMetadata({
flatFieldMetadata,
flatObjectMetadataMaps,
});
if (!isDefined(relationTargetFlatFieldMetadata)) {
return undefined;
}
return getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas({
flatObjectMetadataMaps,
flatFieldMetadatas: [relationTargetFlatFieldMetadata],
});
};
import { WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service';
@Injectable()
export class FlatObjectMetadataValidatorService {
@@ -215,20 +177,29 @@ export class FlatObjectMetadataValidatorService {
});
for (const flatFieldMetadataToValidate of flatObjectMetadataToValidate.flatFieldMetadatas) {
const relationTargetFlatObjectMetadataMaps =
(isFlatFieldMetadataEntityOfType(
flatFieldMetadataToValidate,
FieldMetadataType.RELATION,
) ||
isFlatFieldMetadataEntityOfType(
flatFieldMetadataToValidate,
FieldMetadataType.MORPH_RELATION,
)) &&
isDefined(otherFlatObjectMetadataMapsToValidate)
? computeRelationTargetFlatObjectMetadataMaps({
flatFieldMetadata: flatFieldMetadataToValidate,
flatObjectMetadataMaps: otherFlatObjectMetadataMapsToValidate,
})
: undefined;
const flatFieldValidatorErrors =
await this.flatFieldMetadataValidatorService.validateFlatFieldMetadataCreation(
{
existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatFieldMetadataToValidate,
workspaceId: flatObjectMetadataToValidate.workspaceId,
otherFlatObjectMetadataMapsToValidate: isDefined(
otherFlatObjectMetadataMapsToValidate,
)
? computeRelationTargetFlatObjectMetadataMapsForValidation({
flatFieldMetadata: flatFieldMetadataToValidate,
flatObjectMetadataMaps: otherFlatObjectMetadataMapsToValidate,
})
: undefined,
otherFlatObjectMetadataMapsToValidate:
relationTargetFlatObjectMetadataMaps,
},
);
@@ -0,0 +1,32 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata.util';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-out-of-flat-field-metadatas.util';
export const computeRelationTargetFlatObjectMetadataMaps = ({
flatFieldMetadata,
flatObjectMetadataMaps,
}: {
flatFieldMetadata: FlatFieldMetadata<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>;
flatObjectMetadataMaps: FlatObjectMetadataMaps;
}): FlatObjectMetadataMaps | undefined => {
const relationTargetFlatFieldMetadata =
findRelationFlatFieldMetadataTargetFlatFieldMetadata({
flatFieldMetadata,
flatObjectMetadataMaps,
});
if (!isDefined(relationTargetFlatFieldMetadata)) {
return undefined;
}
return getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas({
flatObjectMetadataMaps,
flatFieldMetadatas: [relationTargetFlatFieldMetadata],
});
};