Add support for indexes on composite fields and unicity constraint on indexes This pull request includes several changes across multiple files to improve error handling, enforce unique constraints, and update database migrations. The most important changes include updating error messages for snack bars, adding a new command to enforce unique constraints, and updating database migrations to include new fields and constraints. ### Error Handling Improvements: * [`packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx`](diffhunk://#diff-e7dc05ced8e4730430f5c7fcd0c75b3aa723da438c26e0bef8130b614427dd9aL23-R23): Updated error messages in `enqueueSnackBar` to use `error.message` directly. * [`packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts`](diffhunk://#diff-74c126d6bc7a5ed6b63be994d298df6669058034bfbc367b11045f9f31a3abe6L44-R46): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts`](diffhunk://#diff-af23a1d99639a66c251f87473e63e2b7bceaa4ee4f70fedfa0fcffe5c7d79181L56-R58): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts`](diffhunk://#diff-da04296cbe280202a1eaf6b1244a30490d4f400411bee139651172c59719088eL22-R24): Simplified error messages in `enqueueSnackBar`. ### New Command for Unique Constraints: * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-enforce-unique-constraints.command.ts`](diffhunk://#diff-8337096c8c80dd2619a5ba691ae5145101f8ae0368a75192a050047e8c6ab7cbR1-R159): Added a new command to enforce unique constraints on company domain names and person emails. * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts`](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14): Integrated the new `EnforceUniqueConstraintsCommand` into the upgrade process. [[1]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14) [[2]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR31) [[3]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR64-R68) * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts`](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7): Registered the new `EnforceUniqueConstraintsCommand` in the module. [[1]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7) [[2]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R24) ### Database Migrations: * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368824-migrationDebt.ts`](diffhunk://#diff-c450aeae7bc0ef4416a0ade2dc613ca3f688629f35d2a32f90a09c3f494febdcR1-R53): Added a migration to update the `relationMetadata_ondeleteaction_enum` and set default values. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368825-addIsUniqueToIndexMetadata.ts`](diffhunk://#diff-8f1e14bd7f6835ec2c3bb39bcc51e3c318a3008d576a981e682f4c985e746fbfR1-R19): Added a migration to include the `isUnique` field in `indexMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726762935841-addCompostiveColumnToIndexFieldMetadata.ts`](diffhunk://#diff-7c96b7276c7722d41ff31de23b2de4d6e09adfdc74815356ba63bc96a2669440R1-R19): Added a migration to include the `compositeColumn` field in `indexFieldMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726766871572-addWhereToIndexMetadata.ts`](diffhunk://#diff-26651295a975eb50e672dce0e4e274e861f66feb1b68105eee5a04df32796190R1-R14): Added a migration to include the `indexWhereClause` field in `indexMetadata`. ### GraphQL Exception Handling: * [`packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts`](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4): Enhanced exception handling for `QueryFailedError` to provide more specific error messages for unique constraint violations. [[1]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4) [[2]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R23-R59) * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts`](diffhunk://#diff-233d58ab2333586dd45e46e33d4f07e04a4b8adde4a11a48e25d86985e5a7943L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts`](diffhunk://#diff-68b803f0762c407f5d2d1f5f8d389655a60654a2dd2394a81318655dcd44dc43L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
551 lines
18 KiB
TypeScript
551 lines
18 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|
import camelCase from 'lodash.camelcase';
|
|
import { FindOneOptions, In, Repository } from 'typeorm';
|
|
import { v4 as uuidV4 } from 'uuid';
|
|
|
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
|
|
import {
|
|
FieldMetadataEntity,
|
|
FieldMetadataType,
|
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
|
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
import { CreateRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/create-relation.input';
|
|
import {
|
|
RelationMetadataException,
|
|
RelationMetadataExceptionCode,
|
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
|
|
import { InvalidStringException } from 'src/engine/metadata-modules/utils/exceptions/invalid-string.exception';
|
|
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
|
|
import { validateMetadataNameValidityOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-validity.utils';
|
|
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
|
import {
|
|
WorkspaceMigrationColumnActionType,
|
|
WorkspaceMigrationColumnDrop,
|
|
WorkspaceMigrationTableActionType,
|
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
|
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
|
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
|
|
import {
|
|
RelationMetadataEntity,
|
|
RelationMetadataType,
|
|
RelationOnDeleteAction,
|
|
} from './relation-metadata.entity';
|
|
|
|
@Injectable()
|
|
export class RelationMetadataService extends TypeOrmQueryService<RelationMetadataEntity> {
|
|
constructor(
|
|
@InjectRepository(RelationMetadataEntity, 'metadata')
|
|
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
|
|
@InjectRepository(FieldMetadataEntity, 'metadata')
|
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
private readonly objectMetadataService: ObjectMetadataService,
|
|
private readonly fieldMetadataService: FieldMetadataService,
|
|
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
|
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
|
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
|
private readonly indexMetadataService: IndexMetadataService,
|
|
) {
|
|
super(relationMetadataRepository);
|
|
}
|
|
|
|
override async createOne(
|
|
relationMetadataInput: CreateRelationInput,
|
|
): Promise<RelationMetadataEntity> {
|
|
const objectMetadataMap = await this.getObjectMetadataMap(
|
|
relationMetadataInput,
|
|
);
|
|
|
|
try {
|
|
validateMetadataNameValidityOrThrow(relationMetadataInput.fromName);
|
|
validateMetadataNameValidityOrThrow(relationMetadataInput.toName);
|
|
} catch (error) {
|
|
if (error instanceof InvalidStringException) {
|
|
throw new RelationMetadataException(
|
|
`Characters used in name "${relationMetadataInput.fromName}" or "${relationMetadataInput.toName}" are not supported`,
|
|
RelationMetadataExceptionCode.INVALID_RELATION_INPUT,
|
|
);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
await this.validateCreateRelationMetadataInput(
|
|
relationMetadataInput,
|
|
objectMetadataMap,
|
|
);
|
|
|
|
// NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations)
|
|
const isCustom = true;
|
|
const columnName = `${camelCase(relationMetadataInput.toName)}Id`;
|
|
|
|
const fromId = uuidV4();
|
|
const toId = uuidV4();
|
|
|
|
const createdRelationFieldsMetadata =
|
|
await this.fieldMetadataService.createMany([
|
|
this.createFieldMetadataForRelationMetadata(
|
|
relationMetadataInput,
|
|
'from',
|
|
isCustom,
|
|
fromId,
|
|
),
|
|
this.createFieldMetadataForRelationMetadata(
|
|
relationMetadataInput,
|
|
'to',
|
|
isCustom,
|
|
toId,
|
|
),
|
|
this.createForeignKeyFieldMetadata(relationMetadataInput, columnName),
|
|
]);
|
|
|
|
const createdRelationMetadata = await super.createOne({
|
|
...relationMetadataInput,
|
|
fromFieldMetadataId: fromId,
|
|
toFieldMetadataId: toId,
|
|
});
|
|
|
|
await this.createWorkspaceCustomMigration(
|
|
relationMetadataInput,
|
|
objectMetadataMap,
|
|
columnName,
|
|
);
|
|
|
|
const toObjectMetadata =
|
|
objectMetadataMap[relationMetadataInput.toObjectMetadataId];
|
|
|
|
const foreignKeyFieldMetadata = createdRelationFieldsMetadata.find(
|
|
(fieldMetadata) => fieldMetadata.type === FieldMetadataType.UUID,
|
|
);
|
|
|
|
if (!foreignKeyFieldMetadata) {
|
|
throw new RelationMetadataException(
|
|
`ForeignKey field metadata not found`,
|
|
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const deletedFieldMetadata = toObjectMetadata.fields.find(
|
|
(fieldMetadata) =>
|
|
fieldMetadata.standardId === BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
|
);
|
|
|
|
if (!deletedFieldMetadata) {
|
|
throw new RelationMetadataException(
|
|
`Deleted field metadata not found`,
|
|
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
await this.indexMetadataService.createIndex(
|
|
relationMetadataInput.workspaceId,
|
|
toObjectMetadata,
|
|
[foreignKeyFieldMetadata, deletedFieldMetadata],
|
|
false,
|
|
false,
|
|
);
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
relationMetadataInput.workspaceId,
|
|
);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
relationMetadataInput.workspaceId,
|
|
);
|
|
|
|
return createdRelationMetadata;
|
|
}
|
|
|
|
private async validateCreateRelationMetadataInput(
|
|
relationMetadataInput: CreateRelationInput,
|
|
objectMetadataMap: { [key: string]: ObjectMetadataEntity },
|
|
) {
|
|
if (
|
|
relationMetadataInput.relationType === RelationMetadataType.MANY_TO_MANY
|
|
) {
|
|
throw new RelationMetadataException(
|
|
'Many to many relations are not supported yet',
|
|
RelationMetadataExceptionCode.INVALID_RELATION_INPUT,
|
|
);
|
|
}
|
|
|
|
if (
|
|
objectMetadataMap[relationMetadataInput.fromObjectMetadataId] ===
|
|
undefined ||
|
|
objectMetadataMap[relationMetadataInput.toObjectMetadataId] === undefined
|
|
) {
|
|
throw new RelationMetadataException(
|
|
'Can\t find an existing object matching with fromObjectMetadataId or toObjectMetadataId',
|
|
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
await this.checkIfFieldMetadataRelationNameExists(
|
|
relationMetadataInput,
|
|
objectMetadataMap,
|
|
'from',
|
|
);
|
|
await this.checkIfFieldMetadataRelationNameExists(
|
|
relationMetadataInput,
|
|
objectMetadataMap,
|
|
'to',
|
|
);
|
|
|
|
validateFieldNameAvailabilityOrThrow(
|
|
relationMetadataInput.fromName,
|
|
objectMetadataMap[relationMetadataInput.fromObjectMetadataId],
|
|
);
|
|
validateFieldNameAvailabilityOrThrow(
|
|
relationMetadataInput.toName,
|
|
objectMetadataMap[relationMetadataInput.toObjectMetadataId],
|
|
);
|
|
}
|
|
|
|
private async checkIfFieldMetadataRelationNameExists(
|
|
relationMetadataInput: CreateRelationInput,
|
|
objectMetadataMap: { [key: string]: ObjectMetadataEntity },
|
|
relationDirection: 'from' | 'to',
|
|
) {
|
|
const fieldAlreadyExists =
|
|
await this.fieldMetadataService.findOneWithinWorkspace(
|
|
relationMetadataInput.workspaceId,
|
|
{
|
|
where: {
|
|
name: relationMetadataInput[`${relationDirection}Name`],
|
|
objectMetadataId:
|
|
relationMetadataInput[`${relationDirection}ObjectMetadataId`],
|
|
},
|
|
},
|
|
);
|
|
|
|
if (fieldAlreadyExists) {
|
|
throw new RelationMetadataException(
|
|
`Field on ${
|
|
objectMetadataMap[
|
|
relationMetadataInput[`${relationDirection}ObjectMetadataId`]
|
|
].nameSingular
|
|
} already exists`,
|
|
RelationMetadataExceptionCode.RELATION_ALREADY_EXISTS,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async createWorkspaceCustomMigration(
|
|
relationMetadataInput: CreateRelationInput,
|
|
objectMetadataMap: { [key: string]: ObjectMetadataEntity },
|
|
columnName: string,
|
|
) {
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(`create-${relationMetadataInput.fromName}`),
|
|
relationMetadataInput.workspaceId,
|
|
[
|
|
// Create the column
|
|
{
|
|
name: computeObjectTargetTable(
|
|
objectMetadataMap[relationMetadataInput.toObjectMetadataId],
|
|
),
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
columns: [
|
|
{
|
|
action: WorkspaceMigrationColumnActionType.CREATE,
|
|
columnName,
|
|
columnType: 'uuid',
|
|
isNullable: true,
|
|
defaultValue: null,
|
|
},
|
|
],
|
|
},
|
|
// Create the foreignKey
|
|
{
|
|
name: computeObjectTargetTable(
|
|
objectMetadataMap[relationMetadataInput.toObjectMetadataId],
|
|
),
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
columns: [
|
|
{
|
|
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
|
|
columnName,
|
|
referencedTableName: computeObjectTargetTable(
|
|
objectMetadataMap[relationMetadataInput.fromObjectMetadataId],
|
|
),
|
|
referencedTableColumnName: 'id',
|
|
isUnique:
|
|
relationMetadataInput.relationType ===
|
|
RelationMetadataType.ONE_TO_ONE,
|
|
onDelete: RelationOnDeleteAction.SET_NULL,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
private createFieldMetadataForRelationMetadata(
|
|
relationMetadataInput: CreateRelationInput,
|
|
relationDirection: 'from' | 'to',
|
|
isCustom: boolean,
|
|
id?: string,
|
|
) {
|
|
return {
|
|
...(id && { id: id }),
|
|
name: relationMetadataInput[`${relationDirection}Name`],
|
|
label: relationMetadataInput[`${relationDirection}Label`],
|
|
description: relationMetadataInput[`${relationDirection}Description`],
|
|
icon: relationMetadataInput[`${relationDirection}Icon`],
|
|
isCustom,
|
|
isActive: true,
|
|
isNullable: true,
|
|
type: FieldMetadataType.RELATION,
|
|
objectMetadataId:
|
|
relationMetadataInput[`${relationDirection}ObjectMetadataId`],
|
|
workspaceId: relationMetadataInput.workspaceId,
|
|
};
|
|
}
|
|
|
|
private createForeignKeyFieldMetadata(
|
|
relationMetadataInput: CreateRelationInput,
|
|
columnName: string,
|
|
) {
|
|
return {
|
|
name: columnName,
|
|
label: `${relationMetadataInput.toLabel} Foreign Key`,
|
|
description: relationMetadataInput.toDescription
|
|
? `${relationMetadataInput.toDescription} Foreign Key`
|
|
: undefined,
|
|
icon: undefined,
|
|
isCustom: true,
|
|
isActive: true,
|
|
isNullable: true,
|
|
isSystem: true,
|
|
type: FieldMetadataType.UUID,
|
|
objectMetadataId: relationMetadataInput.toObjectMetadataId,
|
|
workspaceId: relationMetadataInput.workspaceId,
|
|
};
|
|
}
|
|
|
|
private async getObjectMetadataMap(
|
|
relationMetadataInput: CreateRelationInput,
|
|
): Promise<{ [key: string]: ObjectMetadataEntity }> {
|
|
const objectMetadataEntries =
|
|
await this.objectMetadataService.findManyWithinWorkspace(
|
|
relationMetadataInput.workspaceId,
|
|
{
|
|
where: {
|
|
id: In([
|
|
relationMetadataInput.fromObjectMetadataId,
|
|
relationMetadataInput.toObjectMetadataId,
|
|
]),
|
|
},
|
|
},
|
|
);
|
|
|
|
return objectMetadataEntries.reduce(
|
|
(acc, curr) => {
|
|
acc[curr.id] = curr;
|
|
|
|
return acc;
|
|
},
|
|
{} as { [key: string]: ObjectMetadataEntity },
|
|
);
|
|
}
|
|
|
|
public async findOneWithinWorkspace(
|
|
workspaceId: string,
|
|
options: FindOneOptions<RelationMetadataEntity>,
|
|
) {
|
|
return this.relationMetadataRepository.findOne({
|
|
...options,
|
|
where: {
|
|
...options.where,
|
|
workspaceId,
|
|
},
|
|
relations: ['fromFieldMetadata', 'toFieldMetadata'],
|
|
});
|
|
}
|
|
|
|
public async deleteOneRelation(
|
|
id: string,
|
|
workspaceId: string,
|
|
): Promise<RelationMetadataEntity> {
|
|
// TODO: This logic is duplicated with the BeforeDeleteOneRelation hook
|
|
const relationMetadata = await this.relationMetadataRepository.findOne({
|
|
where: { id },
|
|
relations: [
|
|
'fromFieldMetadata',
|
|
'toFieldMetadata',
|
|
'fromObjectMetadata',
|
|
'toObjectMetadata',
|
|
],
|
|
});
|
|
|
|
if (!relationMetadata) {
|
|
throw new RelationMetadataException(
|
|
'Relation does not exist',
|
|
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const foreignKeyFieldMetadataName = `${camelCase(
|
|
relationMetadata.toFieldMetadata.name,
|
|
)}Id`;
|
|
|
|
const foreignKeyFieldMetadata = await this.fieldMetadataRepository.findOne({
|
|
where: {
|
|
name: foreignKeyFieldMetadataName,
|
|
objectMetadataId: relationMetadata.toObjectMetadataId,
|
|
workspaceId: relationMetadata.workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!foreignKeyFieldMetadata) {
|
|
throw new RelationMetadataException(
|
|
`Foreign key fieldMetadata not found (${foreignKeyFieldMetadataName}) for relation ${relationMetadata.id}`,
|
|
RelationMetadataExceptionCode.FOREIGN_KEY_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
await super.deleteOne(id);
|
|
|
|
// TODO: Move to a cdc scheduler
|
|
await this.fieldMetadataService.deleteMany({
|
|
id: {
|
|
in: [
|
|
relationMetadata.fromFieldMetadataId,
|
|
relationMetadata.toFieldMetadataId,
|
|
foreignKeyFieldMetadata.id,
|
|
],
|
|
},
|
|
});
|
|
|
|
const columnName = `${camelCase(relationMetadata.toFieldMetadata.name)}Id`;
|
|
const objectTargetTable = computeObjectTargetTable(
|
|
relationMetadata.toObjectMetadata,
|
|
);
|
|
|
|
await this.deleteRelationWorkspaceCustomMigration(
|
|
relationMetadata,
|
|
objectTargetTable,
|
|
columnName,
|
|
);
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
relationMetadata.workspaceId,
|
|
);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
// TODO: Return id for delete endpoints
|
|
return relationMetadata;
|
|
}
|
|
|
|
async findManyRelationMetadataByFieldMetadataIds(
|
|
fieldMetadataItems: Array<
|
|
Pick<FieldMetadataInterface, 'id' | 'type' | 'objectMetadataId'>
|
|
>,
|
|
workspaceId: string,
|
|
): Promise<(RelationMetadataEntity | NotFoundException)[]> {
|
|
const metadataVersion =
|
|
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
|
|
|
if (!metadataVersion) {
|
|
throw new NotFoundException(
|
|
`Metadata version not found for workspace ${workspaceId}`,
|
|
);
|
|
}
|
|
|
|
const objectMetadataMap =
|
|
await this.workspaceCacheStorageService.getObjectMetadataMap(
|
|
workspaceId,
|
|
metadataVersion,
|
|
);
|
|
|
|
if (!objectMetadataMap) {
|
|
throw new NotFoundException(
|
|
`Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`,
|
|
);
|
|
}
|
|
|
|
const mappedResult = fieldMetadataItems.map((fieldMetadataItem) => {
|
|
const objectMetadata =
|
|
objectMetadataMap[fieldMetadataItem.objectMetadataId];
|
|
|
|
const fieldMetadata = objectMetadata.fields[fieldMetadataItem.id];
|
|
|
|
const relationMetadata =
|
|
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
|
|
|
if (!relationMetadata) {
|
|
return new NotFoundException(
|
|
`From object metadata not found for relation ${fieldMetadata?.id}`,
|
|
);
|
|
}
|
|
|
|
const fromObjectMetadata =
|
|
objectMetadataMap[relationMetadata.fromObjectMetadataId];
|
|
|
|
const toObjectMetadata =
|
|
objectMetadataMap[relationMetadata.toObjectMetadataId];
|
|
|
|
const fromFieldMetadata =
|
|
objectMetadataMap[fromObjectMetadata.id].fields[
|
|
relationMetadata.fromFieldMetadataId
|
|
];
|
|
|
|
const toFieldMetadata =
|
|
objectMetadataMap[toObjectMetadata.id].fields[
|
|
relationMetadata.toFieldMetadataId
|
|
];
|
|
|
|
return {
|
|
...relationMetadata,
|
|
fromObjectMetadata,
|
|
toObjectMetadata,
|
|
fromFieldMetadata,
|
|
toFieldMetadata,
|
|
};
|
|
});
|
|
|
|
return mappedResult as (RelationMetadataEntity | NotFoundException)[];
|
|
}
|
|
|
|
private async deleteRelationWorkspaceCustomMigration(
|
|
relationMetadata: RelationMetadataEntity,
|
|
objectTargetTable: string,
|
|
columnName: string,
|
|
) {
|
|
await this.workspaceMigrationService.createCustomMigration(
|
|
generateMigrationName(
|
|
`delete-relation-from-${relationMetadata.fromObjectMetadata.nameSingular}-to-${relationMetadata.toObjectMetadata.nameSingular}`,
|
|
),
|
|
relationMetadata.workspaceId,
|
|
[
|
|
// Delete the column
|
|
{
|
|
name: objectTargetTable,
|
|
action: WorkspaceMigrationTableActionType.ALTER,
|
|
columns: [
|
|
{
|
|
action: WorkspaceMigrationColumnActionType.DROP,
|
|
columnName,
|
|
} satisfies WorkspaceMigrationColumnDrop,
|
|
],
|
|
},
|
|
],
|
|
);
|
|
}
|
|
}
|