diff --git a/packages/backend/src/db/migrations/20220928162525_soft-delete-base-model.ts b/packages/backend/src/db/migrations/20220928162525_soft-delete-base-model.ts new file mode 100644 index 00000000..862b6691 --- /dev/null +++ b/packages/backend/src/db/migrations/20220928162525_soft-delete-base-model.ts @@ -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 { + 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 { + 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'); +} diff --git a/packages/backend/src/helpers/pagination.ts b/packages/backend/src/helpers/pagination.ts index b283dd93..eb0a760a 100644 --- a/packages/backend/src/helpers/pagination.ts +++ b/packages/backend/src/helpers/pagination.ts @@ -1,7 +1,8 @@ -import { QueryBuilder, Model } from 'objection'; +import { Model } from 'objection'; +import ExtendedQueryBuilder from '../models/query-builder'; const paginate = async ( - query: QueryBuilder, + query: ExtendedQueryBuilder, limit: number, offset: number ) => { diff --git a/packages/backend/src/models/base.ts b/packages/backend/src/models/base.ts index 95c40746..6bfdc60d 100644 --- a/packages/backend/src/models/base.ts +++ b/packages/backend/src/models/base.ts @@ -2,9 +2,15 @@ import { AjvValidator, Model, snakeCaseMappers } from 'objection'; import type { QueryContext, ModelOptions, ColumnNameMappers } from 'objection'; import addFormats from 'ajv-formats'; +import ExtendedQueryBuilder from './query-builder'; + class Base extends Model { createdAt!: string; updatedAt!: string; + deletedAt: string; + + QueryBuilderType!: ExtendedQueryBuilder; + static QueryBuilder = ExtendedQueryBuilder; static get columnNameMappers(): ColumnNameMappers { return snakeCaseMappers(); @@ -30,10 +36,10 @@ class Base extends Model { this.updatedAt = new Date().toISOString(); } - async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise { + async $beforeUpdate(opts: ModelOptions, queryContext: QueryContext): Promise { this.updatedAt = new Date().toISOString(); - await super.$beforeUpdate(opt, queryContext); + await super.$beforeUpdate(opts, queryContext); } } diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts index fea848e1..bb4015e0 100644 --- a/packages/backend/src/models/flow.ts +++ b/packages/backend/src/models/flow.ts @@ -1,5 +1,6 @@ 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 Step from './step'; import Execution from './execution'; @@ -36,7 +37,7 @@ class Flow extends Base { from: 'flows.id', to: 'steps.flow_id', }, - filter(builder: QueryBuilder) { + filter(builder: ExtendedQueryBuilder) { builder.orderBy('position', 'asc'); }, }, diff --git a/packages/backend/src/models/query-builder.ts b/packages/backend/src/models/query-builder.ts new file mode 100644 index 00000000..8d960068 --- /dev/null +++ b/packages/backend/src/models/query-builder.ts @@ -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 extends Model.QueryBuilder { + ArrayQueryBuilderType!: ExtendedQueryBuilder; + SingleQueryBuilderType!: ExtendedQueryBuilder; + MaybeSingleQueryBuilderType!: ExtendedQueryBuilder; + NumberQueryBuilderType!: ExtendedQueryBuilder; + PageQueryBuilderType!: ExtendedQueryBuilder>; + + static forClass: ForClassMethod = buildQueryBuidlerForClass(); + + delete() { + return this.patch({ + [DELETED_COLUMN_NAME]: (new Date()).toISOString(), + } as unknown as PartialModelObject); + } + + hardDelete() { + return super.delete(); + } + + withSoftDeleted() { + this.context().withSoftDeleted = true; + return this; + } + + restore() { + return this.patch({ + [DELETED_COLUMN_NAME]: null, + } as unknown as PartialModelObject); + } +} + +export default ExtendedQueryBuilder; diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts index 35600ba1..99908d41 100644 --- a/packages/backend/src/models/user.ts +++ b/packages/backend/src/models/user.ts @@ -13,6 +13,7 @@ class User extends Base { connections?: Connection[]; flows?: Flow[]; steps?: Step[]; + executions?: Execution[]; static tableName = 'users';