Files
twenty/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts
Weiko 31ecaf2a0d fix object metadata renaming (#8175)
## Context
Latest refactoring broke the findOneWithinWorkspace method which is
called during object update, I'm simply reverting this change.
object-metadata-relation.service was naively computing a namePlural
based on the nameSingular while we already had that info in the DB...
Should fix some issues with renaming as well because the original field
was not computed with the right name.
2024-10-29 14:45:27 +00:00

492 lines
16 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import console from 'console';
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { isDefined } from 'class-validator';
import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
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 { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service';
import { buildDefaultFieldsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util';
import {
validateNameAndLabelAreSyncOrThrow,
validateNameSingularAndNamePluralAreDifferentOrThrow,
validateObjectMetadataInputOrThrow,
} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
import { SearchService } from 'src/engine/metadata-modules/search/search.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
import { ObjectMetadataEntity } from './object-metadata.entity';
import { CreateObjectInput } from './dtos/create-object.input';
@Injectable()
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEntity> {
constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly remoteTableRelationsService: RemoteTableRelationsService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly searchService: SearchService,
private readonly objectMetadataRelationService: ObjectMetadataRelationService,
private readonly objectMetadataMigrationService: ObjectMetadataMigrationService,
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
) {
super(objectMetadataRepository);
}
override async query(
query: Query<ObjectMetadataEntity>,
opts?: QueryOptions<ObjectMetadataEntity> | undefined,
): Promise<ObjectMetadataEntity[]> {
const start = performance.now();
const result = super.query(query, opts);
const end = performance.now();
console.log(`metadata query time: ${end - start} ms`);
return result;
}
override async createOne(
objectMetadataInput: CreateObjectInput,
): Promise<ObjectMetadataEntity> {
const lastDataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
objectMetadataInput.workspaceId,
);
validateObjectMetadataInputOrThrow(objectMetadataInput);
validateNameSingularAndNamePluralAreDifferentOrThrow(
objectMetadataInput.nameSingular,
objectMetadataInput.namePlural,
);
if (objectMetadataInput.isLabelSyncedWithName === true) {
validateNameAndLabelAreSyncOrThrow(
objectMetadataInput.labelSingular,
objectMetadataInput.nameSingular,
);
validateNameAndLabelAreSyncOrThrow(
objectMetadataInput.labelPlural,
objectMetadataInput.namePlural,
);
}
this.validatesNoOtherObjectWithSameNameExistsOrThrows({
objectMetadataNamePlural: objectMetadataInput.namePlural,
objectMetadataNameSingular: objectMetadataInput.nameSingular,
workspaceId: objectMetadataInput.workspaceId,
});
const createdObjectMetadata = await super.createOne({
...objectMetadataInput,
dataSourceId: lastDataSourceMetadata.id,
targetTableName: 'DEPRECATED',
isActive: true,
isCustom: !objectMetadataInput.isRemote,
isSystem: false,
isRemote: objectMetadataInput.isRemote,
fields: objectMetadataInput.isRemote
? []
: buildDefaultFieldsForCustomObject(objectMetadataInput.workspaceId),
});
if (objectMetadataInput.isRemote) {
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
objectMetadataInput.workspaceId,
createdObjectMetadata,
objectMetadataInput.primaryKeyFieldMetadataSettings,
objectMetadataInput.primaryKeyColumnType,
);
} else {
await this.objectMetadataMigrationService.createObjectMigration(
createdObjectMetadata,
);
await this.objectMetadataMigrationService.createFieldMigrations(
createdObjectMetadata,
createdObjectMetadata.fields,
);
await this.createRelationsMetadataAndMigrations(
objectMetadataInput,
createdObjectMetadata,
);
await this.searchService.createSearchVectorFieldForObject(
objectMetadataInput,
createdObjectMetadata,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
createdObjectMetadata.workspaceId,
);
await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords(
createdObjectMetadata,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
objectMetadataInput.workspaceId,
);
return createdObjectMetadata;
}
public async updateOneObject(
input: UpdateOneObjectInput,
workspaceId: string,
): Promise<ObjectMetadataEntity> {
validateObjectMetadataInputOrThrow(input.update);
const existingObjectMetadata = await this.objectMetadataRepository.findOne({
where: { id: input.id, workspaceId: workspaceId },
});
if (!existingObjectMetadata) {
throw new ObjectMetadataException(
'Object does not exist',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
const fullObjectMetadataAfterUpdate = {
...existingObjectMetadata,
...input.update,
};
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
objectMetadataNameSingular: fullObjectMetadataAfterUpdate.nameSingular,
objectMetadataNamePlural: fullObjectMetadataAfterUpdate.namePlural,
workspaceId: workspaceId,
existingObjectMetadataId: fullObjectMetadataAfterUpdate.id,
});
if (fullObjectMetadataAfterUpdate.isLabelSyncedWithName) {
validateNameAndLabelAreSyncOrThrow(
fullObjectMetadataAfterUpdate.labelSingular,
fullObjectMetadataAfterUpdate.nameSingular,
);
validateNameAndLabelAreSyncOrThrow(
fullObjectMetadataAfterUpdate.labelPlural,
fullObjectMetadataAfterUpdate.namePlural,
);
}
if (
isDefined(input.update.nameSingular) ||
isDefined(input.update.namePlural)
) {
validateNameSingularAndNamePluralAreDifferentOrThrow(
fullObjectMetadataAfterUpdate.nameSingular,
fullObjectMetadataAfterUpdate.namePlural,
);
}
const updatedObject = await super.updateOne(input.id, input.update);
await this.handleObjectNameAndLabelUpdates(
existingObjectMetadata,
fullObjectMetadataAfterUpdate,
input,
);
if (input.update.isActive !== undefined) {
await this.objectMetadataRelationService.updateObjectRelationships(
input.id,
input.update.isActive,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
if (input.update.labelIdentifierFieldMetadataId) {
const labelIdentifierFieldMetadata =
await this.fieldMetadataRepository.findOneByOrFail({
id: input.update.labelIdentifierFieldMetadataId,
objectMetadataId: input.id,
workspaceId: workspaceId,
});
if (isSearchableFieldType(labelIdentifierFieldMetadata.type)) {
await this.searchService.updateSearchVector(
input.id,
[
{
name: labelIdentifierFieldMetadata.name,
type: labelIdentifierFieldMetadata.type,
},
],
workspaceId,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
}
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
return updatedObject;
}
public async deleteOneObject(
input: DeleteOneObjectInput,
workspaceId: string,
): Promise<ObjectMetadataEntity> {
const objectMetadata = await this.objectMetadataRepository.findOne({
relations: [
'fromRelations.fromFieldMetadata',
'fromRelations.toFieldMetadata',
'toRelations.fromFieldMetadata',
'toRelations.toFieldMetadata',
'fromRelations.fromObjectMetadata',
'fromRelations.toObjectMetadata',
'toRelations.fromObjectMetadata',
'toRelations.toObjectMetadata',
],
where: {
id: input.id,
workspaceId,
},
});
if (!objectMetadata) {
throw new ObjectMetadataException(
'Object does not exist',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
if (objectMetadata.isRemote) {
await this.remoteTableRelationsService.deleteForeignKeysMetadataAndCreateMigrations(
objectMetadata.workspaceId,
objectMetadata,
);
} else {
await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable(
objectMetadata,
workspaceId,
);
}
await this.objectMetadataRelatedRecordsService.deleteObjectViews(
objectMetadata,
workspaceId,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.objectMetadataRepository.delete(objectMetadata.id);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
return objectMetadata;
}
public async findOneWithinWorkspace(
workspaceId: string,
options: FindOneOptions<ObjectMetadataEntity>,
): Promise<ObjectMetadataEntity | null> {
return this.objectMetadataRepository.findOne({
relations: [
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
],
...options,
where: {
...options.where,
workspaceId,
},
});
}
public async findManyWithinWorkspace(
workspaceId: string,
options?: FindManyOptions<ObjectMetadataEntity>,
) {
return this.objectMetadataRepository.find({
relations: [
'fields.object',
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
'fields.fromRelationMetadata.toObjectMetadata',
],
...options,
where: {
...options?.where,
workspaceId,
},
});
}
public async findMany(options?: FindManyOptions<ObjectMetadataEntity>) {
return this.objectMetadataRepository.find({
relations: [
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
'fields.fromRelationMetadata.toObjectMetadata',
],
...options,
where: {
...options?.where,
},
});
}
public async deleteObjectsMetadata(workspaceId: string) {
await this.objectMetadataRepository.delete({ workspaceId });
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
}
private async createRelationsMetadataAndMigrations(
objectMetadataInput: CreateObjectInput,
createdObjectMetadata: ObjectMetadataEntity,
) {
const relatedObjectTypes = [
'timelineActivity',
'activityTarget',
'favorite',
'attachment',
'noteTarget',
'taskTarget',
];
const createdRelatedObjectMetadata = await Promise.all(
relatedObjectTypes.map(async (relationType) =>
this.objectMetadataRelationService.createMetadata(
objectMetadataInput.workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
),
objectMetadataInput.primaryKeyFieldMetadataSettings,
relationType,
),
),
);
await this.objectMetadataMigrationService.createRelationMigrations(
createdObjectMetadata,
createdRelatedObjectMetadata,
);
}
private async handleObjectNameAndLabelUpdates(
existingObjectMetadata: ObjectMetadataEntity,
objectMetadataForUpdate: ObjectMetadataEntity,
input: UpdateOneObjectInput,
) {
const newTargetTableName = computeObjectTargetTable(
objectMetadataForUpdate,
);
const existingTargetTableName = computeObjectTargetTable(
existingObjectMetadata,
);
if (!(newTargetTableName === existingTargetTableName)) {
await this.objectMetadataMigrationService.createRenameTableMigration(
existingObjectMetadata,
objectMetadataForUpdate,
);
await this.objectMetadataMigrationService.createRelationsUpdatesMigrations(
existingObjectMetadata,
objectMetadataForUpdate,
);
}
if (input.update.labelPlural || input.update.icon) {
if (
!(input.update.labelPlural === existingObjectMetadata.labelPlural) ||
!(input.update.icon === existingObjectMetadata.icon)
) {
await this.objectMetadataRelatedRecordsService.updateObjectViews(
objectMetadataForUpdate,
objectMetadataForUpdate.workspaceId,
);
}
}
}
private validatesNoOtherObjectWithSameNameExistsOrThrows = async ({
objectMetadataNameSingular,
objectMetadataNamePlural,
workspaceId,
existingObjectMetadataId,
}: {
objectMetadataNameSingular: string;
objectMetadataNamePlural: string;
workspaceId: string;
existingObjectMetadataId?: string;
}): Promise<void> => {
const baseWhereConditions = [
{ nameSingular: objectMetadataNameSingular, workspaceId },
{ nameSingular: objectMetadataNamePlural, workspaceId },
{ namePlural: objectMetadataNameSingular, workspaceId },
{ namePlural: objectMetadataNamePlural, workspaceId },
];
const whereConditions = baseWhereConditions.map((condition) => {
return {
...condition,
...(isDefined(existingObjectMetadataId)
? { id: Not(In([existingObjectMetadataId])) }
: {}),
};
});
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
where: whereConditions,
});
if (objectAlreadyExists) {
throw new ObjectMetadataException(
'Object already exists',
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
);
}
};
}