A plugin that adds soft-delete functionality to Objection.js
- Installation
- Register the plugin
- Usage
- Methods
- .whereNotDeleted()
- .whereDeleted()
- .undelete()
- .hardDelete()
- Filters
- notDeleted
- deleted
- in a relationship
- Using with upsertGraph
- Lifecycle functions
- Methods
npm i objection-js-soft-delete
yarn add objection-js-soft-delete
The mixin provides the following configuration to override the default options:
columnName: the column name that indicate if the record is deleted. The column must exist on the table for the model.
Default: 'deleted_at'
deletedValue: define which value indicate if the record is deleted.
Default: new Date()
(local time zone of the server)
It's also possible to use the time from the database: knex.fn.now()
notDeletedValue: define which value indicate if the record is not deleted. You can set (and should) this option along with deletedValue
.
Default: NULL
// Import objection and the plugin.
import { Model } from 'objection';
import objectionSoftDelete from 'objection-js-soft-delete';
// Specify the options for this plugin. This are the defaults.
const softDelete = objectionSoftDelete({
columnName: 'deleted_at',
deletedValue: new Date(),
notDeletedValue: null,
});
// Inject the plugin to the model
class User extends softDelete(Model) {
static get tableName() {
return 'Users';
}
}
Note: A deletedValue
value of NULL
will result in this plugin an unexpected behavior.
- Soft delete records: When an record will be deleted then the deleted field is set to the value that is specified as deletedValue:
await User.query().where('id', 1).delete();
await User.query().deleteById(1);
// Deleted rows are still in the db:
// User { id: 1, deleted_at: 'Sat Oct 31 2020 15:42:29 GMT+0100 (Central European Standard Time)' , ... }
- .whereNotDeleted(): Returns only records that aren't deleted.
const notDeletedUsers = await User.query().whereNotDeleted();
- .whereDeleted(): Returns only records that are deleted.
const deletedUsers = await User.query().whereDeleted();
- .undelete(): Restore deleted records.
await User.query().where('id', 1).undelete();
// User { id: 1, deleted_at: null }
- .hardDelete(): Permanently delete records.
await User.query.where('id', 1).hardDelete();
A notDeleted
and a deleted
filter will be added to the list of named filters for any model that use this mixin. Internally they are using the methods .whereNotDeleted()
and .whereDeleted()
from above:
- notDeleted: Returns only records from the relation that are not deleted.
// withGraphFetched
const rows = await User.query().withGraphFetched('contact(notDeleted)');
// withGraphJoined
const rows = await User.query().withGraphJoined('contact(notDeleted)');
- deleted: Returns only records from the relation that are deleted.
// withGraphFetched
const rows = await User.query().withGraphFetched('contact(deleted)');
// withGraphJoined
const rows = await User.query().withGraphJoined('contact(deleted)');
- Relationship filter: A filter can be applied also directly to a relationship:
class User extends Model {
static get tableName() {
return 'user';
}
static get relationMappings() {
const Contact = require('./Contact');
return {
contact: {
relation: Model.ManyToManyRelation,
modelClass: Contact,
join: {
from: 'user.id',
through: {
from: 'user_contact.user_id',
to: 'user_contact.contact_id',
},
to: 'contact.id',
},
// .whereNotDeleted() or .whereDeleted()
filter: (f) => f.whereNotDeleted(),
},
};
}
}
then:
// Will return only records from the relationship contact that are not deleted
await User.query().withGraphFetched('contact');
This plugin was actually born out of a need to have .upsertGraph()
soft delete in some tables, and hard delete in others.
// a model with soft delete
class Phone extends softDelete(Model) {
static get tableName() {
return 'Phones';
}
}
// a model without soft delete
class Email extends Model {
static get tableName() {
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: '[email protected]',
},
]
}
await User.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 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:
$beforeUpdate(opt, queryContext) {
if (queryContext.softDelete) {
// do something before a soft delete, possibly including calling your $beforeDelete function.
// Think this through carefully if you are using additional plugins, as their lifecycle
// functions may execute before this one depending on how you have set up your inheritance chain!
} else if (queryContext.undelete) {
// do something before an undelete
} else {
// do something before a normal update
}
}
// same procedure for $afterUpdate
Available flags are:
- softDelete
- undelete
Flags will be true
if set, and undefined
otherwise.
All models with the soft delete mixin will have an isSoftDelete
property, which returns true
.
const modelHasSoftDelete = User.isSoftDelete;
Tests can be run with:
npm test
or:
yarn test
The linter can be run with:
npm run lint
or:
yarn lint