feat(core): Add enum check helper to migration DSL (#30900)

This commit is contained in:
Tomi Turtiainen 2026-05-27 09:38:42 +03:00 committed by GitHub
parent d4f9223842
commit 1b9dfb20c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 87 additions and 13 deletions

View File

@ -2,7 +2,7 @@ import type { Driver, QueryRunner, Table } from '@n8n/typeorm';
import { mock } from 'jest-mock-extended';
import { Column } from '../column';
import { AddColumns, CreateTable, DropEnumCheck } from '../table';
import { AddColumns, AddEnumCheck, CreateTable, DropEnumCheck } from '../table';
const createMocks = (escapeChar = '"') => {
const driver = mock<Driver>();
@ -201,3 +201,43 @@ describe('AddColumns with column-level enum checks', () => {
]);
});
});
describe('AddEnumCheck', () => {
it('should create the check constraint with correct name and expression', async () => {
const { queryRunner } = createMocks();
await new AddEnumCheck('test_table', 'status', ['active', 'inactive'], 'n8n_', queryRunner);
expect(queryRunner.createCheckConstraints).toHaveBeenCalledWith('n8n_test_table', [
expect.objectContaining({
name: 'CHK_n8n_test_table_status',
expression: "\"status\" IN ('active', 'inactive')",
}),
]);
});
it('should work without a table prefix', async () => {
const { queryRunner } = createMocks();
await new AddEnumCheck('test_table', 'role', ['admin', 'user', 'guest'], '', queryRunner);
expect(queryRunner.createCheckConstraints).toHaveBeenCalledWith('test_table', [
expect.objectContaining({
name: 'CHK_test_table_role',
expression: "\"role\" IN ('admin', 'user', 'guest')",
}),
]);
});
it('should escape single quotes in enum values', async () => {
const { queryRunner } = createMocks();
await new AddEnumCheck('test_table', 'label', ["it's", "they're"], '', queryRunner);
expect(queryRunner.createCheckConstraints).toHaveBeenCalledWith('test_table', [
expect.objectContaining({
expression: "\"label\" IN ('it''s', 'they''re')",
}),
]);
});
});

View File

@ -4,6 +4,7 @@ import { Column } from './column';
import { CreateIndex, DropIndex } from './indices';
import {
AddColumns,
AddEnumCheck,
AddForeignKey,
AddNotNull,
CreateTable,
@ -95,6 +96,10 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne
dropNotNull: (tableName: string, columnName: string) =>
new DropNotNull(tableName, columnName, tablePrefix, queryRunner),
/** WARNING: This recreates the entire table on SQLite. */
addEnumCheck: (tableName: string, columnName: string, values: string[]) =>
new AddEnumCheck(tableName, columnName, values, tablePrefix, queryRunner),
/** WARNING: This recreates the entire table on SQLite. */
dropEnumCheck: (tableName: string, columnName: string) =>
new DropEnumCheck(tableName, columnName, tablePrefix, queryRunner),

View File

@ -5,24 +5,32 @@ import LazyPromise from 'p-lazy';
import { Column } from './column';
function buildEnumCheck(
columnName: string,
values: string[],
prefix: string,
tableName: string,
driver: Driver,
): TableCheck {
const checkName = `CHK_${prefix}${tableName}_${columnName}`;
const escapedColumnName = driver.escape(columnName);
const escapedValues = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(', ');
const expression = `${escapedColumnName} IN (${escapedValues})`;
return new TableCheck({ name: checkName, expression });
}
function buildEnumChecks(
columns: Column[],
prefix: string,
tableName: string,
driver: Driver,
): TableCheck[] {
const checks: TableCheck[] = [];
for (const column of columns) {
const enumCheck = column.getEnumCheck();
if (enumCheck) {
const checkName = `CHK_${prefix}${tableName}_${enumCheck.columnName}`;
const escapedColumnName = driver.escape(enumCheck.columnName);
const escapedValues = enumCheck.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(', ');
const expression = `${escapedColumnName} IN (${escapedValues})`;
checks.push(new TableCheck({ name: checkName, expression }));
}
}
return checks;
return columns
.map((column) => column.getEnumCheck())
.filter((enumCheck) => enumCheck !== undefined)
.map((enumCheck) =>
buildEnumCheck(enumCheck.columnName, enumCheck.values, prefix, tableName, driver),
);
}
abstract class TableOperation<R = void> extends LazyPromise<R> {
@ -286,3 +294,24 @@ export class DropEnumCheck extends TableOperation {
return await queryRunner.dropCheckConstraint(fullTableName, checkName);
}
}
export class AddEnumCheck extends TableOperation {
constructor(
tableName: string,
protected columnName: string,
protected values: string[],
prefix: string,
queryRunner: QueryRunner,
) {
super(tableName, prefix, queryRunner);
}
async execute(queryRunner: QueryRunner) {
const { tableName, prefix, columnName, values } = this;
const { driver } = queryRunner.connection;
const fullTableName = `${prefix}${tableName}`;
return await queryRunner.createCheckConstraints(fullTableName, [
buildEnumCheck(columnName, values, prefix, tableName, driver),
]);
}
}