Bookshelf Paranoia
bookshelf-paranoia is a plugin for Bookshelf.js that provides a transparent soft-delete mechanism for database records. Instead of permanently removing rows when `destroy` is called on a model, it sets a `deleted_at` timestamp on the record, effectively marking it as deleted without losing the data. This allows for easier data recovery and maintains historical data integrity within the database. The package is currently at version 0.13.1. A crucial aspect of this package is its "unmaintained" status, as explicitly stated by the author, who only dedicates minimal time to small fixes at a slow pace. This implies an uncertain release cadence and potential for slow resolution of issues. Its primary differentiator lies in seamlessly integrating soft-delete logic directly into Bookshelf models and queries, automatically excluding soft-deleted records from standard `fetch` operations and eager loadings, while offering overrides for hard deletion or retrieval of deleted records. This transparent approach minimizes changes required in application logic when implementing soft deletes.
Common errors
-
SQLITE_CONSTRAINT: UNIQUE constraint failed: users.email
cause Attempted to insert a new record with a value for a unique column (e.g., 'email') that matches a value in a soft-deleted row, triggering a database-level unique constraint violation.fixModify your database schema to use partial unique indexes. For PostgreSQL, `CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;`. For MySQL, this often requires a more complex multi-column unique key including `deleted_at` or application-level checks. Ensure your `deleted_at` column allows NULL values and defaults to NULL.
Warnings
- breaking The package is explicitly marked as 'unmaintained' by its author. While small fixes might occur, active development, new features, or timely security patches are not expected. Consider forks or alternative solutions for critical projects.
- gotcha Unique constraints on database columns will still apply to soft-deleted rows by default. This can lead to errors when attempting to insert a new record with a value that already exists in a soft-deleted row, even though it's logically 'deleted'.
- gotcha By default, soft delete operations using `destroy()` on a Bookshelf model still emit 'destroying' and 'destroyed' events. This might be unexpected if event listeners are configured to only react to permanent data removal, potentially triggering unintended side effects.
Install
-
npm install bookshelf-paranoia -
yarn add bookshelf-paranoia -
pnpm add bookshelf-paranoia
Imports
- plugin
import Paranoia from 'bookshelf-paranoia'
bookshelf.plugin(require('bookshelf-paranoia'))
Quickstart
const Knex = require('knex');
const Bookshelf = require('bookshelf');
// Mock Knex setup for demonstration
const knex = Knex({
client: 'sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true
});
const bookshelf = Bookshelf(knex);
// Add the plugin
bookshelf.plugin(require('bookshelf-paranoia'));
// Define a model with soft delete enabled
const User = bookshelf.Model.extend({
tableName: 'users',
softDelete: true,
idAttribute: 'id'
});
async function runExample() {
try {
// Create users table if it doesn't exist
await knex.schema.hasTable('users').then(exists => {
if (!exists) {
return knex.schema.createTable('users', table => {
table.increments('id').primary();
table.string('name');
table.timestamp('deleted_at'); // Default field for soft delete
});
}
});
// Insert a user
const newUser = await User.forge({ name: 'Alice' }).save();
console.log('Created user:', newUser.toJSON());
// Soft delete the user
await newUser.destroy();
console.log('Soft-deleted user.');
// Try to fetch the user (should return null as it's soft-deleted)
const fetchedUser = await User.forge({ id: newUser.id }).fetch();
console.log('Fetched user after soft-delete (should be null):', fetchedUser ? fetchedUser.toJSON() : 'null');
// Fetch the user, including soft-deleted records
const fetchedWithDeleted = await User.forge({ id: newUser.id }).fetch({ withDeleted: true });
console.log('Fetched user withDeleted: ', fetchedWithDeleted.toJSON());
console.log('Deleted at timestamp:', fetchedWithDeleted.get('deleted_at'));
// Perform a hard delete (bypassing soft delete)
const userToHardDelete = await User.forge({ name: 'Bob' }).save();
console.log('Created user for hard delete:', userToHardDelete.toJSON());
await userToHardDelete.destroy({ hardDelete: true });
console.log('Hard-deleted user.');
const fetchedHardDeleted = await User.forge({ id: userToHardDelete.id }).fetch({ withDeleted: true });
console.log('Fetched hard-deleted user (should be null):', fetchedHardDeleted ? fetchedHardDeleted.toJSON() : 'null');
} catch (error) {
console.error('Error during example run:', error);
} finally {
await knex.destroy();
}
}
runExample();