npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@knax/objection-soft-delete

v1.0.8

Published

A plugin for objection js to support soft delete functionallity

Downloads

85

Readme

CircleCI Coverage Status

objection-soft-delete

A plugin that adds soft-delete functionality to Objection.js

Installation

NPM

npm i objection-soft-delete --save

Yarn

yarn add objection-soft-delete

Usage

Mixin the plugin on an object representing a table that uses a boolean column as a flag for soft delete

// Import objection model.
const Model = require('objection').Model;

// Import the plugin
const softDelete = require('objection-soft-delete');

// Mixin the plugin and specify the column to to use.  'deleted' will be used if none is specified:
class User extends softDelete({ columnName: 'deleted' })(Model) {
  static get tableName() {
    return 'Users';
  }
  
  static get jsonSchema() {
    return {
      type: 'object',
      required: [],

      properties: {
        id: { type: 'integer' },
        // matches the columnName passed above
        deleted: { type: 'boolean' },
        // other columns
      },
    }
  }
}

Note: Make sure the 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.

When .delete() or .del() is called for that model, the matching row(s) are flagged true instead of deleted

Delete a User:

await User.query().where('id', 1).delete(); // db now has: { User id: 1, deleted: true, ... }

Or:

const user = await User.query().where('id', 1).first();
await user.$query().delete(); // same

Deleted rows are still in the db:

const deletedUser = await User.query().where('id', 1).first(); // => { User id: 1, deleted: true, ... }

Filter out deleted rows without having to remember each model's "deleted" columnName:

const activeUsers = await User.query().whereNotDeleted();

Get only deleted rows:

const deletedUsers = await User.query().whereDeleted();

Restore row(s):

await User.query().where('id', 1).undelete(); // db now has: { User id: 1, deleted: false, ... }

Permanently remove row(s) from the db:

await User.query.where('id', 1).hardDelete(); // => row with id:1 is permanently deleted

Filtering out deleted/undeleted records in .eager() or .joinRelation()

Using the named filters

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:

// some other Model with a relation to the `User` model:
const group = await UserGroup.query()
  .where('id', 1)
  .first()
  .eager('users(notDeleted)'); // => now group.users contains only records that are not deleted

Or:

// some other Model with a relation to the `User` model:
const group = await UserGroup.query()
  .where('id', 1)
  .first()
  .eager('users(deleted)'); // => now group.users contains only records that are deleted

With .joinRelation():

// some other Model with a relation to the `User` model:
const group = await UserGroup.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';

Using a relationship filter

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:
class UserGroup extends Model {
  static get tableName() {
    return 'UserGroups';
  }
  
  ...
  
  static get relationMappings() {
    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:

const group = await UserGroup.query()
  .where('id', 1)
  .first()
  .eager('users'); // => `User` rows are filtered out automatically without having to specify the filter here

Per-model 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:

class User extends softDelete({ columnName: 'deleted' })(Model) {
  ...
}

class UserGroup extends softDelete({ columnName: 'inactive' })(Model) {
  ...
}

// everything will work as expected:
await User.query()
  .whereNotDeleted(); // => all undeleted users

await UserGroup.query()
  .whereNotDeleted(); // => all undeleted user groups

await UserGroup.query()
  .whereNotDeleted()
  .eager('users(notDeleted)'); // => all undeleted user groups, with all related undeleted users eagerly loaded

await User.query()
  .whereDeleted()
  .eager('groups(deleted)'); // => all deleted users, with all related deleted user groups eagerly loaded

await User.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...

Using with .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():

// 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]',
    },
  ]
}

// then:

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

Lifecycle Functions

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.

Checking Whether a Model is SoftDelete

All models with the soft delete mixin will have an isSoftDelete property, which returns true.

Options

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:

const softDelete = require('objection-soft-delete')({
  columnName: 'inactive',
});

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)

const softDelete = require('objection-soft-delete')({
  columnName: 'deleted_at',
  deletedValue: knex.fn.now(),
});

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)

const softDelete = require('objection-soft-delete')({
  columnName: 'deleted_at',
  deletedValue: knex.fn.now(),
  notDeletedValue: null,
});

Tests

Tests can be run with:

npm test

or:

yarn test

Linting

The linter can be run with:

npm run lint

or:

yarn lint

Contributing

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.