Add FK creation in migration v2 (#14685)
## Context - packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts Fixing targetFlatFieldMetadata not being accurate for default relation during object creation (was not used yet) - packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-foreign-key-manager.service.ts Simplifying API + Removing unused methods and the ones that were querying pg schema as we want to avoid those as much as possible - packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/create-field-action-handler.service.ts Adding FK creation when Join column is created
This commit is contained in:
+2
-2
@@ -239,7 +239,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
},
|
||||
flatRelationTargetObjectMetadata:
|
||||
fromFlatObjectMetadataToFlatObjectMetadataWithoutFields(
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -254,7 +254,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
},
|
||||
flatRelationTargetObjectMetadata:
|
||||
fromFlatObjectMetadataToFlatObjectMetadataWithoutFields(
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
+6
-232
@@ -7,29 +7,20 @@ export class WorkspaceSchemaForeignKeyManagerService {
|
||||
async createForeignKey({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
foreignKey,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
foreignKey: WorkspaceSchemaForeignKeyDefinition;
|
||||
}): Promise<void> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeForeignKeyName = removeSqlDDLInjection(foreignKey.name);
|
||||
const safeReferencedTableName = removeSqlDDLInjection(
|
||||
foreignKey.referencedTableName,
|
||||
const foreignKeyName = queryRunner.connection.namingStrategy.foreignKeyName(
|
||||
foreignKey.tableName,
|
||||
[foreignKey.columnName],
|
||||
`${schemaName}.${foreignKey.referencedTableName}`,
|
||||
[foreignKey.referencedColumnName],
|
||||
);
|
||||
|
||||
const quotedColumns = foreignKey.columnNames
|
||||
.map((col) => `"${removeSqlDDLInjection(col)}"`)
|
||||
.join(', ');
|
||||
const quotedRefColumns = foreignKey.referencedColumnNames
|
||||
.map((col) => `"${removeSqlDDLInjection(col)}"`)
|
||||
.join(', ');
|
||||
|
||||
let sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ADD CONSTRAINT "${safeForeignKeyName}" FOREIGN KEY (${quotedColumns}) REFERENCES "${safeSchemaName}"."${safeReferencedTableName}" (${quotedRefColumns})`;
|
||||
let sql = `ALTER TABLE "${schemaName}"."${foreignKey.tableName}" ADD CONSTRAINT "${foreignKeyName}" FOREIGN KEY ("${foreignKey.columnName}") REFERENCES "${schemaName}"."${foreignKey.referencedTableName}" ("${foreignKey.referencedColumnName}")`;
|
||||
|
||||
if (foreignKey.onDelete) {
|
||||
sql += ` ON DELETE ${foreignKey.onDelete}`;
|
||||
@@ -61,223 +52,6 @@ export class WorkspaceSchemaForeignKeyManagerService {
|
||||
await queryRunner.query(sql);
|
||||
}
|
||||
|
||||
async dropForeignKeyByColumn({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
}): Promise<void> {
|
||||
const foreignKeyName = await this.getForeignKeyNameByColumn({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName,
|
||||
});
|
||||
|
||||
if (foreignKeyName) {
|
||||
await this.dropForeignKey({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
foreignKeyName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async foreignKeyExists({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
foreignKeyName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
foreignKeyName: string;
|
||||
}): Promise<boolean> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeForeignKeyName = removeSqlDDLInjection(foreignKeyName);
|
||||
|
||||
const result = await queryRunner.query(
|
||||
`SELECT EXISTS (
|
||||
SELECT FROM information_schema.table_constraints
|
||||
WHERE constraint_schema = $1
|
||||
AND table_name = $2
|
||||
AND constraint_name = $3
|
||||
AND constraint_type = 'FOREIGN KEY'
|
||||
)`,
|
||||
[safeSchemaName, safeTableName, safeForeignKeyName],
|
||||
);
|
||||
|
||||
return result[0]?.exists || false;
|
||||
}
|
||||
|
||||
async getForeignKeyNameByColumn({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
}): Promise<string | null> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeColumnName = removeSqlDDLInjection(columnName);
|
||||
|
||||
const result = await queryRunner.query(
|
||||
`SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = $1
|
||||
AND tc.table_name = $2
|
||||
AND kcu.column_name = $3`,
|
||||
[safeSchemaName, safeTableName, safeColumnName],
|
||||
);
|
||||
|
||||
return result[0]?.constraint_name || null;
|
||||
}
|
||||
|
||||
async getForeignKeysForTable({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
}): Promise<
|
||||
Array<{
|
||||
constraint_name: string;
|
||||
column_name: string;
|
||||
foreign_table_name: string;
|
||||
foreign_column_name: string;
|
||||
delete_rule: string;
|
||||
update_rule: string;
|
||||
}>
|
||||
> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
|
||||
const result = await queryRunner.query(
|
||||
`SELECT
|
||||
tc.constraint_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name,
|
||||
rc.delete_rule,
|
||||
rc.update_rule
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
JOIN information_schema.referential_constraints AS rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
AND tc.table_schema = rc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = $1
|
||||
AND tc.table_name = $2`,
|
||||
[safeSchemaName, safeTableName],
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async createForeignKeyFromColumn({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName,
|
||||
referencedTableName,
|
||||
referencedColumnName = 'id',
|
||||
onDelete,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
referencedTableName: string;
|
||||
referencedColumnName?: string;
|
||||
onDelete?: WorkspaceSchemaForeignKeyDefinition['onDelete'];
|
||||
}): Promise<void> {
|
||||
const foreignKeyName = queryRunner.connection.namingStrategy.foreignKeyName(
|
||||
tableName,
|
||||
[columnName],
|
||||
`${schemaName}.${referencedTableName}`,
|
||||
[referencedColumnName],
|
||||
);
|
||||
|
||||
const foreignKey: WorkspaceSchemaForeignKeyDefinition = {
|
||||
name: foreignKeyName,
|
||||
columnNames: [columnName],
|
||||
referencedTableName,
|
||||
referencedColumnNames: [referencedColumnName],
|
||||
onDelete,
|
||||
};
|
||||
|
||||
await this.createForeignKey({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
foreignKey,
|
||||
});
|
||||
}
|
||||
|
||||
async renameForeignKey({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
oldConstraintName,
|
||||
newConstraintName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
oldConstraintName: string;
|
||||
newConstraintName: string;
|
||||
}): Promise<void> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeOldConstraintName = removeSqlDDLInjection(oldConstraintName);
|
||||
const safeNewConstraintName = removeSqlDDLInjection(newConstraintName);
|
||||
const sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" RENAME CONSTRAINT "${safeOldConstraintName}" TO "${safeNewConstraintName}"`;
|
||||
|
||||
await queryRunner.query(sql);
|
||||
}
|
||||
|
||||
async validateForeignKey({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
foreignKeyName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
foreignKeyName: string;
|
||||
}): Promise<void> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeForeignKeyName = removeSqlDDLInjection(foreignKeyName);
|
||||
const sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" VALIDATE CONSTRAINT "${safeForeignKeyName}"`;
|
||||
|
||||
await queryRunner.query(sql);
|
||||
}
|
||||
|
||||
async setForeignKeyNotDeferrable({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
export type WorkspaceSchemaForeignKeyDefinition = {
|
||||
name: string;
|
||||
columnNames: string[];
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
referencedTableName: string;
|
||||
referencedColumnNames: string[];
|
||||
referencedColumnName: string;
|
||||
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | 'SET DEFAULT';
|
||||
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | 'SET DEFAULT';
|
||||
};
|
||||
|
||||
+40
@@ -1,5 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
@@ -7,9 +10,12 @@ import {
|
||||
|
||||
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util';
|
||||
import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
|
||||
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/generate-column-definitions.util';
|
||||
@@ -106,5 +112,39 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
tableName,
|
||||
columnDefinitions,
|
||||
});
|
||||
|
||||
if (
|
||||
isMorphOrRelationFlatFieldMetadata(flatFieldMetadata) &&
|
||||
flatFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
const referencedTableName = computeObjectTargetTable(
|
||||
flatFieldMetadata.flatRelationTargetObjectMetadata,
|
||||
);
|
||||
|
||||
const joinColumnName = flatFieldMetadata.settings.joinColumnName;
|
||||
|
||||
if (!isDefined(joinColumnName)) {
|
||||
throw new Error(
|
||||
'Join column name is not defined in a MANY_TO_ONE relation',
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceSchemaManagerService.foreignKeyManager.createForeignKey(
|
||||
{
|
||||
queryRunner,
|
||||
schemaName,
|
||||
foreignKey: {
|
||||
tableName,
|
||||
columnName: joinColumnName,
|
||||
referencedTableName,
|
||||
referencedColumnName: 'id',
|
||||
onDelete:
|
||||
convertOnDeleteActionToOnDelete(
|
||||
flatFieldMetadata.settings.onDelete,
|
||||
) ?? 'CASCADE',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user