{"id":17177,"library":"bookshelf-paranoia","title":"Bookshelf Paranoia","description":"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.","status":"maintenance","version":"0.13.1","language":"javascript","source_language":"en","source_url":"https://github.com/estate/bookshelf-paranoia","tags":["javascript","bookshelf","knex","db","delete","safe","paranoia","database","soft"],"install":[{"cmd":"npm install bookshelf-paranoia","lang":"bash","label":"npm"},{"cmd":"yarn add bookshelf-paranoia","lang":"bash","label":"yarn"},{"cmd":"pnpm add bookshelf-paranoia","lang":"bash","label":"pnpm"}],"dependencies":[{"reason":"Core ORM library that this package extends as a plugin.","package":"bookshelf"},{"reason":"Bookshelf's underlying query builder, indirectly required.","package":"knex"}],"imports":[{"note":"This package is a Bookshelf plugin, intended for use with CommonJS `require()` and registered directly with a Bookshelf instance. It does not provide named or default ESM exports for direct import.","wrong":"import Paranoia from 'bookshelf-paranoia'","symbol":"plugin","correct":"bookshelf.plugin(require('bookshelf-paranoia'))"}],"quickstart":{"code":"const Knex = require('knex');\nconst Bookshelf = require('bookshelf');\n\n// Mock Knex setup for demonstration\nconst knex = Knex({\n  client: 'sqlite3',\n  connection: { filename: ':memory:' },\n  useNullAsDefault: true\n});\n\nconst bookshelf = Bookshelf(knex);\n\n// Add the plugin\nbookshelf.plugin(require('bookshelf-paranoia'));\n\n// Define a model with soft delete enabled\nconst User = bookshelf.Model.extend({\n  tableName: 'users',\n  softDelete: true,\n  idAttribute: 'id'\n});\n\nasync function runExample() {\n  try {\n    // Create users table if it doesn't exist\n    await knex.schema.hasTable('users').then(exists => {\n      if (!exists) {\n        return knex.schema.createTable('users', table => {\n          table.increments('id').primary();\n          table.string('name');\n          table.timestamp('deleted_at'); // Default field for soft delete\n        });\n      }\n    });\n\n    // Insert a user\n    const newUser = await User.forge({ name: 'Alice' }).save();\n    console.log('Created user:', newUser.toJSON());\n\n    // Soft delete the user\n    await newUser.destroy();\n    console.log('Soft-deleted user.');\n\n    // Try to fetch the user (should return null as it's soft-deleted)\n    const fetchedUser = await User.forge({ id: newUser.id }).fetch();\n    console.log('Fetched user after soft-delete (should be null):', fetchedUser ? fetchedUser.toJSON() : 'null');\n\n    // Fetch the user, including soft-deleted records\n    const fetchedWithDeleted = await User.forge({ id: newUser.id }).fetch({ withDeleted: true });\n    console.log('Fetched user withDeleted: ', fetchedWithDeleted.toJSON());\n    console.log('Deleted at timestamp:', fetchedWithDeleted.get('deleted_at'));\n\n    // Perform a hard delete (bypassing soft delete)\n    const userToHardDelete = await User.forge({ name: 'Bob' }).save();\n    console.log('Created user for hard delete:', userToHardDelete.toJSON());\n    await userToHardDelete.destroy({ hardDelete: true });\n    console.log('Hard-deleted user.');\n    const fetchedHardDeleted = await User.forge({ id: userToHardDelete.id }).fetch({ withDeleted: true });\n    console.log('Fetched hard-deleted user (should be null):', fetchedHardDeleted ? fetchedHardDeleted.toJSON() : 'null');\n\n  } catch (error) {\n    console.error('Error during example run:', error);\n  } finally {\n    await knex.destroy();\n  }\n}\n\nrunExample();","lang":"javascript","description":"Demonstrates the installation, model configuration, and basic usage of `bookshelf-paranoia` including soft deletion, fetching deleted records, and performing hard deletes."},"warnings":[{"fix":"Evaluate the project's long-term viability for your application. Consider contributing to a fork or migrating to an actively maintained solution for soft deletes in Bookshelf.js or your ORM of choice.","message":"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.","severity":"breaking","affected_versions":">=0.1.0"},{"fix":"Implement partial (or 'scoped') unique indexes at the database level where the uniqueness constraint only applies to records where `deleted_at` IS NULL. Alternatively, modify your application logic to check for soft-deleted conflicts before insertion, or consider unique constraints that include the `deleted_at` field.","message":"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'.","severity":"gotcha","affected_versions":">=0.1.0"},{"fix":"If this behavior is undesirable, you can disable event emission for soft deletes when configuring the plugin: `bookshelf.plugin(require('bookshelf-paranoia'), { events: false })` or disable specific events: `bookshelf.plugin(require('bookshelf-paranoia'), { events: { destroying: false } })`. Adjust your event listeners to account for soft deletions or only trigger on `hardDelete: true` scenarios.","message":"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.","severity":"gotcha","affected_versions":">=0.1.0"}],"env_vars":null,"last_verified":"2026-04-22T00:00:00.000Z","next_check":"2026-07-21T00:00:00.000Z","problems":[{"fix":"Modify 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.","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.","error":"SQLITE_CONSTRAINT: UNIQUE constraint failed: users.email"}],"ecosystem":"npm","meta_description":null}