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:
Jérémy M
2025-01-22 17:01:54 +01:00
committed by GitHub
parent 984dc4dec0
commit b662609948
9 changed files with 260 additions and 0 deletions

View File

@ -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,
};
}
}

View File

@ -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,
);
}
}

View File

@ -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 {}

View File

@ -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"`,
);
}
}

View File

@ -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,

View File

@ -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'> =

View File

@ -0,0 +1,6 @@
export enum RelationOnDeleteAction {
CASCADE = 'CASCADE',
RESTRICT = 'RESTRICT',
SET_NULL = 'SET_NULL',
NO_ACTION = 'NO_ACTION',
}

View File

@ -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',
}

View File

@ -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',
})