@kysera/soft-delete 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Repository, Plugin } from '@kysera/repository';
1
+ import { Plugin } from '@kysera/executor';
2
2
  import { KyseraLogger } from '@kysera/core';
3
3
  import { z } from 'zod';
4
4
 
@@ -78,10 +78,11 @@ interface SoftDeleteMethods<T> {
78
78
  }
79
79
  /**
80
80
  * Repository extended with soft delete methods
81
+ * Note: Repository type is from @kysera/repository
81
82
  */
82
- type SoftDeleteRepository<Entity, DB> = Repository<Entity, DB> & SoftDeleteMethods<Entity>;
83
+ type SoftDeleteRepository<Entity> = any & SoftDeleteMethods<Entity>;
83
84
  /**
84
- * Soft Delete Plugin for Kysera ORM
85
+ * Soft Delete Plugin for Kysera
85
86
  *
86
87
  * This plugin implements soft delete functionality using the Method Override pattern:
87
88
  * - Automatically filters out soft-deleted records from SELECT queries
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import {sql}from'kysely';import {silentLogger,NotFoundError}from'@kysera/core';import {z}from'zod';var N=z.object({deletedAtColumn:z.string().optional(),includeDeleted:z.boolean().optional(),tables:z.array(z.string()).optional(),primaryKeyColumn:z.string().optional()}),P=(y={})=>{let{deletedAtColumn:s="deleted_at",includeDeleted:d=false,tables:i,primaryKeyColumn:r="id",logger:o=silentLogger}=y;return {name:"@kysera/soft-delete",version:"0.5.1",interceptQuery(a,e){return (!i||i.includes(e.table))&&e.operation==="select"&&!e.metadata.includeDeleted&&!d?(o.debug(`Filtering soft-deleted records from ${e.table}`),a.where(`${e.table}.${s}`,"is",null)):a},extendRepository(a){let e=a;if(!("tableName"in e)||!("executor"in e))return a;if(!(!i||i.includes(e.tableName)))return o.debug(`Table ${e.tableName} does not support soft delete, skipping extension`),a;o.debug(`Extending repository for table ${e.tableName} with soft delete methods`);let f=e.findAll.bind(e),u=e.findById.bind(e);return {...e,async findAll(){return d?await f():await e.executor.selectFrom(e.tableName).selectAll().where(s,"is",null).execute()},async findById(t){return d?r!=="id"?await e.executor.selectFrom(e.tableName).selectAll().where(r,"=",t).executeTakeFirst()??null:await u(t):await e.executor.selectFrom(e.tableName).selectAll().where(r,"=",t).where(s,"is",null).executeTakeFirst()??null},async softDelete(t){o.info(`Soft deleting record ${t} from ${e.tableName}`),await e.executor.updateTable(e.tableName).set({[s]:sql`CURRENT_TIMESTAMP`}).where(r,"=",t).execute();let n;if(r!=="id"?n=await e.executor.selectFrom(e.tableName).selectAll().where(r,"=",t).executeTakeFirst():n=await u(t),!n)throw o.warn(`Record ${t} not found in ${e.tableName} for soft delete`),new NotFoundError("Record",{id:t});return n},async restore(t){o.info(`Restoring soft-deleted record ${t} from ${e.tableName}`),await e.executor.updateTable(e.tableName).set({[s]:null}).where(r,"=",t).execute();let n;if(r!=="id"?n=await e.executor.selectFrom(e.tableName).selectAll().where(r,"=",t).executeTakeFirst():n=await u(t),!n)throw o.warn(`Record ${t} not found in ${e.tableName} for restore`),new NotFoundError("Record",{id:t});return n},async hardDelete(t){o.info(`Hard deleting record ${t} from ${e.tableName}`),await e.executor.deleteFrom(e.tableName).where(r,"=",t).execute();},async findWithDeleted(t){return r!=="id"?await e.executor.selectFrom(e.tableName).selectAll().where(r,"=",t).executeTakeFirst()??null:await u(t)},async findAllWithDeleted(){return await f()},async findDeleted(){return await e.executor.selectFrom(e.tableName).selectAll().where(s,"is not",null).execute()},async softDeleteMany(t){if(t.length===0)return [];o.info(`Soft deleting ${t.length} records from ${e.tableName}`),await e.executor.updateTable(e.tableName).set({[s]:sql`CURRENT_TIMESTAMP`}).where(r,"in",t).execute();let n=await e.executor.selectFrom(e.tableName).selectAll().where(r,"in",t).execute();if(n.length!==t.length){let p=n.map(c=>c[r]),w=t.filter(c=>!p.includes(c));throw o.warn(`Some records not found for soft delete: ${w.join(", ")}`),new NotFoundError("Records",{ids:w})}return n},async restoreMany(t){return t.length===0?[]:(o.info(`Restoring ${t.length} soft-deleted records from ${e.tableName}`),await e.executor.updateTable(e.tableName).set({[s]:null}).where(r,"in",t).execute(),await e.executor.selectFrom(e.tableName).selectAll().where(r,"in",t).execute())},async hardDeleteMany(t){t.length!==0&&(o.info(`Hard deleting ${t.length} records from ${e.tableName}`),await e.executor.deleteFrom(e.tableName).where(r,"in",t).execute());}}}}};export{N as SoftDeleteOptionsSchema,P as softDeletePlugin};//# sourceMappingURL=index.js.map
1
+ import {getRawDb}from'@kysera/executor';import {sql}from'kysely';import {silentLogger,NotFoundError}from'@kysera/core';import {z}from'zod';var P=z.object({deletedAtColumn:z.string().optional(),includeDeleted:z.boolean().optional(),tables:z.array(z.string()).optional(),primaryKeyColumn:z.string().optional()}),T=(y={})=>{let{deletedAtColumn:l="deleted_at",includeDeleted:m=false,tables:u,primaryKeyColumn:n="id",logger:s=silentLogger}=y;return {name:"@kysera/soft-delete",version:"0.7.0",interceptQuery(a,e){return (!u||u.includes(e.table))&&e.operation==="select"&&!e.metadata.includeDeleted&&!m?(s.debug(`Filtering soft-deleted records from ${e.table}`),a.where(`${e.table}.${l}`,"is",null)):a},extendRepository(a){let e=a;if(!("tableName"in e)||!("executor"in e))return a;if(!(!u||u.includes(e.tableName)))return s.debug(`Table ${e.tableName} does not support soft delete, skipping extension`),a;s.debug(`Extending repository for table ${e.tableName} with soft delete methods`);let r=getRawDb(e.executor);return {...e,async findAll(){return m?await r.selectFrom(e.tableName).selectAll().execute():await r.selectFrom(e.tableName).selectAll().where(l,"is",null).execute()},async findById(t){return m?await r.selectFrom(e.tableName).selectAll().where(n,"=",t).executeTakeFirst()??null:await r.selectFrom(e.tableName).selectAll().where(n,"=",t).where(l,"is",null).executeTakeFirst()??null},async softDelete(t){s.info(`Soft deleting record ${t} from ${e.tableName}`),await r.updateTable(e.tableName).set({[l]:sql`CURRENT_TIMESTAMP`}).where(n,"=",t).execute();let o=await r.selectFrom(e.tableName).selectAll().where(n,"=",t).executeTakeFirst();if(!o)throw s.warn(`Record ${t} not found in ${e.tableName} for soft delete`),new NotFoundError("Record",{id:t});return o},async restore(t){s.info(`Restoring soft-deleted record ${t} from ${e.tableName}`),await r.updateTable(e.tableName).set({[l]:null}).where(n,"=",t).execute();let o=await r.selectFrom(e.tableName).selectAll().where(n,"=",t).executeTakeFirst();if(!o)throw s.warn(`Record ${t} not found in ${e.tableName} for restore`),new NotFoundError("Record",{id:t});return o},async hardDelete(t){s.info(`Hard deleting record ${t} from ${e.tableName}`),await r.deleteFrom(e.tableName).where(n,"=",t).execute();},async findWithDeleted(t){return await r.selectFrom(e.tableName).selectAll().where(n,"=",t).executeTakeFirst()??null},async findAllWithDeleted(){return await r.selectFrom(e.tableName).selectAll().execute()},async findDeleted(){return await r.selectFrom(e.tableName).selectAll().where(l,"is not",null).execute()},async softDeleteMany(t){if(t.length===0)return [];s.info(`Soft deleting ${t.length} records from ${e.tableName}`),await r.updateTable(e.tableName).set({[l]:sql`CURRENT_TIMESTAMP`}).where(n,"in",t).execute();let o=await r.selectFrom(e.tableName).selectAll().where(n,"in",t).execute();if(o.length!==t.length){let f=o.map(c=>c[n]),w=t.filter(c=>!f.includes(c));throw s.warn(`Some records not found for soft delete: ${w.join(", ")}`),new NotFoundError("Records",{ids:w})}return o},async restoreMany(t){return t.length===0?[]:(s.info(`Restoring ${t.length} soft-deleted records from ${e.tableName}`),await r.updateTable(e.tableName).set({[l]:null}).where(n,"in",t).execute(),await r.selectFrom(e.tableName).selectAll().where(n,"in",t).execute())},async hardDeleteMany(t){t.length!==0&&(s.info(`Hard deleting ${t.length} records from ${e.tableName}`),await r.deleteFrom(e.tableName).where(n,"in",t).execute());}}}}};export{P as SoftDeleteOptionsSchema,T as softDeletePlugin};//# sourceMappingURL=index.js.map
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["SoftDeleteOptionsSchema","z","softDeletePlugin","options","deletedAtColumn","includeDeleted","tables","primaryKeyColumn","logger","silentLogger","qb","context","repo","baseRepo","originalFindAll","originalFindById","id","sql","record","NotFoundError","ids","records","foundIds","r","missingIds"],"mappings":"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","file":"index.js","sourcesContent":["import type { Plugin, AnyQueryBuilder, Repository } from '@kysera/repository';\nimport type { SelectQueryBuilder, Kysely } from 'kysely';\nimport { sql } from 'kysely';\nimport { NotFoundError, silentLogger } from '@kysera/core';\nimport type { KyseraLogger } from '@kysera/core';\nimport { z } from 'zod';\n\n/**\n * Configuration options for the soft delete plugin.\n *\n * @example\n * ```typescript\n * const plugin = softDeletePlugin({\n * deletedAtColumn: 'deleted_at',\n * includeDeleted: false,\n * tables: ['users', 'posts'], // Only these tables support soft delete\n * primaryKeyColumn: 'id' // Default primary key column\n * })\n * ```\n */\nexport interface SoftDeleteOptions {\n /**\n * Column name for soft delete timestamp.\n *\n * @default 'deleted_at'\n */\n deletedAtColumn?: string;\n\n /**\n * Include deleted records by default in queries.\n * When false, soft-deleted records are automatically filtered out.\n *\n * @default false\n */\n includeDeleted?: boolean;\n\n /**\n * List of tables that support soft delete.\n * If not provided, all tables are assumed to support it.\n *\n * @example ['users', 'posts', 'comments']\n */\n tables?: string[];\n\n /**\n * Primary key column name used for identifying records.\n * Tables with different primary key names (uuid, user_id, etc.) can be configured.\n *\n * @default 'id'\n * @example 'uuid', 'user_id', 'post_id'\n */\n primaryKeyColumn?: string;\n\n /**\n * Logger for plugin operations.\n * Uses KyseraLogger interface from @kysera/core.\n *\n * @default silentLogger (no output)\n */\n logger?: KyseraLogger;\n}\n\n/**\n * Zod schema for SoftDeleteOptions\n * Used for validation and configuration in the kysera-cli\n */\nexport const SoftDeleteOptionsSchema = z.object({\n deletedAtColumn: z.string().optional(),\n includeDeleted: z.boolean().optional(),\n tables: z.array(z.string()).optional(),\n primaryKeyColumn: z.string().optional(),\n});\n\n/**\n * Methods added to repositories by the soft delete plugin\n */\nexport interface SoftDeleteMethods<T> {\n softDelete(id: number | string): Promise<T>;\n restore(id: number | string): Promise<T>;\n hardDelete(id: number | string): Promise<void>;\n findWithDeleted(id: number | string): Promise<T | null>;\n findAllWithDeleted(): Promise<T[]>;\n findDeleted(): Promise<T[]>;\n softDeleteMany(ids: (number | string)[]): Promise<T[]>;\n restoreMany(ids: (number | string)[]): Promise<T[]>;\n hardDeleteMany(ids: (number | string)[]): Promise<void>;\n}\n\n/**\n * Repository extended with soft delete methods\n */\nexport type SoftDeleteRepository<Entity, DB> = Repository<Entity, DB> & SoftDeleteMethods<Entity>;\n\ninterface BaseRepository {\n tableName: string;\n executor: Kysely<Record<string, unknown>>;\n findAll: () => Promise<unknown[]>;\n findById: (id: number) => Promise<unknown>;\n update: (id: number, data: Record<string, unknown>) => Promise<unknown>;\n}\n\n/**\n * Soft Delete Plugin for Kysera ORM\n *\n * This plugin implements soft delete functionality using the Method Override pattern:\n * - Automatically filters out soft-deleted records from SELECT queries\n * - Adds softDelete(), restore(), and hardDelete() methods to repositories\n * - Provides findWithDeleted() and findDeleted() utility methods\n *\n * ## Usage\n *\n * ```typescript\n * import { softDeletePlugin } from '@kysera/soft-delete'\n * import { createORM } from '@kysera/repository'\n *\n * const orm = await createORM(db, [\n * softDeletePlugin({\n * deletedAtColumn: 'deleted_at',\n * tables: ['users', 'posts']\n * })\n * ])\n *\n * const userRepo = orm.createRepository(createUserRepository)\n *\n * // Soft delete a user (sets deleted_at)\n * await userRepo.softDelete(1)\n *\n * // Find all users (excludes soft-deleted)\n * await userRepo.findAll()\n *\n * // Find including deleted\n * await userRepo.findAllWithDeleted()\n *\n * // Restore a soft-deleted user\n * await userRepo.restore(1)\n *\n * // Permanently delete (real DELETE)\n * await userRepo.hardDelete(1)\n * ```\n *\n * ## Architecture Note\n *\n * This plugin uses Method Override, not full query interception:\n * - ✅ SELECT queries are automatically filtered\n * - ❌ DELETE queries are NOT automatically converted to soft deletes\n * - Use softDelete() method explicitly instead of delete()\n *\n * This design is intentional for simplicity and explicitness.\n *\n * ## Transaction Behavior\n *\n * **IMPORTANT**: Soft delete operations respect ACID properties and work correctly with transactions:\n *\n * - ✅ **Commits with transaction**: softDelete/restore operations use the same executor\n * as other repository operations, so they commit together\n * - ✅ **Rolls back with transaction**: If a transaction is rolled back, soft delete\n * operations are also rolled back\n * - ✅ **Atomic operations**: All soft delete operations (including bulk) are atomic\n *\n * ### Correct Transaction Usage\n *\n * ```typescript\n * // ✅ CORRECT: Soft delete is part of transaction\n * await db.transaction().execute(async (trx) => {\n * const repos = createRepositories(trx) // Use transaction executor\n * await repos.users.softDelete(1)\n * await repos.posts.softDeleteMany([1, 2, 3])\n * // If transaction rolls back, both operations roll back\n * })\n * ```\n *\n * ### Cascade Soft Delete Pattern\n *\n * For related entities, you need to manually implement cascade soft delete:\n *\n * ```typescript\n * // Cascade soft delete pattern\n * await db.transaction().execute(async (trx) => {\n * const repos = createRepositories(trx)\n * const userId = 123\n *\n * // First, soft delete child records\n * const userPosts = await repos.posts.findBy({ user_id: userId })\n * await repos.posts.softDeleteMany(userPosts.map(p => p.id))\n *\n * // Then, soft delete parent\n * await repos.users.softDelete(userId)\n * })\n * ```\n *\n * @param options - Configuration options for soft delete behavior\n * @returns Plugin instance that can be used with createORM\n */\nexport const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {\n const {\n deletedAtColumn = 'deleted_at',\n includeDeleted = false,\n tables,\n primaryKeyColumn = 'id',\n logger = silentLogger,\n } = options;\n\n return {\n name: '@kysera/soft-delete',\n version: '0.5.1',\n\n /**\n * Intercept queries to automatically filter soft-deleted records.\n *\n * NOTE: This plugin uses the Method Override pattern, not full query interception.\n * - SELECT queries are automatically filtered to exclude soft-deleted records\n * - DELETE operations are NOT automatically converted to soft deletes\n * - Use the softDelete() method instead of delete() to perform soft deletes\n * - Use hardDelete() method to bypass soft delete and perform a real DELETE\n *\n * This approach is simpler and more explicit than full query interception.\n */\n interceptQuery<QB extends AnyQueryBuilder>(\n qb: QB,\n context: { operation: string; table: string; metadata: Record<string, unknown> }\n ): QB {\n // Check if table supports soft delete\n const supportsSoftDelete = !tables || tables.includes(context.table);\n\n // Only filter SELECT queries when not explicitly including deleted\n if (\n supportsSoftDelete &&\n context.operation === 'select' &&\n !context.metadata['includeDeleted'] &&\n !includeDeleted\n ) {\n logger.debug(`Filtering soft-deleted records from ${context.table}`);\n // Add WHERE deleted_at IS NULL to the query builder\n type GenericSelectQueryBuilder = SelectQueryBuilder<Record<string, unknown>, string, Record<string, unknown>>;\n return (qb as unknown as GenericSelectQueryBuilder).where(\n `${context.table}.${deletedAtColumn}` as never,\n 'is',\n null\n ) as QB;\n }\n\n // Note: DELETE operations are NOT intercepted here\n // Use softDelete() method instead of delete() to perform soft deletes\n // This is by design - method override is simpler and more explicit\n\n return qb;\n },\n\n /**\n * Extend repository with soft delete methods.\n *\n * Adds the following methods to repositories:\n * - softDelete(id): Marks record as deleted by setting deleted_at timestamp\n * - restore(id): Restores a soft-deleted record by setting deleted_at to null\n * - hardDelete(id): Permanently deletes a record (bypasses soft delete)\n * - findWithDeleted(id): Find a record including soft-deleted ones\n * - findAllWithDeleted(): Find all records including soft-deleted ones\n * - findDeleted(): Find only soft-deleted records\n * - softDeleteMany(ids): Soft delete multiple records (bulk operation)\n * - restoreMany(ids): Restore multiple soft-deleted records (bulk operation)\n * - hardDeleteMany(ids): Permanently delete multiple records (bulk operation)\n *\n * Also overrides findAll() and findById() to automatically filter out\n * soft-deleted records (unless includeDeleted option is set).\n */\n extendRepository<T extends object>(repo: T): T {\n // Type assertion is safe here as we're checking for BaseRepository properties\n const baseRepo = repo as unknown as BaseRepository;\n\n // Check if it's actually a repository (has required properties)\n if (!('tableName' in baseRepo) || !('executor' in baseRepo)) {\n return repo;\n }\n\n // Check if table supports soft delete\n const supportsSoftDelete = !tables || tables.includes(baseRepo.tableName);\n\n // If table doesn't support soft delete, return unmodified repo\n if (!supportsSoftDelete) {\n logger.debug(`Table ${baseRepo.tableName} does not support soft delete, skipping extension`);\n return repo;\n }\n\n logger.debug(`Extending repository for table ${baseRepo.tableName} with soft delete methods`);\n\n // Wrap original methods to apply soft delete filtering\n const originalFindAll = baseRepo.findAll.bind(baseRepo);\n const originalFindById = baseRepo.findById.bind(baseRepo);\n\n const extendedRepo = {\n ...baseRepo,\n\n // Override base methods to filter soft-deleted records\n async findAll(): Promise<unknown[]> {\n if (!includeDeleted) {\n const result = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(deletedAtColumn as never, 'is', null)\n .execute();\n return result as unknown[];\n }\n return await originalFindAll();\n },\n\n async findById(id: number): Promise<unknown> {\n if (!includeDeleted) {\n const result = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .where(deletedAtColumn as never, 'is', null)\n .executeTakeFirst();\n return result ?? null;\n }\n // When includeDeleted is true, use custom PK column if configured\n // Otherwise fall back to original method (which uses 'id')\n if (primaryKeyColumn !== 'id') {\n const result = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n return result ?? null;\n }\n return await originalFindById(id);\n },\n\n async softDelete(id: number): Promise<unknown> {\n logger.info(`Soft deleting record ${id} from ${baseRepo.tableName}`);\n // Use CURRENT_TIMESTAMP directly in SQL to avoid datetime format issues\n // This works across all databases (MySQL, PostgreSQL, SQLite)\n // We bypass repository.update() to avoid Zod validation issues with RawBuilder\n await baseRepo.executor\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: sql`CURRENT_TIMESTAMP` } as never)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n\n // Fetch the updated record to verify it exists\n // Use custom PK column if configured\n let record;\n if (primaryKeyColumn !== 'id') {\n record = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n } else {\n record = await originalFindById(id);\n }\n\n // If record not found or deleted_at not set, throw error\n if (!record) {\n logger.warn(`Record ${id} not found in ${baseRepo.tableName} for soft delete`);\n throw new NotFoundError('Record', { id });\n }\n\n return record;\n },\n\n async restore(id: number): Promise<unknown> {\n logger.info(`Restoring soft-deleted record ${id} from ${baseRepo.tableName}`);\n // Use direct executor to support custom primary key columns\n await baseRepo.executor\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: null } as never)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n\n // Fetch and return the restored record\n let record;\n if (primaryKeyColumn !== 'id') {\n record = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n } else {\n record = await originalFindById(id);\n }\n\n if (!record) {\n logger.warn(`Record ${id} not found in ${baseRepo.tableName} for restore`);\n throw new NotFoundError('Record', { id });\n }\n\n return record;\n },\n\n async hardDelete(id: number): Promise<void> {\n logger.info(`Hard deleting record ${id} from ${baseRepo.tableName}`);\n // Direct hard delete - bypass soft delete\n await baseRepo.executor\n .deleteFrom(baseRepo.tableName)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n },\n\n async findWithDeleted(id: number): Promise<unknown> {\n // Use custom PK column if configured, otherwise use original method\n if (primaryKeyColumn !== 'id') {\n const result = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n return result ?? null;\n }\n return await originalFindById(id);\n },\n\n async findAllWithDeleted(): Promise<unknown[]> {\n // Use original method without filtering\n return await originalFindAll();\n },\n\n async findDeleted(): Promise<unknown[]> {\n const result = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(deletedAtColumn as never, 'is not', null)\n .execute();\n return result as unknown[];\n },\n\n async softDeleteMany(ids: (number | string)[]): Promise<unknown[]> {\n // Handle empty arrays gracefully\n if (ids.length === 0) {\n return [];\n }\n\n logger.info(`Soft deleting ${ids.length} records from ${baseRepo.tableName}`);\n\n // Efficient bulk UPDATE query\n await baseRepo.executor\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: sql`CURRENT_TIMESTAMP` } as never)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n // Fetch all affected records to verify and return them\n const records = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n // Verify all records were found\n if (records.length !== ids.length) {\n const foundIds = records.map((r: any) => r[primaryKeyColumn]);\n const missingIds = ids.filter((id) => !foundIds.includes(id));\n logger.warn(`Some records not found for soft delete: ${missingIds.join(', ')}`);\n throw new NotFoundError('Records', { ids: missingIds });\n }\n\n return records as unknown[];\n },\n\n async restoreMany(ids: (number | string)[]): Promise<unknown[]> {\n // Handle empty arrays gracefully\n if (ids.length === 0) {\n return [];\n }\n\n logger.info(`Restoring ${ids.length} soft-deleted records from ${baseRepo.tableName}`);\n\n // Efficient bulk UPDATE query to restore records\n await baseRepo.executor\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: null } as never)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n // Fetch all affected records to return them\n const records = await baseRepo.executor\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n return records as unknown[];\n },\n\n async hardDeleteMany(ids: (number | string)[]): Promise<void> {\n // Handle empty arrays gracefully\n if (ids.length === 0) {\n return;\n }\n\n logger.info(`Hard deleting ${ids.length} records from ${baseRepo.tableName}`);\n\n // Efficient bulk DELETE query\n await baseRepo.executor\n .deleteFrom(baseRepo.tableName)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n },\n };\n\n return extendedRepo as T;\n },\n };\n};\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["SoftDeleteOptionsSchema","z","softDeletePlugin","options","deletedAtColumn","includeDeleted","tables","primaryKeyColumn","logger","silentLogger","qb","context","repo","baseRepo","rawDb","getRawDb","id","sql","record","NotFoundError","ids","records","foundIds","r","missingIds"],"mappings":"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","file":"index.js","sourcesContent":["import type { Plugin, QueryBuilderContext } from '@kysera/executor';\nimport { getRawDb } from '@kysera/executor';\nimport type { SelectQueryBuilder, Kysely } from 'kysely';\nimport { sql } from 'kysely';\nimport { NotFoundError, silentLogger } from '@kysera/core';\nimport type { KyseraLogger } from '@kysera/core';\nimport { z } from 'zod';\n\n/**\n * Configuration options for the soft delete plugin.\n *\n * @example\n * ```typescript\n * const plugin = softDeletePlugin({\n * deletedAtColumn: 'deleted_at',\n * includeDeleted: false,\n * tables: ['users', 'posts'], // Only these tables support soft delete\n * primaryKeyColumn: 'id' // Default primary key column\n * })\n * ```\n */\nexport interface SoftDeleteOptions {\n /**\n * Column name for soft delete timestamp.\n *\n * @default 'deleted_at'\n */\n deletedAtColumn?: string;\n\n /**\n * Include deleted records by default in queries.\n * When false, soft-deleted records are automatically filtered out.\n *\n * @default false\n */\n includeDeleted?: boolean;\n\n /**\n * List of tables that support soft delete.\n * If not provided, all tables are assumed to support it.\n *\n * @example ['users', 'posts', 'comments']\n */\n tables?: string[];\n\n /**\n * Primary key column name used for identifying records.\n * Tables with different primary key names (uuid, user_id, etc.) can be configured.\n *\n * @default 'id'\n * @example 'uuid', 'user_id', 'post_id'\n */\n primaryKeyColumn?: string;\n\n /**\n * Logger for plugin operations.\n * Uses KyseraLogger interface from @kysera/core.\n *\n * @default silentLogger (no output)\n */\n logger?: KyseraLogger;\n}\n\n/**\n * Zod schema for SoftDeleteOptions\n * Used for validation and configuration in the kysera-cli\n */\nexport const SoftDeleteOptionsSchema = z.object({\n deletedAtColumn: z.string().optional(),\n includeDeleted: z.boolean().optional(),\n tables: z.array(z.string()).optional(),\n primaryKeyColumn: z.string().optional(),\n});\n\n/**\n * Methods added to repositories by the soft delete plugin\n */\nexport interface SoftDeleteMethods<T> {\n softDelete(id: number | string): Promise<T>;\n restore(id: number | string): Promise<T>;\n hardDelete(id: number | string): Promise<void>;\n findWithDeleted(id: number | string): Promise<T | null>;\n findAllWithDeleted(): Promise<T[]>;\n findDeleted(): Promise<T[]>;\n softDeleteMany(ids: (number | string)[]): Promise<T[]>;\n restoreMany(ids: (number | string)[]): Promise<T[]>;\n hardDeleteMany(ids: (number | string)[]): Promise<void>;\n}\n\n/**\n * Repository extended with soft delete methods\n * Note: Repository type is from @kysera/repository\n */\nexport type SoftDeleteRepository<Entity> = any & SoftDeleteMethods<Entity>;\n\ninterface BaseRepository {\n tableName: string;\n executor: Kysely<Record<string, unknown>>;\n findAll: () => Promise<unknown[]>;\n findById: (id: number) => Promise<unknown>;\n update: (id: number, data: Record<string, unknown>) => Promise<unknown>;\n}\n\n/**\n * Soft Delete Plugin for Kysera\n *\n * This plugin implements soft delete functionality using the Method Override pattern:\n * - Automatically filters out soft-deleted records from SELECT queries\n * - Adds softDelete(), restore(), and hardDelete() methods to repositories\n * - Provides findWithDeleted() and findDeleted() utility methods\n *\n * ## Usage\n *\n * ```typescript\n * import { softDeletePlugin } from '@kysera/soft-delete'\n * import { createORM } from '@kysera/repository'\n *\n * const orm = await createORM(db, [\n * softDeletePlugin({\n * deletedAtColumn: 'deleted_at',\n * tables: ['users', 'posts']\n * })\n * ])\n *\n * const userRepo = orm.createRepository(createUserRepository)\n *\n * // Soft delete a user (sets deleted_at)\n * await userRepo.softDelete(1)\n *\n * // Find all users (excludes soft-deleted)\n * await userRepo.findAll()\n *\n * // Find including deleted\n * await userRepo.findAllWithDeleted()\n *\n * // Restore a soft-deleted user\n * await userRepo.restore(1)\n *\n * // Permanently delete (real DELETE)\n * await userRepo.hardDelete(1)\n * ```\n *\n * ## Architecture Note\n *\n * This plugin uses Method Override, not full query interception:\n * - ✅ SELECT queries are automatically filtered\n * - ❌ DELETE queries are NOT automatically converted to soft deletes\n * - Use softDelete() method explicitly instead of delete()\n *\n * This design is intentional for simplicity and explicitness.\n *\n * ## Transaction Behavior\n *\n * **IMPORTANT**: Soft delete operations respect ACID properties and work correctly with transactions:\n *\n * - ✅ **Commits with transaction**: softDelete/restore operations use the same executor\n * as other repository operations, so they commit together\n * - ✅ **Rolls back with transaction**: If a transaction is rolled back, soft delete\n * operations are also rolled back\n * - ✅ **Atomic operations**: All soft delete operations (including bulk) are atomic\n *\n * ### Correct Transaction Usage\n *\n * ```typescript\n * // ✅ CORRECT: Soft delete is part of transaction\n * await db.transaction().execute(async (trx) => {\n * const repos = createRepositories(trx) // Use transaction executor\n * await repos.users.softDelete(1)\n * await repos.posts.softDeleteMany([1, 2, 3])\n * // If transaction rolls back, both operations roll back\n * })\n * ```\n *\n * ### Cascade Soft Delete Pattern\n *\n * For related entities, you need to manually implement cascade soft delete:\n *\n * ```typescript\n * // Cascade soft delete pattern\n * await db.transaction().execute(async (trx) => {\n * const repos = createRepositories(trx)\n * const userId = 123\n *\n * // First, soft delete child records\n * const userPosts = await repos.posts.findBy({ user_id: userId })\n * await repos.posts.softDeleteMany(userPosts.map(p => p.id))\n *\n * // Then, soft delete parent\n * await repos.users.softDelete(userId)\n * })\n * ```\n *\n * @param options - Configuration options for soft delete behavior\n * @returns Plugin instance that can be used with createORM\n */\nexport const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {\n const {\n deletedAtColumn = 'deleted_at',\n includeDeleted = false,\n tables,\n primaryKeyColumn = 'id',\n logger = silentLogger,\n } = options;\n\n return {\n name: '@kysera/soft-delete',\n version: '0.7.0',\n\n /**\n * Intercept queries to automatically filter soft-deleted records.\n *\n * NOTE: This plugin uses the Method Override pattern, not full query interception.\n * - SELECT queries are automatically filtered to exclude soft-deleted records\n * - DELETE operations are NOT automatically converted to soft deletes\n * - Use the softDelete() method instead of delete() to perform soft deletes\n * - Use hardDelete() method to bypass soft delete and perform a real DELETE\n *\n * This approach is simpler and more explicit than full query interception.\n *\n * Works with both Repository and DAL patterns through the unified executor layer.\n */\n interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {\n // Check if table supports soft delete\n const supportsSoftDelete = !tables || tables.includes(context.table);\n\n // Only filter SELECT queries when not explicitly including deleted\n if (\n supportsSoftDelete &&\n context.operation === 'select' &&\n !context.metadata['includeDeleted'] &&\n !includeDeleted\n ) {\n logger.debug(`Filtering soft-deleted records from ${context.table}`);\n // Add WHERE deleted_at IS NULL to the query builder\n type GenericSelectQueryBuilder = SelectQueryBuilder<Record<string, unknown>, string, Record<string, unknown>>;\n return (qb as unknown as GenericSelectQueryBuilder).where(\n `${context.table}.${deletedAtColumn}` as never,\n 'is',\n null\n ) as QB;\n }\n\n // Note: DELETE operations are NOT intercepted here\n // Use softDelete() method instead of delete() to perform soft deletes\n // This is by design - method override is simpler and more explicit\n\n return qb;\n },\n\n /**\n * Extend repository with soft delete methods.\n *\n * Adds the following methods to repositories:\n * - softDelete(id): Marks record as deleted by setting deleted_at timestamp\n * - restore(id): Restores a soft-deleted record by setting deleted_at to null\n * - hardDelete(id): Permanently deletes a record (bypasses soft delete)\n * - findWithDeleted(id): Find a record including soft-deleted ones\n * - findAllWithDeleted(): Find all records including soft-deleted ones\n * - findDeleted(): Find only soft-deleted records\n * - softDeleteMany(ids): Soft delete multiple records (bulk operation)\n * - restoreMany(ids): Restore multiple soft-deleted records (bulk operation)\n * - hardDeleteMany(ids): Permanently delete multiple records (bulk operation)\n *\n * Also overrides findAll() and findById() to automatically filter out\n * soft-deleted records (unless includeDeleted option is set).\n */\n extendRepository<T extends object>(repo: T): T {\n // Type assertion is safe here as we're checking for BaseRepository properties\n const baseRepo = repo as unknown as BaseRepository;\n\n // Check if it's actually a repository (has required properties)\n if (!('tableName' in baseRepo) || !('executor' in baseRepo)) {\n return repo;\n }\n\n // Check if table supports soft delete\n const supportsSoftDelete = !tables || tables.includes(baseRepo.tableName);\n\n // If table doesn't support soft delete, return unmodified repo\n if (!supportsSoftDelete) {\n logger.debug(`Table ${baseRepo.tableName} does not support soft delete, skipping extension`);\n return repo;\n }\n\n logger.debug(`Extending repository for table ${baseRepo.tableName} with soft delete methods`);\n\n // Get raw db for queries that need to bypass interceptors\n const rawDb = getRawDb(baseRepo.executor);\n\n const extendedRepo = {\n ...baseRepo,\n\n // Override base methods to filter soft-deleted records\n // Use rawDb to avoid double filtering from interceptor\n async findAll(): Promise<unknown[]> {\n if (!includeDeleted) {\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(deletedAtColumn as never, 'is', null)\n .execute();\n return result as unknown[];\n }\n // Include deleted: return all records\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .execute();\n return result as unknown[];\n },\n\n async findById(id: number): Promise<unknown> {\n if (!includeDeleted) {\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .where(deletedAtColumn as never, 'is', null)\n .executeTakeFirst();\n return result ?? null;\n }\n // Include deleted: find by id regardless of deleted status\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n return result ?? null;\n },\n\n async softDelete(id: number): Promise<unknown> {\n logger.info(`Soft deleting record ${id} from ${baseRepo.tableName}`);\n // Use rawDb to bypass interceptors (UPDATE doesn't need filtering anyway)\n await rawDb\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: sql`CURRENT_TIMESTAMP` } as never)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n\n // Fetch the updated record - use rawDb to see the just-deleted record\n const record = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n\n if (!record) {\n logger.warn(`Record ${id} not found in ${baseRepo.tableName} for soft delete`);\n throw new NotFoundError('Record', { id });\n }\n\n return record;\n },\n\n async restore(id: number): Promise<unknown> {\n logger.info(`Restoring soft-deleted record ${id} from ${baseRepo.tableName}`);\n await rawDb\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: null } as never)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n\n // Fetch the restored record\n const record = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n\n if (!record) {\n logger.warn(`Record ${id} not found in ${baseRepo.tableName} for restore`);\n throw new NotFoundError('Record', { id });\n }\n\n return record;\n },\n\n async hardDelete(id: number): Promise<void> {\n logger.info(`Hard deleting record ${id} from ${baseRepo.tableName}`);\n await rawDb\n .deleteFrom(baseRepo.tableName)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n },\n\n async findWithDeleted(id: number): Promise<unknown> {\n // Use rawDb to bypass soft-delete filter\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, '=', id as never)\n .executeTakeFirst();\n return result ?? null;\n },\n\n async findAllWithDeleted(): Promise<unknown[]> {\n // Use rawDb to bypass soft-delete filter and return ALL records\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .execute();\n return result as unknown[];\n },\n\n async findDeleted(): Promise<unknown[]> {\n // Use rawDb to bypass soft-delete filter, then filter for deleted only\n const result = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(deletedAtColumn as never, 'is not', null)\n .execute();\n return result as unknown[];\n },\n\n async softDeleteMany(ids: (number | string)[]): Promise<unknown[]> {\n if (ids.length === 0) {\n return [];\n }\n\n logger.info(`Soft deleting ${ids.length} records from ${baseRepo.tableName}`);\n\n await rawDb\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: sql`CURRENT_TIMESTAMP` } as never)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n // Use rawDb to see the just-deleted records\n const records = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n if (records.length !== ids.length) {\n const foundIds = records.map((r: any) => r[primaryKeyColumn]);\n const missingIds = ids.filter((id) => !foundIds.includes(id));\n logger.warn(`Some records not found for soft delete: ${missingIds.join(', ')}`);\n throw new NotFoundError('Records', { ids: missingIds });\n }\n\n return records as unknown[];\n },\n\n async restoreMany(ids: (number | string)[]): Promise<unknown[]> {\n if (ids.length === 0) {\n return [];\n }\n\n logger.info(`Restoring ${ids.length} soft-deleted records from ${baseRepo.tableName}`);\n\n await rawDb\n .updateTable(baseRepo.tableName)\n .set({ [deletedAtColumn]: null } as never)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n const records = await rawDb\n .selectFrom(baseRepo.tableName)\n .selectAll()\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n\n return records as unknown[];\n },\n\n async hardDeleteMany(ids: (number | string)[]): Promise<void> {\n if (ids.length === 0) {\n return;\n }\n\n logger.info(`Hard deleting ${ids.length} records from ${baseRepo.tableName}`);\n\n await rawDb\n .deleteFrom(baseRepo.tableName)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute();\n },\n };\n\n return extendedRepo as T;\n },\n };\n};\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kysera/soft-delete",
3
- "version": "0.6.0",
4
- "description": "Soft delete plugin for Kysera ORM",
3
+ "version": "0.7.0",
4
+ "description": "Soft delete plugin for Kysely repositories",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -16,11 +16,12 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@kysera/core": "0.6.0"
19
+ "@kysera/executor": "0.7.0",
20
+ "@kysera/core": "0.7.0"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/better-sqlite3": "^7.6.13",
23
- "@types/node": "^24.10.1",
24
+ "@types/node": "^25.0.0",
24
25
  "@vitest/coverage-v8": "^4.0.15",
25
26
  "better-sqlite3": "^12.5.0",
26
27
  "kysely": "^0.28.8",
@@ -28,12 +29,13 @@
28
29
  "typescript": "^5.9.3",
29
30
  "vitest": "^4.0.15",
30
31
  "zod": "^4.1.13",
31
- "@kysera/repository": "0.6.0"
32
+ "@kysera/dal": "0.7.0",
33
+ "@kysera/repository": "0.7.0"
32
34
  },
33
35
  "peerDependencies": {
34
36
  "kysely": ">=0.28.8",
35
37
  "zod": "^4.1.13",
36
- "@kysera/repository": "0.6.0"
38
+ "@kysera/executor": "0.7.0"
37
39
  },
38
40
  "peerDependenciesMeta": {
39
41
  "zod": {
@@ -42,7 +44,7 @@
42
44
  },
43
45
  "keywords": [
44
46
  "kysely",
45
- "orm",
47
+ "data-access",
46
48
  "soft-delete",
47
49
  "plugin"
48
50
  ],
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Plugin, AnyQueryBuilder, Repository } from '@kysera/repository';
1
+ import type { Plugin, QueryBuilderContext } from '@kysera/executor';
2
+ import { getRawDb } from '@kysera/executor';
2
3
  import type { SelectQueryBuilder, Kysely } from 'kysely';
3
4
  import { sql } from 'kysely';
4
5
  import { NotFoundError, silentLogger } from '@kysera/core';
@@ -88,8 +89,9 @@ export interface SoftDeleteMethods<T> {
88
89
 
89
90
  /**
90
91
  * Repository extended with soft delete methods
92
+ * Note: Repository type is from @kysera/repository
91
93
  */
92
- export type SoftDeleteRepository<Entity, DB> = Repository<Entity, DB> & SoftDeleteMethods<Entity>;
94
+ export type SoftDeleteRepository<Entity> = any & SoftDeleteMethods<Entity>;
93
95
 
94
96
  interface BaseRepository {
95
97
  tableName: string;
@@ -100,7 +102,7 @@ interface BaseRepository {
100
102
  }
101
103
 
102
104
  /**
103
- * Soft Delete Plugin for Kysera ORM
105
+ * Soft Delete Plugin for Kysera
104
106
  *
105
107
  * This plugin implements soft delete functionality using the Method Override pattern:
106
108
  * - Automatically filters out soft-deleted records from SELECT queries
@@ -202,7 +204,7 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
202
204
 
203
205
  return {
204
206
  name: '@kysera/soft-delete',
205
- version: '0.5.1',
207
+ version: '0.7.0',
206
208
 
207
209
  /**
208
210
  * Intercept queries to automatically filter soft-deleted records.
@@ -214,11 +216,10 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
214
216
  * - Use hardDelete() method to bypass soft delete and perform a real DELETE
215
217
  *
216
218
  * This approach is simpler and more explicit than full query interception.
219
+ *
220
+ * Works with both Repository and DAL patterns through the unified executor layer.
217
221
  */
218
- interceptQuery<QB extends AnyQueryBuilder>(
219
- qb: QB,
220
- context: { operation: string; table: string; metadata: Record<string, unknown> }
221
- ): QB {
222
+ interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
222
223
  // Check if table supports soft delete
223
224
  const supportsSoftDelete = !tables || tables.includes(context.table);
224
225
 
@@ -283,29 +284,34 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
283
284
 
284
285
  logger.debug(`Extending repository for table ${baseRepo.tableName} with soft delete methods`);
285
286
 
286
- // Wrap original methods to apply soft delete filtering
287
- const originalFindAll = baseRepo.findAll.bind(baseRepo);
288
- const originalFindById = baseRepo.findById.bind(baseRepo);
287
+ // Get raw db for queries that need to bypass interceptors
288
+ const rawDb = getRawDb(baseRepo.executor);
289
289
 
290
290
  const extendedRepo = {
291
291
  ...baseRepo,
292
292
 
293
293
  // Override base methods to filter soft-deleted records
294
+ // Use rawDb to avoid double filtering from interceptor
294
295
  async findAll(): Promise<unknown[]> {
295
296
  if (!includeDeleted) {
296
- const result = await baseRepo.executor
297
+ const result = await rawDb
297
298
  .selectFrom(baseRepo.tableName)
298
299
  .selectAll()
299
300
  .where(deletedAtColumn as never, 'is', null)
300
301
  .execute();
301
302
  return result as unknown[];
302
303
  }
303
- return await originalFindAll();
304
+ // Include deleted: return all records
305
+ const result = await rawDb
306
+ .selectFrom(baseRepo.tableName)
307
+ .selectAll()
308
+ .execute();
309
+ return result as unknown[];
304
310
  },
305
311
 
306
312
  async findById(id: number): Promise<unknown> {
307
313
  if (!includeDeleted) {
308
- const result = await baseRepo.executor
314
+ const result = await rawDb
309
315
  .selectFrom(baseRepo.tableName)
310
316
  .selectAll()
311
317
  .where(primaryKeyColumn as never, '=', id as never)
@@ -313,44 +319,31 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
313
319
  .executeTakeFirst();
314
320
  return result ?? null;
315
321
  }
316
- // When includeDeleted is true, use custom PK column if configured
317
- // Otherwise fall back to original method (which uses 'id')
318
- if (primaryKeyColumn !== 'id') {
319
- const result = await baseRepo.executor
320
- .selectFrom(baseRepo.tableName)
321
- .selectAll()
322
- .where(primaryKeyColumn as never, '=', id as never)
323
- .executeTakeFirst();
324
- return result ?? null;
325
- }
326
- return await originalFindById(id);
322
+ // Include deleted: find by id regardless of deleted status
323
+ const result = await rawDb
324
+ .selectFrom(baseRepo.tableName)
325
+ .selectAll()
326
+ .where(primaryKeyColumn as never, '=', id as never)
327
+ .executeTakeFirst();
328
+ return result ?? null;
327
329
  },
328
330
 
329
331
  async softDelete(id: number): Promise<unknown> {
330
332
  logger.info(`Soft deleting record ${id} from ${baseRepo.tableName}`);
331
- // Use CURRENT_TIMESTAMP directly in SQL to avoid datetime format issues
332
- // This works across all databases (MySQL, PostgreSQL, SQLite)
333
- // We bypass repository.update() to avoid Zod validation issues with RawBuilder
334
- await baseRepo.executor
333
+ // Use rawDb to bypass interceptors (UPDATE doesn't need filtering anyway)
334
+ await rawDb
335
335
  .updateTable(baseRepo.tableName)
336
336
  .set({ [deletedAtColumn]: sql`CURRENT_TIMESTAMP` } as never)
337
337
  .where(primaryKeyColumn as never, '=', id as never)
338
338
  .execute();
339
339
 
340
- // Fetch the updated record to verify it exists
341
- // Use custom PK column if configured
342
- let record;
343
- if (primaryKeyColumn !== 'id') {
344
- record = await baseRepo.executor
345
- .selectFrom(baseRepo.tableName)
346
- .selectAll()
347
- .where(primaryKeyColumn as never, '=', id as never)
348
- .executeTakeFirst();
349
- } else {
350
- record = await originalFindById(id);
351
- }
340
+ // Fetch the updated record - use rawDb to see the just-deleted record
341
+ const record = await rawDb
342
+ .selectFrom(baseRepo.tableName)
343
+ .selectAll()
344
+ .where(primaryKeyColumn as never, '=', id as never)
345
+ .executeTakeFirst();
352
346
 
353
- // If record not found or deleted_at not set, throw error
354
347
  if (!record) {
355
348
  logger.warn(`Record ${id} not found in ${baseRepo.tableName} for soft delete`);
356
349
  throw new NotFoundError('Record', { id });
@@ -361,24 +354,18 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
361
354
 
362
355
  async restore(id: number): Promise<unknown> {
363
356
  logger.info(`Restoring soft-deleted record ${id} from ${baseRepo.tableName}`);
364
- // Use direct executor to support custom primary key columns
365
- await baseRepo.executor
357
+ await rawDb
366
358
  .updateTable(baseRepo.tableName)
367
359
  .set({ [deletedAtColumn]: null } as never)
368
360
  .where(primaryKeyColumn as never, '=', id as never)
369
361
  .execute();
370
362
 
371
- // Fetch and return the restored record
372
- let record;
373
- if (primaryKeyColumn !== 'id') {
374
- record = await baseRepo.executor
375
- .selectFrom(baseRepo.tableName)
376
- .selectAll()
377
- .where(primaryKeyColumn as never, '=', id as never)
378
- .executeTakeFirst();
379
- } else {
380
- record = await originalFindById(id);
381
- }
363
+ // Fetch the restored record
364
+ const record = await rawDb
365
+ .selectFrom(baseRepo.tableName)
366
+ .selectAll()
367
+ .where(primaryKeyColumn as never, '=', id as never)
368
+ .executeTakeFirst();
382
369
 
383
370
  if (!record) {
384
371
  logger.warn(`Record ${id} not found in ${baseRepo.tableName} for restore`);
@@ -390,33 +377,34 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
390
377
 
391
378
  async hardDelete(id: number): Promise<void> {
392
379
  logger.info(`Hard deleting record ${id} from ${baseRepo.tableName}`);
393
- // Direct hard delete - bypass soft delete
394
- await baseRepo.executor
380
+ await rawDb
395
381
  .deleteFrom(baseRepo.tableName)
396
382
  .where(primaryKeyColumn as never, '=', id as never)
397
383
  .execute();
398
384
  },
399
385
 
400
386
  async findWithDeleted(id: number): Promise<unknown> {
401
- // Use custom PK column if configured, otherwise use original method
402
- if (primaryKeyColumn !== 'id') {
403
- const result = await baseRepo.executor
404
- .selectFrom(baseRepo.tableName)
405
- .selectAll()
406
- .where(primaryKeyColumn as never, '=', id as never)
407
- .executeTakeFirst();
408
- return result ?? null;
409
- }
410
- return await originalFindById(id);
387
+ // Use rawDb to bypass soft-delete filter
388
+ const result = await rawDb
389
+ .selectFrom(baseRepo.tableName)
390
+ .selectAll()
391
+ .where(primaryKeyColumn as never, '=', id as never)
392
+ .executeTakeFirst();
393
+ return result ?? null;
411
394
  },
412
395
 
413
396
  async findAllWithDeleted(): Promise<unknown[]> {
414
- // Use original method without filtering
415
- return await originalFindAll();
397
+ // Use rawDb to bypass soft-delete filter and return ALL records
398
+ const result = await rawDb
399
+ .selectFrom(baseRepo.tableName)
400
+ .selectAll()
401
+ .execute();
402
+ return result as unknown[];
416
403
  },
417
404
 
418
405
  async findDeleted(): Promise<unknown[]> {
419
- const result = await baseRepo.executor
406
+ // Use rawDb to bypass soft-delete filter, then filter for deleted only
407
+ const result = await rawDb
420
408
  .selectFrom(baseRepo.tableName)
421
409
  .selectAll()
422
410
  .where(deletedAtColumn as never, 'is not', null)
@@ -425,28 +413,25 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
425
413
  },
426
414
 
427
415
  async softDeleteMany(ids: (number | string)[]): Promise<unknown[]> {
428
- // Handle empty arrays gracefully
429
416
  if (ids.length === 0) {
430
417
  return [];
431
418
  }
432
419
 
433
420
  logger.info(`Soft deleting ${ids.length} records from ${baseRepo.tableName}`);
434
421
 
435
- // Efficient bulk UPDATE query
436
- await baseRepo.executor
422
+ await rawDb
437
423
  .updateTable(baseRepo.tableName)
438
424
  .set({ [deletedAtColumn]: sql`CURRENT_TIMESTAMP` } as never)
439
425
  .where(primaryKeyColumn as never, 'in', ids as never)
440
426
  .execute();
441
427
 
442
- // Fetch all affected records to verify and return them
443
- const records = await baseRepo.executor
428
+ // Use rawDb to see the just-deleted records
429
+ const records = await rawDb
444
430
  .selectFrom(baseRepo.tableName)
445
431
  .selectAll()
446
432
  .where(primaryKeyColumn as never, 'in', ids as never)
447
433
  .execute();
448
434
 
449
- // Verify all records were found
450
435
  if (records.length !== ids.length) {
451
436
  const foundIds = records.map((r: any) => r[primaryKeyColumn]);
452
437
  const missingIds = ids.filter((id) => !foundIds.includes(id));
@@ -458,22 +443,19 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
458
443
  },
459
444
 
460
445
  async restoreMany(ids: (number | string)[]): Promise<unknown[]> {
461
- // Handle empty arrays gracefully
462
446
  if (ids.length === 0) {
463
447
  return [];
464
448
  }
465
449
 
466
450
  logger.info(`Restoring ${ids.length} soft-deleted records from ${baseRepo.tableName}`);
467
451
 
468
- // Efficient bulk UPDATE query to restore records
469
- await baseRepo.executor
452
+ await rawDb
470
453
  .updateTable(baseRepo.tableName)
471
454
  .set({ [deletedAtColumn]: null } as never)
472
455
  .where(primaryKeyColumn as never, 'in', ids as never)
473
456
  .execute();
474
457
 
475
- // Fetch all affected records to return them
476
- const records = await baseRepo.executor
458
+ const records = await rawDb
477
459
  .selectFrom(baseRepo.tableName)
478
460
  .selectAll()
479
461
  .where(primaryKeyColumn as never, 'in', ids as never)
@@ -483,15 +465,13 @@ export const softDeletePlugin = (options: SoftDeleteOptions = {}): Plugin => {
483
465
  },
484
466
 
485
467
  async hardDeleteMany(ids: (number | string)[]): Promise<void> {
486
- // Handle empty arrays gracefully
487
468
  if (ids.length === 0) {
488
469
  return;
489
470
  }
490
471
 
491
472
  logger.info(`Hard deleting ${ids.length} records from ${baseRepo.tableName}`);
492
473
 
493
- // Efficient bulk DELETE query
494
- await baseRepo.executor
474
+ await rawDb
495
475
  .deleteFrom(baseRepo.tableName)
496
476
  .where(primaryKeyColumn as never, 'in', ids as never)
497
477
  .execute();