Gathering detailed insights and metrics for objection-soft-delete
Gathering detailed insights and metrics for objection-soft-delete
Gathering detailed insights and metrics for objection-soft-delete
Gathering detailed insights and metrics for objection-soft-delete
objection-js-soft-delete
A plugin for objection js that supports soft delete
@jonstuebe/objection-soft-delete
A plugin for objection js to support soft delete functionallity
@knax/objection-soft-delete
A plugin for objection js to support soft delete functionallity
objection-paranoia
A Objectionjs plugin to add soft delete.
npm install objection-soft-delete
Typescript
Module System
Node Version
NPM Version
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
A plugin that adds soft-delete functionality to Objection.js
1npm i objection-soft-delete --save
1yarn add objection-soft-delete
1// Import objection model. 2const Model = require('objection').Model; 3 4// Import the plugin 5const softDelete = require('objection-soft-delete'); 6 7// Mixin the plugin and specify the column to to use. 'deleted' will be used if none is specified: 8class User extends softDelete({ columnName: 'deleted' })(Model) { 9 static get tableName() { 10 return 'Users'; 11 } 12 13 static get jsonSchema() { 14 return { 15 type: 'object', 16 required: [], 17 18 properties: { 19 id: { type: 'integer' }, 20 // matches the columnName passed above 21 deleted: { type: 'boolean' }, 22 // other columns 23 }, 24 } 25 } 26}
deleted
field of your table has a default value of false
(and, while not required, you'll probably want to make it not nullable as well). A deleted
value of NULL
will result in this plugin producing unexpected behavior..delete()
or .del()
is called for that model, the matching row(s) are flagged true
instead of deleted1await User.query().where('id', 1).delete(); // db now has: { User id: 1, deleted: true, ... }
1const user = await User.query().where('id', 1).first(); 2await user.$query().delete(); // same
1const deletedUser = await User.query().where('id', 1).first(); // => { User id: 1, deleted: true, ... }
1const activeUsers = await User.query().whereNotDeleted();
1const deletedUsers = await User.query().whereDeleted();
1await User.query().where('id', 1).undelete(); // db now has: { User id: 1, deleted: false, ... }
1await User.query.where('id', 1).hardDelete(); // => row with id:1 is permanently deleted
.eager()
or .joinRelation()
A 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:
1// some other Model with a relation to the `User` model: 2const group = await UserGroup.query() 3 .where('id', 1) 4 .first() 5 .eager('users(notDeleted)'); // => now group.users contains only records that are not deleted
Or:
1// some other Model with a relation to the `User` model: 2const group = await UserGroup.query() 3 .where('id', 1) 4 .first() 5 .eager('users(deleted)'); // => now group.users contains only records that are deleted
With .joinRelation()
:
1// some other Model with a relation to the `User` model: 2const group = await UserGroup.query() 3 .where('id', 1) 4 .joinRelation('users(notDeleted)') 5 .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:
1// some other class that has a FK to User: 2class UserGroup extends Model { 3 static get tableName() { 4 return 'UserGroups'; 5 } 6 7 ... 8 9 static get relationMappings() { 10 return { 11 users: { 12 relation: Model.ManyToManyRelation, 13 modelClass: User, 14 join: { 15 from: 'UserGroups.id', 16 through: { 17 from: 'GroupUsers.groupId', 18 to: 'GroupUsers.userId', 19 }, 20 to: 'Users.id', 21 }, 22 filter: (f) => { 23 f.whereNotDeleted(); // or f.whereDeleted(), as needed. 24 }, 25 }, 26 } 27 } 28}
then:
1const group = await UserGroup.query()
2 .where('id', 1)
3 .first()
4 .eager('users'); // => `User` rows are filtered out automatically without having to specify the filter here
columnName
If 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:
1class User extends softDelete({ columnName: 'deleted' })(Model) { 2 ... 3} 4 5class UserGroup extends softDelete({ columnName: 'inactive' })(Model) { 6 ... 7} 8 9// everything will work as expected: 10await User.query() 11 .whereNotDeleted(); // => all undeleted users 12 13await UserGroup.query() 14 .whereNotDeleted(); // => all undeleted user groups 15 16await UserGroup.query() 17 .whereNotDeleted() 18 .eager('users(notDeleted)'); // => all undeleted user groups, with all related undeleted users eagerly loaded 19 20await User.query() 21 .whereDeleted() 22 .eager('groups(deleted)'); // => all deleted users, with all related deleted user groups eagerly loaded 23 24await User.query() 25 .whereNotDeleted() 26 .joinRelation('groups(notDeleted)') 27 .where('groups.name', 'like', '%local%') 28 .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. 29 30// and so on...
.upsertGraph()
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()
:
1// a model with soft delete 2class Phone extends softDelete(Model) { 3 static get tableName() { 4 return 'Phones'; 5 } 6} 7 8// a model without soft delete 9class Email extends Model { 10 static get tableName() { 11 return 'Emails'; 12 } 13} 14 15// assume a User model that relates to both, and the following existing data: 16User { 17 id: 1, 18 name: 'Johnny Cash', 19 phones: [ 20 { 21 id: 6, 22 number: '+19195551234', 23 }, 24 ], 25 emails: [ 26 { 27 id: 3, 28 address: 'mib@americanrecords.com', 29 }, 30 ] 31} 32 33// then: 34 35await User.query().upsertGraph({ 36 id: 1, 37 name: 'Johnny Cash', 38 phones: [], 39 emails: [], 40}); // => phone id 6 will be flagged deleted (and will still be related to Johnny!), email id 3 will be removed from the database
One issue that comes with doing soft deletes is that your calls to .delete()
will actually trigger lifecycle functions for .update()
, which may not be expected or desired. To help address this, some context flags have been added to the queryContext
that is passed into lifecycle functions to help discern whether the event that triggered (e.g.) $beforeUpdate
was a true update, a soft delete, or an undelete:
1 $beforeUpdate(opt, queryContext) { 2 if (queryContext.softDelete) { 3 // do something before a soft delete, possibly including calling your $beforeDelete function. 4 // Think this through carefully if you are using additional plugins, as their lifecycle 5 // functions may execute before this one depending on how you have set up your inheritance chain! 6 } else if (queryContext.undelete) { 7 // do something before an undelete 8 } else { 9 // do something before a normal update 10 } 11 } 12 13 // same procedure for $afterUpdate
Available flags are:
Flags will be true
if set, and undefined
otherwise.
All models with the soft delete mixin will have an isSoftDelete
property, which returns true
.
columnName: 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:
1const softDelete = require('objection-soft-delete')({ 2 columnName: 'inactive', 3});
deletedValue: you can set this option to allow a different value than "true" to be set in the specified column. For instance, you can use the following code to make a timestamp (you need knex instance to do so)
1const softDelete = require('objection-soft-delete')({ 2 columnName: 'deleted_at', 3 deletedValue: knex.fn.now(), 4});
notDeletedValue: you can set (and should) this option along with deletedValue
to allow a different value than "false" to be set in the specified column.
For instance, you can use the following code to restore the column to null (you need knex instance to do so)
1const softDelete = require('objection-soft-delete')({ 2 columnName: 'deleted_at', 3 deletedValue: knex.fn.now(), 4 notDeletedValue: null, 5});
Tests can be run with:
1npm test
or:
1yarn test
The linter can be run with:
1npm run lint
or:
1yarn lint
The 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.
No vulnerabilities found.
No security vulnerabilities found.