ObjectMetadataServiceV2.deleteOne (#13871)

# Introduction
- Implementing the delete one for the new object metadata service v2.
- Handling relation fields and fields in the first place to finally
remove the object
- puting back updatedAt and createdAt in flat metadatas
- duplicate criteria addition for flat object
- removing datasource id from object metadata dto
This commit is contained in:
Paul Rastoin
2025-08-13 11:20:17 +02:00
committed by GitHub
parent 98e1a325a8
commit e8e81957a6
44 changed files with 863 additions and 473 deletions
@@ -35,9 +35,6 @@ export class ObjectMetadataDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType)
dataSourceId: string;
@Field()
nameSingular: string;
@@ -1,12 +1,21 @@
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 { 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';
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.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 {
ObjectMetadataException,
ObjectMetadataExceptionCode,
@@ -24,11 +33,99 @@ export class ObjectMetadataServiceV2 {
private readonly flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService,
) {}
async createOne({
objectMetadataInput,
async deleteOne({
deleteObjectInput,
workspaceId,
}: {
objectMetadataInput: Omit<CreateObjectInput, '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 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 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({
fromFlatObjectMetadataMaps,
toFlatObjectMetadataMaps,
inferDeletionFromMissingObjectFieldIndex: true,
workspaceId,
});
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
} catch {
throw new ObjectMetadataException(
'Workspace migration failed to run',
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
return fromFlatObjectMetadataToObjectMetadataDto(
flatObjectMetadataToDelete,
);
}
async createOne({
createObjectInput,
workspaceId,
}: {
createObjectInput: Omit<CreateObjectInput, 'workspaceId'>;
workspaceId: string;
}): Promise<FlatObjectMetadata> {
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
@@ -40,7 +137,7 @@ export class ObjectMetadataServiceV2 {
const flatObjectMetadataToCreate =
fromCreateObjectInputToFlatObjectMetadata({
objectMetadataInput,
createObjectInput,
workspaceId,
});
@@ -80,6 +177,23 @@ export class ObjectMetadataServiceV2 {
);
}
return flatObjectMetadataToCreate; // TODO retrieve from cache
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId,
},
);
const createdFlatObjectMetadata =
recomputedFlatObjectMetadataMaps.byId[flatObjectMetadataToCreate.id];
if (!isDefined(createdFlatObjectMetadata)) {
throw new ObjectMetadataException(
'Fail to find just created object metadata',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return createdFlatObjectMetadata;
}
}
@@ -96,18 +96,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}
override async createOne(
objectMetadataInput: CreateObjectInput,
createObjectInput: CreateObjectInput,
): Promise<ObjectMetadataEntity> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
objectMetadataInput.workspaceId,
createObjectInput.workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
const flatObjectMetadata = await this.objectMetadataServiceV2.createOne({
objectMetadataInput,
workspaceId: objectMetadataInput.workspaceId,
createObjectInput,
workspaceId: createObjectInput.workspaceId,
});
// Since V2 returns FlatObjectMetadata, we need to fetch the created entity
@@ -115,7 +115,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{
where: {
id: flatObjectMetadata.id,
workspaceId: objectMetadataInput.workspaceId,
workspaceId: createObjectInput.workspaceId,
},
},
);
@@ -144,61 +144,56 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{
workspaceId: objectMetadataInput.workspaceId,
workspaceId: createObjectInput.workspaceId,
},
);
const lastDataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
objectMetadataInput.workspaceId,
createObjectInput.workspaceId,
);
objectMetadataInput.labelSingular = capitalize(
objectMetadataInput.labelSingular,
);
objectMetadataInput.labelPlural = capitalize(
objectMetadataInput.labelPlural,
createObjectInput.labelSingular = capitalize(
createObjectInput.labelSingular,
);
createObjectInput.labelPlural = capitalize(createObjectInput.labelPlural);
validateObjectMetadataInputNamesOrThrow(objectMetadataInput);
validateObjectMetadataInputLabelsOrThrow(objectMetadataInput);
validateObjectMetadataInputNamesOrThrow(createObjectInput);
validateObjectMetadataInputLabelsOrThrow(createObjectInput);
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
inputs: [
objectMetadataInput.nameSingular,
objectMetadataInput.namePlural,
],
inputs: [createObjectInput.nameSingular, createObjectInput.namePlural],
message:
'The singular and plural names cannot be the same for an object',
});
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
inputs: [
objectMetadataInput.labelPlural,
objectMetadataInput.labelSingular,
createObjectInput.labelPlural,
createObjectInput.labelSingular,
],
message:
'The singular and plural labels cannot be the same for an object',
});
if (objectMetadataInput.isLabelSyncedWithName === true) {
if (createObjectInput.isLabelSyncedWithName === true) {
validateNameAndLabelAreSyncOrThrow({
label: objectMetadataInput.labelSingular,
name: objectMetadataInput.nameSingular,
label: createObjectInput.labelSingular,
name: createObjectInput.nameSingular,
});
validateNameAndLabelAreSyncOrThrow({
label: objectMetadataInput.labelPlural,
name: objectMetadataInput.namePlural,
label: createObjectInput.labelPlural,
name: createObjectInput.namePlural,
});
}
validatesNoOtherObjectWithSameNameExistsOrThrows({
objectMetadataNamePlural: objectMetadataInput.namePlural,
objectMetadataNameSingular: objectMetadataInput.nameSingular,
objectMetadataNamePlural: createObjectInput.namePlural,
objectMetadataNameSingular: createObjectInput.nameSingular,
objectMetadataMaps,
});
const baseCustomFields = buildDefaultFieldsForCustomObject(
objectMetadataInput.workspaceId,
createObjectInput.workspaceId,
);
const labelIdentifierFieldMetadataId = baseCustomFields.find(
@@ -213,24 +208,24 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}
const createdObjectMetadata = await objectMetadataRepository.save({
...objectMetadataInput,
...createObjectInput,
dataSourceId: lastDataSourceMetadata.id,
targetTableName: 'DEPRECATED',
isActive: true,
isCustom: !objectMetadataInput.isRemote,
isCustom: !createObjectInput.isRemote,
isSystem: false,
isRemote: objectMetadataInput.isRemote,
isSearchable: !objectMetadataInput.isRemote,
fields: objectMetadataInput.isRemote ? [] : baseCustomFields,
isRemote: createObjectInput.isRemote,
isSearchable: !createObjectInput.isRemote,
fields: createObjectInput.isRemote ? [] : baseCustomFields,
labelIdentifierFieldMetadataId,
});
if (objectMetadataInput.isRemote) {
if (createObjectInput.isRemote) {
throw new Error('Remote objects are not supported yet');
} else {
const createdRelatedObjectMetadataCollection =
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
objectMetadataInput.workspaceId,
createObjectInput.workspaceId,
createdObjectMetadata,
objectMetadataMaps,
queryRunner,
@@ -254,7 +249,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
await this.searchVectorService.createSearchVectorFieldForObject(
objectMetadataInput,
createObjectInput,
createdObjectMetadata,
queryRunner,
);
@@ -270,7 +265,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
// After commit, do non-transactional work
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId: objectMetadataInput.workspaceId,
workspaceId: createObjectInput.workspaceId,
},
);
await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords(
@@ -278,7 +273,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
objectMetadataInput.workspaceId,
createObjectInput.workspaceId,
);
return createdObjectMetadata;
@@ -455,9 +450,22 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}
public async deleteOneObject(
input: DeleteOneObjectInput,
deleteObjectInput: DeleteOneObjectInput,
workspaceId: string,
): Promise<Partial<ObjectMetadataEntity>> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.objectMetadataServiceV2.deleteOne({
deleteObjectInput,
workspaceId,
});
}
const mainDataSource =
await this.workspaceDataSourceService.connectToMainDataSource();
const queryRunner = mainDataSource.createQueryRunner();
@@ -479,7 +487,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
'fields.relationTargetFieldMetadata.object',
],
where: {
id: input.id,
id: deleteObjectInput.id,
workspaceId,
},
});
@@ -125,6 +125,7 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
workspaceId,
objectMetadataId,
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
const createdAt = new Date();
const idField: FlatFieldMetadata<FieldMetadataType.UUID> = {
type: FieldMetadataType.UUID,
id: v4(),
@@ -144,6 +145,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: true,
defaultValue: 'uuid',
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
@@ -172,6 +175,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: false,
defaultValue: "'Untitled'",
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
@@ -200,6 +205,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: false,
defaultValue: 'now',
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
@@ -228,6 +235,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: false,
defaultValue: 'now',
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
@@ -256,6 +265,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: false,
defaultValue: null,
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
@@ -284,6 +295,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: false,
defaultValue: { name: "''", source: "'MANUAL'" },
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
@@ -312,6 +325,8 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
isSystem: true,
defaultValue: 0,
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,