Files
twenty/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts
T
Jérémy M d532f22fbb feat: migration can be applied on a specific schema & some enhancements (#2998)
* fix: remove old metadata seed files

* feat: wip standard to core relation

* fix: lint

* fix: merge

* fix: remove debug files

* feat: add feature flag for core object metadata

* fix: remove debug

* feat: always disable the standard core relation

* fix: missing feature flag

* fix: remove debug

* fix: feature flag doesn't seems to disable relation

* fix: delete .vscode folder, change this in another PR

* Update packages/twenty-server/src/workspace/workspace-sync-metadata/reflective-metadata.factory.ts

Co-authored-by: Weiko <corentin@twenty.com>

* Update packages/twenty-server/src/workspace/workspace-sync-metadata/reflective-metadata.factory.ts

Co-authored-by: Weiko <corentin@twenty.com>

* Update packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync.metadata.service.ts

Co-authored-by: Weiko <corentin@twenty.com>

* fix: remove optional fields from metadata entities

* fix: renamed variable

* fix: put back CursorScalarType

* fix: delete test command

* fix: remove unused workspace standard migration command

* fix: drop core object metadata declaration

* fix: rename variable

* fix: drop creation of core datasource

* fix: remove feature flag

* fix: drop support of standard to core relations

* feat: add user email field on workspace-member standard object

* fix: update seed accordingly

* fix: missing remove command file

* fix: datasource label should remain nullable

* fix: better asserts

* Remove unused code

* Remove unused code

---------

Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2023-12-21 19:15:05 +01:00

314 lines
9.4 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import {
QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableUnique,
} from 'typeorm';
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationColumnRelation,
WorkspaceMigrationColumnAlter,
} from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceCacheVersionService } from 'src/metadata/workspace-cache-version/workspace-cache-version.service';
import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service';
import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
@Injectable()
export class WorkspaceMigrationRunnerService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
private readonly workspaceMigrationEnumService: WorkspaceMigrationEnumService,
) {}
/**
* Executes pending migrations for a given workspace
*
* @param workspaceId string
* @returns Promise<WorkspaceMigrationTableAction[]>
*/
public async executeMigrationFromPendingMigrations(
workspaceId: string,
): Promise<WorkspaceMigrationTableAction[]> {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
if (!workspaceDataSource) {
throw new Error('Workspace data source not found');
}
const pendingMigrations =
await this.workspaceMigrationService.getPendingMigrations(workspaceId);
if (pendingMigrations.length === 0) {
return [];
}
const flattenedPendingMigrations: WorkspaceMigrationTableAction[] =
pendingMigrations.reduce((acc, pendingMigration) => {
return [...acc, ...pendingMigration.migrations];
}, []);
const queryRunner = workspaceDataSource?.createQueryRunner();
// Loop over each migration and create or update the table
// TODO: Should be done in a transaction
for (const migration of flattenedPendingMigrations) {
await this.handleTableChanges(queryRunner, migration);
}
// Update appliedAt date for each migration
// TODO: Should be done after the migration is successful
for (const pendingMigration of pendingMigrations) {
await this.workspaceMigrationService.setAppliedAtForMigration(
workspaceId,
pendingMigration,
);
}
await queryRunner.release();
// Increment workspace cache version
await this.workspaceCacheVersionService.incrementVersion(workspaceId);
return flattenedPendingMigrations;
}
/**
* Handles table changes for a given migration
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableMigration WorkspaceMigrationTableChange
*/
private async handleTableChanges(
queryRunner: QueryRunner,
tableMigration: WorkspaceMigrationTableAction,
) {
switch (tableMigration.action) {
case 'create':
await this.createTable(
queryRunner,
tableMigration.schemaName,
tableMigration.name,
);
break;
case 'alter':
await this.handleColumnChanges(
queryRunner,
tableMigration.schemaName,
tableMigration.name,
tableMigration?.columns,
);
break;
default:
throw new Error(
`Migration table action ${tableMigration.action} not supported`,
);
}
}
/**
* Creates a table for a given schema and table name
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableName string
*/
private async createTable(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
) {
await queryRunner.createTable(
new Table({
name: tableName,
schema: schemaName,
columns: customTableDefaultColumns,
}),
true,
);
}
/**
* Handles column changes for a given migration
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableName string
* @param columnMigrations WorkspaceMigrationColumnAction[]
* @returns
*/
private async handleColumnChanges(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
columnMigrations?: WorkspaceMigrationColumnAction[],
) {
if (!columnMigrations || columnMigrations.length === 0) {
return;
}
for (const columnMigration of columnMigrations) {
switch (columnMigration.action) {
case WorkspaceMigrationColumnActionType.CREATE:
await this.createColumn(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.ALTER:
await this.alterColumn(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.RELATION:
await this.createRelation(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.DROP:
await queryRunner.dropColumn(
`${schemaName}.${tableName}`,
columnMigration.columnName,
);
break;
default:
throw new Error(`Migration column action not supported`);
}
}
}
/**
* Creates a column for a given schema, table name, and column migration
*
* @param queryRunner QueryRunner
* @param schemaName string
* @param tableName string
* @param migrationColumn WorkspaceMigrationColumnAction
*/
private async createColumn(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnCreate,
) {
const hasColumn = await queryRunner.hasColumn(
`${schemaName}.${tableName}`,
migrationColumn.columnName,
);
if (hasColumn) {
return;
}
await queryRunner.addColumn(
`${schemaName}.${tableName}`,
new TableColumn({
name: migrationColumn.columnName,
type: migrationColumn.columnType,
default: migrationColumn.defaultValue,
enum: migrationColumn.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.isArray,
isNullable: migrationColumn.isNullable,
}),
);
}
private async alterColumn(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
const enumValues = migrationColumn.alteredColumnDefinition.enum;
// TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
if (enumValues) {
// This is returning the old enum values to avoid TypeORM droping the enum type
await this.workspaceMigrationEnumService.alterEnum(
queryRunner,
schemaName,
tableName,
migrationColumn,
);
}
await queryRunner.changeColumn(
`${schemaName}.${tableName}`,
new TableColumn({
name: migrationColumn.currentColumnDefinition.columnName,
type: migrationColumn.currentColumnDefinition.columnType,
default: migrationColumn.currentColumnDefinition.defaultValue,
enum: migrationColumn.currentColumnDefinition.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.currentColumnDefinition.isArray,
isNullable: migrationColumn.currentColumnDefinition.isNullable,
}),
new TableColumn({
name: migrationColumn.alteredColumnDefinition.columnName,
type: migrationColumn.alteredColumnDefinition.columnType,
default: migrationColumn.alteredColumnDefinition.defaultValue,
enum: migrationColumn.currentColumnDefinition.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.alteredColumnDefinition.isArray,
isNullable: migrationColumn.alteredColumnDefinition.isNullable,
}),
);
}
private async createRelation(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnRelation,
) {
await queryRunner.createForeignKey(
`${schemaName}.${tableName}`,
new TableForeignKey({
columnNames: [migrationColumn.columnName],
referencedColumnNames: [migrationColumn.referencedTableColumnName],
referencedSchema: migrationColumn.referencedSchema,
referencedTableName: migrationColumn.referencedTableName,
onDelete: 'CASCADE',
}),
);
// Create unique constraint if for one to one relation
if (migrationColumn.isUnique) {
await queryRunner.createUniqueConstraint(
`${schemaName}.${tableName}`,
new TableUnique({
name: `UNIQUE_${tableName}_${migrationColumn.columnName}`,
columnNames: [migrationColumn.columnName],
}),
);
}
}
}