feat: Adding support for new FieldMetadataType with Postgres enums (#2674)

* feat: add enum type (RATING, SELECT, MULTI_SELECT)

feat: wip enum type

feat: try to alter enum

feat: wip enum

feat: wip enum

feat: schema-builder can handle enum

fix: return default value in field metadata response

* fix: create fieldMedata with options

* fix: lint issues

* fix: rename abstract factory

* feat: drop `PHONE` and `EMAIL` fieldMetadata types

* feat: drop `VARCHAR` fieldMetadata type and rely on `TEXT`

* Revert "feat: drop `PHONE` and `EMAIL` fieldMetadata types"

This reverts commit 3857539f7d42f17c81f6ab92a6db950140b3c8e5.
This commit is contained in:
Jérémy M
2023-11-30 15:24:26 +01:00
committed by GitHub
parent c2131a29b8
commit 6e6f0af26e
92 changed files with 1371 additions and 484 deletions

View File

@ -0,0 +1,183 @@
import { Injectable } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import { WorkspaceMigrationColumnAlter } from 'src/metadata/workspace-migration/workspace-migration.entity';
@Injectable()
export class WorkspaceMigrationEnumService {
async alterEnum(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
const oldEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum`;
const newEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum_new`;
const enumValues =
migrationColumn.enum?.map((enumValue) => {
if (typeof enumValue === 'string') {
return enumValue;
}
return enumValue.to;
}) ?? [];
if (!migrationColumn.isNullable && !migrationColumn.defaultValue) {
migrationColumn.defaultValue = migrationColumn.enum?.[0];
}
// Create new enum type with new values
await this.createNewEnumType(
newEnumTypeName,
queryRunner,
schemaName,
enumValues,
);
// Temporarily change column type to text
await queryRunner.query(`
ALTER TABLE "${schemaName}"."${tableName}"
ALTER COLUMN "${migrationColumn.columnName}" TYPE TEXT
`);
// Migrate existing values to new values
await this.migrateEnumValues(
queryRunner,
schemaName,
tableName,
migrationColumn,
);
// Update existing rows to handle missing values
await this.handleMissingEnumValues(
queryRunner,
schemaName,
tableName,
migrationColumn,
enumValues,
);
// Alter column type to new enum
await this.updateColumnToNewEnum(
queryRunner,
schemaName,
tableName,
migrationColumn.columnName,
newEnumTypeName,
);
// Drop old enum type
await this.dropOldEnumType(queryRunner, schemaName, oldEnumTypeName);
// Rename new enum type to old enum type name
await this.renameEnumType(
queryRunner,
schemaName,
oldEnumTypeName,
newEnumTypeName,
);
}
private async createNewEnumType(
name: string,
queryRunner: QueryRunner,
schemaName: string,
newValues: string[],
) {
const enumValues = newValues
.map((value) => `'${value.replace(/'/g, "''")}'`)
.join(', ');
await queryRunner.query(
`CREATE TYPE "${schemaName}"."${name}" AS ENUM (${enumValues})`,
);
}
private async migrateEnumValues(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
if (!migrationColumn.enum) {
return;
}
for (const enumValue of migrationColumn.enum) {
// Skip string values
if (typeof enumValue === 'string') {
continue;
}
await queryRunner.query(`
UPDATE "${schemaName}"."${tableName}"
SET "${migrationColumn.columnName}" = '${enumValue.to}'
WHERE "${migrationColumn.columnName}" = '${enumValue.from}'
`);
}
}
private async handleMissingEnumValues(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
enumValues: string[],
) {
// Set missing values to null or default value
let defaultValue = 'NULL';
if (migrationColumn.defaultValue) {
if (Array.isArray(migrationColumn.defaultValue)) {
defaultValue = `ARRAY[${migrationColumn.defaultValue
.map((e) => `'${e}'`)
.join(', ')}]`;
} else {
defaultValue = `'${migrationColumn.defaultValue}'`;
}
}
await queryRunner.query(`
UPDATE "${schemaName}"."${tableName}"
SET "${migrationColumn.columnName}" = ${defaultValue}
WHERE "${migrationColumn.columnName}" NOT IN (${enumValues
.map((e) => `'${e}'`)
.join(', ')})
`);
}
private async updateColumnToNewEnum(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
columnName: string,
newEnumTypeName: string,
) {
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE "${schemaName}"."${newEnumTypeName}" USING ("${columnName}"::text::"${schemaName}"."${newEnumTypeName}")`,
);
}
private async dropOldEnumType(
queryRunner: QueryRunner,
schemaName: string,
oldEnumTypeName: string,
) {
await queryRunner.query(
`DROP TYPE IF EXISTS "${schemaName}"."${oldEnumTypeName}"`,
);
}
private async renameEnumType(
queryRunner: QueryRunner,
schemaName: string,
oldEnumTypeName: string,
newEnumTypeName: string,
) {
await queryRunner.query(`
ALTER TYPE "${schemaName}"."${newEnumTypeName}"
RENAME TO "${oldEnumTypeName}"
`);
}
}

View File

@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceCacheVersionModule } from 'src/metadata/workspace-cache-version/workspace-cache-version.module';
import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service';
import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.service';
@ -12,7 +13,7 @@ import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.se
WorkspaceMigrationModule,
WorkspaceCacheVersionModule,
],
providers: [WorkspaceMigrationRunnerService, WorkspaceMigrationEnumService],
exports: [WorkspaceMigrationRunnerService],
providers: [WorkspaceMigrationRunnerService],
})
export class WorkspaceMigrationRunnerModule {}

View File

@ -16,8 +16,10 @@ import {
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';
@ -27,6 +29,7 @@ export class WorkspaceMigrationRunnerService {
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
private readonly workspaceMigrationEnumService: WorkspaceMigrationEnumService,
) {}
/**
@ -168,6 +171,14 @@ export class WorkspaceMigrationRunnerService {
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.ALTER:
await this.alterColumn(
queryRunner,
schemaName,
tableName,
columnMigration,
);
break;
case WorkspaceMigrationColumnActionType.RELATION:
await this.createForeignKey(
queryRunner,
@ -200,6 +211,7 @@ export class WorkspaceMigrationRunnerService {
`${schemaName}.${tableName}`,
migrationColumn.columnName,
);
if (hasColumn) {
return;
}
@ -210,11 +222,46 @@ export class WorkspaceMigrationRunnerService {
name: migrationColumn.columnName,
type: migrationColumn.columnType,
default: migrationColumn.defaultValue,
enum: migrationColumn.enum?.filter(
(value): value is string => typeof value === 'string',
),
isArray: migrationColumn.isArray,
isNullable: true,
}),
);
}
private async alterColumn(
queryRunner: QueryRunner,
schemaName: string,
tableName: string,
migrationColumn: WorkspaceMigrationColumnAlter,
) {
const enumValues = migrationColumn.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,
);
} else {
await queryRunner.changeColumn(
`${schemaName}.${tableName}`,
migrationColumn.columnName,
new TableColumn({
name: migrationColumn.columnName,
type: migrationColumn.columnType,
default: migrationColumn.defaultValue,
isNullable: migrationColumn.isNullable,
}),
);
}
}
private async createForeignKey(
queryRunner: QueryRunner,
schemaName: string,