From 1b9dfb20c4a345510ec86a4ed888a8287aa16064 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Wed, 27 May 2026 09:38:42 +0300 Subject: [PATCH] feat(core): Add enum check helper to migration DSL (#30900) --- .../dsl/__tests__/enum-check.test.ts | 42 ++++++++++++++- packages/@n8n/db/src/migrations/dsl/index.ts | 5 ++ packages/@n8n/db/src/migrations/dsl/table.ts | 53 ++++++++++++++----- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts b/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts index 4a84babb024..6f3bbd4ff05 100644 --- a/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts +++ b/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts @@ -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(); @@ -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')", + }), + ]); + }); +}); diff --git a/packages/@n8n/db/src/migrations/dsl/index.ts b/packages/@n8n/db/src/migrations/dsl/index.ts index c08ad595f42..1e349172143 100644 --- a/packages/@n8n/db/src/migrations/dsl/index.ts +++ b/packages/@n8n/db/src/migrations/dsl/index.ts @@ -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), diff --git a/packages/@n8n/db/src/migrations/dsl/table.ts b/packages/@n8n/db/src/migrations/dsl/table.ts index 0285e88dcfa..1ca54fc292c 100644 --- a/packages/@n8n/db/src/migrations/dsl/table.ts +++ b/packages/@n8n/db/src/migrations/dsl/table.ts @@ -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 extends LazyPromise { @@ -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), + ]); + } +}