Files
twenty/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts
T
Paul Rastoin 6629092645 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
2025-08-18 11:16:16 +02:00

237 lines
9.6 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
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 { 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,
} 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 { 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 workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async updateOne({
updateObjectInput,
workspaceId,
}: {
workspaceId: string;
updateObjectInput: UpdateOneObjectInput;
}): Promise<ObjectMetadataDTO> {
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const optimisticallyUpdatedFlatObjectMetadata =
fromUpdateObjectInputToFlatObjectMetadata({
existingFlatObjectMetadataMaps,
updateObjectInput,
});
const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [optimisticallyUpdatedFlatObjectMetadata.id],
});
const toFlatObjectMetadataMaps =
replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({
flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata,
flatObjectMetadataMaps: fromFlatObjectMetadataMaps,
});
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
},
workspaceId,
errorMessage:
'Multiple validation errors occurred while updating object',
},
);
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,
}: {
deleteObjectInput: DeleteOneObjectInput;
workspaceId: string;
}): Promise<ObjectMetadataDTO> {
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const { flatFieldMetadatasToDelete, flatObjectMetadataToDelete } =
fromDeleteObjectInputToFlatFieldMetadatasToDelete({
deleteObjectInput,
existingFlatObjectMetadataMaps,
});
const { id: objectMetadataToDeleteId } = flatObjectMetadataToDelete;
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,
}),
);
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
buildOptions: {
inferDeletionFromMissingObjectFieldIndex: true,
isSystemBuild: false,
},
workspaceId,
errorMessage:
'Multiple validation errors occurred while deleting object',
},
);
return fromFlatObjectMetadataToObjectMetadataDto(
flatObjectMetadataToDelete,
);
}
async createOne({
createObjectInput,
workspaceId,
}: {
createObjectInput: Omit<CreateObjectInput, 'workspaceId'>;
workspaceId: string;
}): Promise<FlatObjectMetadata> {
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const flatObjectMetadataToCreate =
fromCreateObjectInputToFlatObjectMetadata({
createObjectInput,
workspaceId,
});
const fromFlatObjectMetadataMaps =
getSubFlatObjectMetadataMaps({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: [flatObjectMetadataToCreate.id],
}) ?? EMPTY_FLAT_OBJECT_METADATA_MAPS;
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps:
addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: EMPTY_FLAT_OBJECT_METADATA_MAPS,
flatObjectMetadata: flatObjectMetadataToCreate,
}),
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingObjectFieldIndex: false,
},
workspaceId,
errorMessage:
'Multiple validation errors occurred while creating object',
},
);
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const createdFlatObjectMetadata =
recomputedFlatObjectMetadataMaps.byId[flatObjectMetadataToCreate.id];
if (!isDefined(createdFlatObjectMetadata)) {
throw new ObjectMetadataException(
'Created object metadata not found in recomputed cache',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return createdFlatObjectMetadata;
}
}