Merge pull request #554 from automatisch/issue-553
feat: soft delete database entities
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
async function addDeletedColumn(knex: Knex, tableName: string) {
|
||||||
|
return await knex.schema.table(tableName, (table) => {
|
||||||
|
table.timestamp('deleted_at').nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dropDeletedColumn(knex: Knex, tableName: string) {
|
||||||
|
return await knex.schema.table(tableName, (table) => {
|
||||||
|
table.dropColumn('deleted_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await addDeletedColumn(knex, 'steps');
|
||||||
|
await addDeletedColumn(knex, 'flows');
|
||||||
|
await addDeletedColumn(knex, 'executions');
|
||||||
|
await addDeletedColumn(knex, 'execution_steps');
|
||||||
|
await addDeletedColumn(knex, 'users');
|
||||||
|
await addDeletedColumn(knex, 'connections');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await dropDeletedColumn(knex, 'steps');
|
||||||
|
await dropDeletedColumn(knex, 'flows');
|
||||||
|
await dropDeletedColumn(knex, 'executions');
|
||||||
|
await dropDeletedColumn(knex, 'execution_steps');
|
||||||
|
await dropDeletedColumn(knex, 'users');
|
||||||
|
await dropDeletedColumn(knex, 'connections');
|
||||||
|
}
|
@@ -1,7 +1,8 @@
|
|||||||
import { QueryBuilder, Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import ExtendedQueryBuilder from '../models/query-builder';
|
||||||
|
|
||||||
const paginate = async (
|
const paginate = async (
|
||||||
query: QueryBuilder<Model, Model[]>,
|
query: ExtendedQueryBuilder<Model, Model[]>,
|
||||||
limit: number,
|
limit: number,
|
||||||
offset: number
|
offset: number
|
||||||
) => {
|
) => {
|
||||||
|
@@ -2,9 +2,15 @@ import { AjvValidator, Model, snakeCaseMappers } from 'objection';
|
|||||||
import type { QueryContext, ModelOptions, ColumnNameMappers } from 'objection';
|
import type { QueryContext, ModelOptions, ColumnNameMappers } from 'objection';
|
||||||
import addFormats from 'ajv-formats';
|
import addFormats from 'ajv-formats';
|
||||||
|
|
||||||
|
import ExtendedQueryBuilder from './query-builder';
|
||||||
|
|
||||||
class Base extends Model {
|
class Base extends Model {
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
updatedAt!: string;
|
updatedAt!: string;
|
||||||
|
deletedAt: string;
|
||||||
|
|
||||||
|
QueryBuilderType!: ExtendedQueryBuilder<this>;
|
||||||
|
static QueryBuilder = ExtendedQueryBuilder;
|
||||||
|
|
||||||
static get columnNameMappers(): ColumnNameMappers {
|
static get columnNameMappers(): ColumnNameMappers {
|
||||||
return snakeCaseMappers();
|
return snakeCaseMappers();
|
||||||
@@ -30,10 +36,10 @@ class Base extends Model {
|
|||||||
this.updatedAt = new Date().toISOString();
|
this.updatedAt = new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise<void> {
|
async $beforeUpdate(opts: ModelOptions, queryContext: QueryContext): Promise<void> {
|
||||||
this.updatedAt = new Date().toISOString();
|
this.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
await super.$beforeUpdate(opts, queryContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { ValidationError } from 'objection';
|
import { ValidationError } from 'objection';
|
||||||
import type { ModelOptions, QueryContext, QueryBuilder } from 'objection';
|
import type { ModelOptions, QueryContext } from 'objection';
|
||||||
|
import ExtendedQueryBuilder from './query-builder';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
@@ -36,7 +37,7 @@ class Flow extends Base {
|
|||||||
from: 'flows.id',
|
from: 'flows.id',
|
||||||
to: 'steps.flow_id',
|
to: 'steps.flow_id',
|
||||||
},
|
},
|
||||||
filter(builder: QueryBuilder<Step>) {
|
filter(builder: ExtendedQueryBuilder<Step>) {
|
||||||
builder.orderBy('position', 'asc');
|
builder.orderBy('position', 'asc');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
48
packages/backend/src/models/query-builder.ts
Normal file
48
packages/backend/src/models/query-builder.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Model, Page, PartialModelObject, ForClassMethod, AnyQueryBuilder } from "objection";
|
||||||
|
|
||||||
|
const DELETED_COLUMN_NAME = 'deleted_at';
|
||||||
|
|
||||||
|
const buildQueryBuidlerForClass = (): ForClassMethod => {
|
||||||
|
return (modelClass) => {
|
||||||
|
const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call(ExtendedQueryBuilder, modelClass);
|
||||||
|
qb.onBuild((builder) => {
|
||||||
|
if (!builder.context().withSoftDeleted) {
|
||||||
|
builder.whereNull(`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return qb;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder<M, R> {
|
||||||
|
ArrayQueryBuilderType!: ExtendedQueryBuilder<M, M[]>;
|
||||||
|
SingleQueryBuilderType!: ExtendedQueryBuilder<M, M>;
|
||||||
|
MaybeSingleQueryBuilderType!: ExtendedQueryBuilder<M, M | undefined>;
|
||||||
|
NumberQueryBuilderType!: ExtendedQueryBuilder<M, number>;
|
||||||
|
PageQueryBuilderType!: ExtendedQueryBuilder<M, Page<M>>;
|
||||||
|
|
||||||
|
static forClass: ForClassMethod = buildQueryBuidlerForClass();
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
return this.patch({
|
||||||
|
[DELETED_COLUMN_NAME]: (new Date()).toISOString(),
|
||||||
|
} as unknown as PartialModelObject<M>);
|
||||||
|
}
|
||||||
|
|
||||||
|
hardDelete() {
|
||||||
|
return super.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
withSoftDeleted() {
|
||||||
|
this.context().withSoftDeleted = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
return this.patch({
|
||||||
|
[DELETED_COLUMN_NAME]: null,
|
||||||
|
} as unknown as PartialModelObject<M>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtendedQueryBuilder;
|
@@ -13,6 +13,7 @@ class User extends Base {
|
|||||||
connections?: Connection[];
|
connections?: Connection[];
|
||||||
flows?: Flow[];
|
flows?: Flow[];
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
|
executions?: Execution[];
|
||||||
|
|
||||||
static tableName = 'users';
|
static tableName = 'users';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user