A plugin that adds soft-delete functionality to Objection.js
npm i objection-soft-delete --saveyarn add objection-soft-deleteMixin the plugin on an object representing a table that uses a boolean column as a flag for soft delete
// Import objection model.constModel=require('objection').Model;// Import the pluginconstsoftDelete=require('objection-soft-delete');// Mixin the plugin and specify the column to to use. 'deleted' will be used if none is specified:classUserextendssoftDelete({columnName: 'deleted'})(Model){staticgettableName(){return'Users';}staticgetjsonSchema(){return{type: 'object',required: [],properties: {id: {type: 'integer'},// matches the columnName passed abovedeleted: {type: 'boolean'},// other columns},}}}When .delete() or .del() is called for that model, the matching row(s) are flagged true instead of deleted
awaitUser.query().where('id',1).delete();// db now has: { User id: 1, deleted: true, ... }constuser=awaitUser.query().where('id',1).first();awaituser.$query().delete();// sameconstdeletedUser=awaitUser.query().where('id',1).first();// => { User id: 1, deleted: true, ... }constactiveUsers=awaitUser.query().whereNotDeleted();constdeletedUsers=awaitUser.query().whereDeleted();awaitUser.query().where('id',1).undelete();// db now has: { User id: 1, deleted: false, ... }awaitUser.query.where('id',1).hardDelete();// => row with id:1 is permanently deletedA notDeleted and a deleted filter will be added to the list of named filters for any model that mixes in the plugin. These filters use the .whereNotDeleted() and .whereDeleted() functions to filter records, and can be used without needing to remember the specific columnName for any model:
// some other Model with a relation to the `User` model:constgroup=awaitUserGroup.query().where('id',1).first().eager('users(notDeleted)');// => now group.users contains only records that are not deletedOr:
// some other Model with a relation to the `User` model:constgroup=awaitUserGroup.query().where('id',1).first().eager('users(deleted)');// => now group.users contains only records that are deletedWith .joinRelation():
// some other Model with a relation to the `User` model:constgroup=awaitUserGroup.query().where('id',1).joinRelation('users(notDeleted)').where('users.firstName','like','a%');// => all groups that have an undeleted user whose first name starts with 'a';A filter can be applied directly to the relationship definition to ensure that deleted/undeleted rows never appear:
// some other class that has a FK to User:classUserGroupextendsModel{staticgettableName(){return'UserGroups';}
...
staticgetrelationMappings(){return{users: {relation: Model.ManyToManyRelation,modelClass: User,join: {from: 'UserGroups.id',through: {from: 'GroupUsers.groupId',to: 'GroupUsers.userId',},to: 'Users.id',},filter: (f)=>{f.whereNotDeleted();// or f.whereDeleted(), as needed.},},}}}then:
constgroup=awaitUserGroup.query().where('id',1).first().eager('users');// => `User` rows are filtered out automatically without having to specify the filter hereIf for some reason you have to deal with different column names for different models (legacy code/schemas can be a bear!), all functionality is fully supported:
classUserextendssoftDelete({columnName: 'deleted'})(Model){
...
}classUserGroupextendssoftDelete({columnName: 'inactive'})(Model){
...
}// everything will work as expected:awaitUser.query().whereNotDeleted();// => all undeleted usersawaitUserGroup.query().whereNotDeleted();// => all undeleted user groupsawaitUserGroup.query().whereNotDeleted().eager('users(notDeleted)');// => all undeleted user groups, with all related undeleted users eagerly loadedawaitUser.query().whereDeleted().eager('groups(deleted)');// => all deleted users, with all related deleted user groups eagerly loadedawaitUser.query().whereNotDeleted().joinRelation('groups(notDeleted)').where('groups.name','like','%local%').eager('groups(notDeleted)');// => all undeleted users that belong to undeleted user groups that have a name containing the string 'local', eagerly load all undeleted groups for said users.// and so on...This plugin was actually born out of a need to have .upsertGraph() soft delete in some tables, and hard delete in others, so it plays nice with
.upsertGraph():
// a model with soft deleteclassPhoneextendssoftDelete(Model){staticgettableName(){return'Phones';}}// a model without soft deleteclassEmailextendsModel{staticgettableName(){return'Emails';}}// assume a User model that relates to both, and the following existing data:User{id: 1,name: 'Johnny Cash',phones: [{id: 6,number: '+19195551234',},],emails: [{id: 3,address: 'mib@americanrecords.com',},]}// then:awaitUser.query().upsertGraph({id: 1,name: 'Johnny Cash',phones: [],emails: [],});// => phone id 6 will be flagged deleted (and will still be related to Johnny!), email id 3 will be removed from the databasecolumnName: the name of the column to use as the soft delete flag on the model (Default: 'deleted'). The column must exist on the table for the model.
You can specify different column names per-model by using the options:
constsoftDelete=require('objection-soft-delete')({columnName: 'inactive',});Tests can be run with:
npm testor:
yarn testThe linter can be run with:
npm run lintor:
yarn lintThe usual spiel: fork, fix/improve, write tests, submit PR. I try to maintain a (mostly) consistent syntax, but am open to suggestions for improvement. Otherwise, the only two rules are: do good work, and no tests = no merge.