diff --git a/packages/datasource-sequelize/src/collection.ts b/packages/datasource-sequelize/src/collection.ts index a2d57e4d4d..d197084cf8 100644 --- a/packages/datasource-sequelize/src/collection.ts +++ b/packages/datasource-sequelize/src/collection.ts @@ -1,4 +1,6 @@ +import type { SequelizeDatasourceOptions } from './types'; import type { + ActionResult, AggregateResult, Aggregation, Caller, @@ -43,6 +45,7 @@ export default class SequelizeCollection extends BaseCollection { // eslint-disable-next-line @typescript-eslint/no-explicit-any model: ModelDefined, logger?: Logger, + options?: SequelizeDatasourceOptions, ) { if (!model) throw new Error('Invalid (null) model instance.'); @@ -63,9 +66,9 @@ export default class SequelizeCollection extends BaseCollection { rawQuery: async ( sql: string, replacements: Replacements, - options?: { syntax?: 'bind' | 'replacements' }, + opts?: { syntax?: 'bind' | 'replacements' }, ) => { - const opt = { syntax: 'replacements', ...options }; + const opt = { syntax: 'replacements', ...opts }; const result = await model.sequelize.query(sql, { type: QueryTypes.RAW, plain: false, @@ -89,6 +92,29 @@ export default class SequelizeCollection extends BaseCollection { this.enableCount(); this.addFields(modelSchema.fields); this.addSegments(modelSchema.segments); + + if (this.model.options.paranoid && options?.actions) { + const { hardDelete, restoreSoftDeleted } = options.actions; + + if ((Array.isArray(hardDelete) && hardDelete.includes(name)) || hardDelete === true) { + this.addAction('Hard Delete', { + scope: 'Bulk', + description: 'Permanently deletes selected records', + staticForm: true, + }); + } + + if ( + (Array.isArray(restoreSoftDeleted) && restoreSoftDeleted.includes(name)) || + restoreSoftDeleted === true + ) { + this.addAction('Restore Soft Deleted', { + scope: 'Bulk', + description: 'Restores selected records', + staticForm: true, + }); + } + } } async create(caller: Caller, data: RecordData[]): Promise { @@ -226,4 +252,39 @@ export default class SequelizeCollection extends BaseCollection { !aggregationFieldSchema || aggregationFieldSchema?.columnType === 'Number', ); } + + override async execute( + caller: Caller, + name: string, + formValues: RecordData, + filter?: Filter, + ): Promise { + const options = { + where: await this.queryConverter.getWhereFromConditionTreeToByPassInclude( + filter.conditionTree, + ), + }; + + if (name === 'Hard Delete') { + try { + await handleErrors('delete', () => this.model.destroy({ ...options, force: true })); + } catch (error) { + return { message: error.message, type: 'Error' }; + } + + return { message: 'Records permanently deleted', type: 'Success', invalidated: new Set() }; + } + + if (name === 'Restore Soft Deleted') { + try { + await handleErrors('update', () => this.model.restore(options)); + } catch (error) { + return { message: error.message, type: 'Error' }; + } + + return { message: 'Records restored', type: 'Success', invalidated: new Set() }; + } + + return super.execute(caller, name, formValues, filter); + } } diff --git a/packages/datasource-sequelize/src/datasource.ts b/packages/datasource-sequelize/src/datasource.ts index 9e9a642875..0866b552a6 100644 --- a/packages/datasource-sequelize/src/datasource.ts +++ b/packages/datasource-sequelize/src/datasource.ts @@ -28,7 +28,7 @@ export default class SequelizeDataSource extends BaseDataSource (modelA.name > modelB.name ? 1 : -1)) .forEach(model => { - const collection = new SequelizeCollection(model.name, this, model, logger); + const collection = new SequelizeCollection(model.name, this, model, logger, options); this.addCollection(collection); }); } diff --git a/packages/datasource-sequelize/src/types.ts b/packages/datasource-sequelize/src/types.ts index 5179080d62..be741c94fc 100644 --- a/packages/datasource-sequelize/src/types.ts +++ b/packages/datasource-sequelize/src/types.ts @@ -1,3 +1,14 @@ export type SequelizeDatasourceOptions = { liveQueryConnections?: string; + actions?: { + /** + * If true, add an action to restore soft deleted records. + * If an array of strings is provided, add an action to restore soft deleted records only for the specified collections. + */ + restoreSoftDeleted?: boolean | string[]; + /** If true, add an action will permanently delete records. + * If an array of strings is provided, add an action to permanently delete records only for the specified collections. + */ + hardDelete?: boolean | string[]; + }; }; diff --git a/packages/datasource-sql/src/index.ts b/packages/datasource-sql/src/index.ts index 582f43e290..900afac22c 100644 --- a/packages/datasource-sql/src/index.ts +++ b/packages/datasource-sql/src/index.ts @@ -104,6 +104,7 @@ export function createSqlDataSource( return new SqlDatasource( new SequelizeDataSource(sequelize, logger, { liveQueryConnections: options?.liveQueryConnections, + actions: options?.actions, }), latestIntrospection.views, ); diff --git a/packages/datasource-sql/src/types.ts b/packages/datasource-sql/src/types.ts index b2b710d74a..fca2a0ce79 100644 --- a/packages/datasource-sql/src/types.ts +++ b/packages/datasource-sql/src/types.ts @@ -50,4 +50,15 @@ export type SqlDatasourceOptions = { introspection?: SupportedIntrospection; displaySoftDeleted?: string[] | true; liveQueryConnections?: string; + actions?: { + /** + * If true, add an action to restore soft deleted records. + * If an array of strings is provided, add an action to restore soft deleted records only for the specified collections. + */ + restoreSoftDeleted?: boolean | string[]; + /** If true, add an action will permanently delete records. + * If an array of strings is provided, add an action to permanently delete records only for the specified collections. + */ + hardDelete?: boolean | string[]; + }; };