Files
Automata/packages/cli/src/databases/dsl/Table.ts
Iván Ovejero 8a28e98ec8 fix(core): Disallow orphan executions (#7069)
Until https://github.com/n8n-io/n8n/pull/7061 we had an edge case where
a manual unsaved workflow when run creates an orphan execution, i.e. a
saved execution not pointing to any workflow. This execution is only
ever visible to the instance owner (even if triggered by a member), and
is wrongly stored as unfinished and crashed. This PR enforces that the
DB disallows any such executions from making it into the DB.

This is needed also for the S3 client, which will include the
`workflowId` in the path-like filename.

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
2023-09-04 16:57:10 +02:00

163 lines
4.3 KiB
TypeScript

import type { TableForeignKeyOptions, TableIndexOptions } from 'typeorm';
import { Table, QueryRunner, TableColumn } from 'typeorm';
import LazyPromise from 'p-lazy';
import { Column } from './Column';
abstract class TableOperation<R = void> extends LazyPromise<R> {
abstract execute(queryRunner: QueryRunner): Promise<R>;
constructor(
protected tableName: string,
protected prefix: string,
queryRunner: QueryRunner,
) {
super((resolve, reject) => {
void this.execute(queryRunner).then(resolve).catch(reject);
});
}
}
export class CreateTable extends TableOperation {
private columns: Column[] = [];
private indices = new Set<TableIndexOptions>();
private foreignKeys = new Set<TableForeignKeyOptions>();
withColumns(...columns: Column[]) {
this.columns.push(...columns);
return this;
}
get withTimestamps() {
this.columns.push(
new Column('createdAt').timestamp().notNull.default('NOW()'),
new Column('updatedAt').timestamp().notNull.default('NOW()'),
);
return this;
}
withIndexOn(columnName: string | string[], isUnique = false) {
const columnNames = Array.isArray(columnName) ? columnName : [columnName];
this.indices.add({ columnNames, isUnique });
return this;
}
withForeignKey(
columnName: string,
ref: { tableName: string; columnName: string; onDelete?: 'CASCADE'; onUpdate?: 'CASCADE' },
) {
const foreignKey: TableForeignKeyOptions = {
columnNames: [columnName],
referencedTableName: `${this.prefix}${ref.tableName}`,
referencedColumnNames: [ref.columnName],
};
if (ref.onDelete) foreignKey.onDelete = ref.onDelete;
if (ref.onUpdate) foreignKey.onUpdate = ref.onUpdate;
this.foreignKeys.add(foreignKey);
return this;
}
async execute(queryRunner: QueryRunner) {
const { driver } = queryRunner.connection;
const { columns, tableName: name, prefix, indices, foreignKeys } = this;
return queryRunner.createTable(
new Table({
name: `${prefix}${name}`,
columns: columns.map((c) => c.toOptions(driver)),
...(indices.size ? { indices: [...indices] } : {}),
...(foreignKeys.size ? { foreignKeys: [...foreignKeys] } : {}),
...('mysql' in driver ? { engine: 'InnoDB' } : {}),
}),
true,
);
}
}
export class DropTable extends TableOperation {
async execute(queryRunner: QueryRunner) {
const { tableName: name, prefix } = this;
return queryRunner.dropTable(`${prefix}${name}`, true);
}
}
export class AddColumns extends TableOperation {
constructor(
tableName: string,
protected columns: Column[],
prefix: string,
queryRunner: QueryRunner,
) {
super(tableName, prefix, queryRunner);
}
async execute(queryRunner: QueryRunner) {
const { driver } = queryRunner.connection;
const { tableName, prefix, columns } = this;
return queryRunner.addColumns(
`${prefix}${tableName}`,
columns.map((c) => new TableColumn(c.toOptions(driver))),
);
}
}
export class DropColumns extends TableOperation {
constructor(
tableName: string,
protected columnNames: string[],
prefix: string,
queryRunner: QueryRunner,
) {
super(tableName, prefix, queryRunner);
}
async execute(queryRunner: QueryRunner) {
const { tableName, prefix, columnNames } = this;
return queryRunner.dropColumns(`${prefix}${tableName}`, columnNames);
}
}
class ModifyNotNull extends TableOperation {
constructor(
tableName: string,
protected columnName: string,
protected isNullable: boolean,
prefix: string,
queryRunner: QueryRunner,
) {
super(tableName, prefix, queryRunner);
}
async execute(queryRunner: QueryRunner) {
const { tableName, prefix, columnName, isNullable } = this;
const table = await queryRunner.getTable(`${prefix}${tableName}`);
if (!table) throw new Error(`No table found with the name ${tableName}`);
const oldColumn = table.findColumnByName(columnName)!;
const newColumn = oldColumn.clone();
newColumn.isNullable = isNullable;
return queryRunner.changeColumn(table, oldColumn, newColumn);
}
}
export class AddNotNull extends ModifyNotNull {
constructor(
tableName: string,
protected columnName: string,
prefix: string,
queryRunner: QueryRunner,
) {
super(tableName, columnName, false, prefix, queryRunner);
}
}
export class DropNotNull extends ModifyNotNull {
constructor(
tableName: string,
protected columnName: string,
prefix: string,
queryRunner: QueryRunner,
) {
super(tableName, columnName, true, prefix, queryRunner);
}
}