feat: add targetFieldMetadataId and migration script for relations (#9793)
Fix https://github.com/twentyhq/core-team-issues/issues/238 and https://github.com/twentyhq/core-team-issues/issues/239
This commit is contained in:
@ -0,0 +1,140 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { isCommandLogger } from 'src/database/commands/logger';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.41:migrate-relations-to-field-metadata',
|
||||
description: 'Migrate relations to field metadata',
|
||||
})
|
||||
export class MigrateRelationsToFieldMetadataCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to create many to one relations');
|
||||
|
||||
if (isCommandLogger(this.logger)) {
|
||||
this.logger.setVerbose(options.verbose ?? false);
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green('Command completed!'));
|
||||
} catch (error) {
|
||||
this.logger.log(chalk.red('Error in workspace'));
|
||||
}
|
||||
}
|
||||
|
||||
private async processWorkspace(
|
||||
workspaceId: string,
|
||||
index: number,
|
||||
total: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
const fieldMetadataCollection = (await this.fieldMetadataRepository.find({
|
||||
where: { workspaceId, type: FieldMetadataType.RELATION },
|
||||
relations: ['fromRelationMetadata', 'toRelationMetadata'],
|
||||
})) as unknown as FieldMetadataEntity<FieldMetadataType.RELATION>[];
|
||||
|
||||
if (!fieldMetadataCollection.length) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`No relation field metadata found for workspace ${workspaceId}.`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldMetadataToUpdateCollection = fieldMetadataCollection.map(
|
||||
(fieldMetadata) => this.mapFieldMetadata(fieldMetadata),
|
||||
);
|
||||
|
||||
if (fieldMetadataToUpdateCollection.length > 0) {
|
||||
await this.fieldMetadataRepository.save(
|
||||
fieldMetadataToUpdateCollection,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||
);
|
||||
} catch {
|
||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
|
||||
}
|
||||
}
|
||||
|
||||
private mapFieldMetadata(
|
||||
fieldMetadata: FieldMetadataEntity<FieldMetadataType.RELATION>,
|
||||
): FieldMetadataEntity<FieldMetadataType.RELATION> {
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
);
|
||||
let relationType = relationMetadata.relationType as unknown as RelationType;
|
||||
|
||||
if (
|
||||
relationDirection === RelationDirection.TO &&
|
||||
relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
relationType = RelationType.MANY_TO_ONE;
|
||||
}
|
||||
|
||||
const relationTargetFieldMetadataId =
|
||||
relationDirection === RelationDirection.FROM
|
||||
? relationMetadata.toFieldMetadataId
|
||||
: relationMetadata.fromFieldMetadataId;
|
||||
|
||||
const relationTargetObjectMetadataId =
|
||||
relationDirection === RelationDirection.FROM
|
||||
? relationMetadata.toObjectMetadataId
|
||||
: relationMetadata.fromObjectMetadataId;
|
||||
|
||||
return {
|
||||
...fieldMetadata,
|
||||
settings: {
|
||||
relationType,
|
||||
onDelete: relationMetadata.onDeleteAction,
|
||||
},
|
||||
relationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command';
|
||||
import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
@ -19,6 +20,7 @@ export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner {
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly seedWorkflowViewsCommand: SeedWorkflowViewsCommand,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly migrateRelationsToFieldMetadata: MigrateRelationsToFieldMetadataCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
@ -44,5 +46,11 @@ export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner {
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.migrateRelationsToFieldMetadata.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command';
|
||||
import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command';
|
||||
import { UpgradeTo0_41Command } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||
import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service';
|
||||
@ -16,6 +18,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
|
||||
TypeORMModule,
|
||||
DataSourceModule,
|
||||
ObjectMetadataModule,
|
||||
@ -28,6 +31,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
SyncWorkspaceMetadataCommand,
|
||||
SeedWorkflowViewsCommand,
|
||||
UpgradeTo0_41Command,
|
||||
MigrateRelationsToFieldMetadataCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_41CommandModule {}
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddRelationTargetFieldAndObjectToFieldMetadata1737561084251
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddRelationTargetFieldAndObjectToFieldMetadata1737561084251';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD "relationTargetFieldMetadataId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD CONSTRAINT "UQ_47a6c57e1652b6475f8248cff78" UNIQUE ("relationTargetFieldMetadataId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD "relationTargetObjectMetadataId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IndexOnRelationTargetObjectMetadataId" ON "metadata"."fieldMetadata" ("relationTargetObjectMetadataId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IndexOnRelationTargetFieldMetadataId" ON "metadata"."fieldMetadata" ("relationTargetFieldMetadataId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD CONSTRAINT "FK_47a6c57e1652b6475f8248cff78" FOREIGN KEY ("relationTargetFieldMetadataId") REFERENCES "metadata"."fieldMetadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD CONSTRAINT "FK_6f6c87ec32cca956d8be321071c" FOREIGN KEY ("relationTargetObjectMetadataId") REFERENCES "metadata"."objectMetadata"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" DROP CONSTRAINT "FK_6f6c87ec32cca956d8be321071c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" DROP CONSTRAINT "FK_47a6c57e1652b6475f8248cff78"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "metadata"."IndexOnRelationTargetFieldMetadataId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "metadata"."IndexOnRelationTargetObjectMetadataId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "relationTargetObjectMetadataId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" DROP CONSTRAINT "UQ_47a6c57e1652b6475f8248cff78"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "relationTargetFieldMetadataId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
@ -28,6 +29,12 @@ import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-met
|
||||
'objectMetadataId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IndexOnRelationTargetFieldMetadataId', [
|
||||
'relationTargetFieldMetadataId',
|
||||
])
|
||||
@Index('IndexOnRelationTargetObjectMetadataId', [
|
||||
'relationTargetObjectMetadataId',
|
||||
])
|
||||
export class FieldMetadataEntity<
|
||||
T extends FieldMetadataType | 'default' = 'default',
|
||||
> implements FieldMetadataInterface<T>
|
||||
@ -95,6 +102,26 @@ export class FieldMetadataEntity<
|
||||
@Column({ default: false })
|
||||
isLabelSyncedWithName: boolean;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
relationTargetFieldMetadataId: string;
|
||||
@OneToOne(
|
||||
() => FieldMetadataEntity,
|
||||
(fieldMetadata: FieldMetadataEntity) =>
|
||||
fieldMetadata.relationTargetFieldMetadataId,
|
||||
)
|
||||
@JoinColumn({ name: 'relationTargetFieldMetadataId' })
|
||||
relationTargetFieldMetadata: Relation<FieldMetadataEntity>;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
relationTargetObjectMetadataId: string;
|
||||
@ManyToOne(
|
||||
() => ObjectMetadataEntity,
|
||||
(objectMetadata: ObjectMetadataEntity) =>
|
||||
objectMetadata.targetRelationFields,
|
||||
)
|
||||
@JoinColumn({ name: 'relationTargetObjectMetadataId' })
|
||||
relationTargetObjectMetadata: Relation<ObjectMetadataEntity>;
|
||||
|
||||
@OneToOne(
|
||||
() => RelationMetadataEntity,
|
||||
(relation: RelationMetadataEntity) => relation.fromFieldMetadata,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
export enum NumberDataType {
|
||||
FLOAT = 'float',
|
||||
INT = 'int',
|
||||
@ -30,11 +33,17 @@ export type FieldMetadataDateTimeSettings = {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
|
||||
export type FieldMetadataRelationSettings = {
|
||||
relationType: RelationType;
|
||||
onDelete?: RelationOnDeleteAction;
|
||||
};
|
||||
|
||||
type FieldMetadataSettingsMapping = {
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
|
||||
[FieldMetadataType.DATE]: FieldMetadataDateSettings;
|
||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
||||
[FieldMetadataType.TEXT]: FieldMetadataTextSettings;
|
||||
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
|
||||
};
|
||||
|
||||
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export enum RelationOnDeleteAction {
|
||||
CASCADE = 'CASCADE',
|
||||
RESTRICT = 'RESTRICT',
|
||||
SET_NULL = 'SET_NULL',
|
||||
NO_ACTION = 'NO_ACTION',
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export enum RelationType {
|
||||
ONE_TO_ONE = 'ONE_TO_ONE',
|
||||
ONE_TO_MANY = 'ONE_TO_MANY',
|
||||
MANY_TO_ONE = 'MANY_TO_ONE',
|
||||
}
|
||||
@ -112,6 +112,12 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
||||
)
|
||||
toRelations: Relation<RelationMetadataEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => FieldMetadataEntity,
|
||||
(field) => field.relationTargetObjectMetadataId,
|
||||
)
|
||||
targetRelationFields: Relation<FieldMetadataEntity[]>;
|
||||
|
||||
@ManyToOne(() => DataSourceEntity, (dataSource) => dataSource.objects, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user