@kysera/soft-delete 0.7.2 → 0.7.4

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/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # @kysera/soft-delete
2
2
 
3
- Soft delete plugin for Kysera. Implements soft delete functionality using the Method Override pattern with automatic filtering of deleted records.
3
+ Soft delete plugin for Kysera (v0.7.3). Implements soft delete functionality through @kysera/executor's Unified Execution Layer with automatic filtering of deleted records.
4
4
 
5
5
  ## Features
6
6
 
7
- - Automatic filtering of soft-deleted records in SELECT queries
7
+ - Automatic filtering of soft-deleted records in SELECT queries via @kysera/executor's plugin interception
8
8
  - Repository methods for soft delete operations (softDelete, restore, hardDelete)
9
- - Bulk operations (softDeleteMany, restoreMany, hardDeleteMany)
9
+ - Bulk operations (softDeleteMany, restoreMany, hardDeleteMany) with optimized single-query fetching
10
10
  - Query methods for deleted records (findWithDeleted, findAllWithDeleted, findDeleted)
11
- - Works with both Repository and DAL patterns through unified executor layer
11
+ - Works with both Repository and DAL patterns through @kysera/executor's Unified Execution Layer
12
12
  - Full transaction support with ACID compliance
13
13
  - Configurable deleted column name, primary key, and table filtering
14
14
  - Cross-runtime compatible (Node.js, Bun, Deno)
@@ -38,112 +38,141 @@ bun add @kysera/soft-delete
38
38
 
39
39
  Note: `zod` is optional (used for configuration schema validation in `kysera-cli`)
40
40
 
41
+ ### Optional Zod Schema Validation
42
+
43
+ If you need to validate configuration options (e.g., in a CLI tool or config file), you can import the Zod schema separately:
44
+
45
+ ```typescript
46
+ import { SoftDeleteOptionsSchema } from '@kysera/soft-delete/schema'
47
+
48
+ const result = SoftDeleteOptionsSchema.safeParse({
49
+ deletedAtColumn: 'deleted_at',
50
+ includeDeleted: false,
51
+ tables: ['users', 'posts']
52
+ })
53
+
54
+ if (result.success) {
55
+ console.log('Valid configuration:', result.data)
56
+ } else {
57
+ console.error('Invalid configuration:', result.error)
58
+ }
59
+ ```
60
+
61
+ **Important**: The main package (`@kysera/soft-delete`) works without Zod installed. Only import `/schema` if you need validation functionality.
62
+
41
63
  ## Quick Start
42
64
 
43
65
  ### With Repository Pattern
44
66
 
45
67
  ```typescript
46
- import { createORM } from '@kysera/repository';
47
- import { softDeletePlugin } from '@kysera/soft-delete';
68
+ import { createORM } from '@kysera/repository'
69
+ import { softDeletePlugin } from '@kysera/soft-delete'
70
+ import { createExecutor } from '@kysera/executor'
48
71
 
49
- // Create plugin container with soft-delete plugin
50
- const orm = await createORM(db, [
72
+ // Step 1: Create executor with soft-delete plugin
73
+ const executor = await createExecutor(db, [
51
74
  softDeletePlugin({
52
75
  deletedAtColumn: 'deleted_at',
53
76
  includeDeleted: false,
54
77
  tables: ['users', 'posts'] // Only these tables support soft delete
55
78
  })
56
- ]);
79
+ ])
57
80
 
58
- // Create repository
59
- const userRepo = orm.createRepository(createUserRepository);
81
+ // Step 2: Create ORM with plugin-enabled executor
82
+ const orm = await createORM(executor, [])
83
+
84
+ // Step 3: Create repository
85
+ const userRepo = orm.createRepository(createUserRepository)
60
86
 
61
87
  // Soft delete a user (sets deleted_at timestamp)
62
- await userRepo.softDelete(1);
88
+ await userRepo.softDelete(1)
63
89
 
64
90
  // Find all users (excludes soft-deleted automatically)
65
- const users = await userRepo.findAll();
91
+ const users = await userRepo.findAll()
66
92
 
67
93
  // Find including deleted records
68
- const allUsers = await userRepo.findAllWithDeleted();
94
+ const allUsers = await userRepo.findAllWithDeleted()
69
95
 
70
96
  // Restore a soft-deleted user
71
- await userRepo.restore(1);
97
+ await userRepo.restore(1)
72
98
 
73
99
  // Permanently delete (real DELETE)
74
- await userRepo.hardDelete(1);
100
+ await userRepo.hardDelete(1)
101
+
102
+ // Batch operations (optimized single-query fetching)
103
+ await userRepo.softDeleteMany([1, 2, 3])
104
+ await userRepo.restoreMany([1, 2, 3])
105
+ await userRepo.hardDeleteMany([1, 2, 3])
75
106
  ```
76
107
 
77
108
  ### With DAL Pattern
78
109
 
79
110
  ```typescript
80
- import { createExecutor } from '@kysera/executor';
81
- import { createContext, createQuery, withTransaction } from '@kysera/dal';
82
- import { softDeletePlugin } from '@kysera/soft-delete';
83
- import { sql } from 'kysely';
111
+ import { createExecutor } from '@kysera/executor'
112
+ import { createContext, createQuery, withTransaction } from '@kysera/dal'
113
+ import { softDeletePlugin } from '@kysera/soft-delete'
114
+ import { sql } from 'kysely'
84
115
 
85
- // Create executor with soft-delete plugin
116
+ // Step 1: Create executor with soft-delete plugin (Unified Execution Layer)
86
117
  const executor = await createExecutor(db, [
87
118
  softDeletePlugin({
88
119
  deletedAtColumn: 'deleted_at',
89
120
  includeDeleted: false
90
121
  })
91
- ]);
122
+ ])
92
123
 
93
- // Create context
94
- const ctx = createContext(executor);
124
+ // Step 2: Create context - plugins automatically apply to all queries
125
+ const ctx = createContext(executor)
95
126
 
96
- // Define queries - soft-delete filter applied automatically
97
- const getUsers = createQuery((ctx) =>
98
- ctx.db.selectFrom('users').selectAll().execute()
99
- );
127
+ // Step 3: Define queries - soft-delete filter applied automatically
128
+ const getUsers = createQuery(ctx => ctx.db.selectFrom('users').selectAll().execute())
100
129
 
101
130
  const getUserById = createQuery((ctx, id: number) =>
102
- ctx.db
103
- .selectFrom('users')
104
- .selectAll()
105
- .where('id', '=', id)
106
- .executeTakeFirst()
107
- );
131
+ ctx.db.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirst()
132
+ )
108
133
 
109
134
  // Execute queries - deleted records automatically filtered
110
- const users = await getUsers(ctx); // Excludes soft-deleted
111
- const user = await getUserById(ctx, 1);
135
+ const users = await getUsers(ctx) // Excludes soft-deleted
136
+ const user = await getUserById(ctx, 1)
112
137
 
113
138
  // Soft delete within transaction
114
- await withTransaction(executor, async (txCtx) => {
139
+ await withTransaction(executor, async txCtx => {
115
140
  await txCtx.db
116
141
  .updateTable('users')
117
142
  .set({ deleted_at: sql`CURRENT_TIMESTAMP` })
118
143
  .where('id', '=', 1)
119
- .execute();
144
+ .execute()
120
145
 
121
146
  // Subsequent queries in same transaction see the deletion
122
- const users = await getUsers(txCtx); // User 1 excluded
123
- });
147
+ const users = await getUsers(txCtx) // User 1 excluded
148
+ })
124
149
  ```
125
150
 
126
151
  ## Plugin Architecture
127
152
 
128
- The soft-delete plugin leverages `@kysera/executor` for unified plugin support across both Repository and DAL patterns.
153
+ The soft-delete plugin leverages `@kysera/executor`'s Unified Execution Layer for seamless plugin support across both Repository and DAL patterns.
129
154
 
130
155
  ### How It Works
131
156
 
132
157
  ```typescript
133
- import { createExecutor, getRawDb } from '@kysera/executor';
134
- import type { Plugin, QueryBuilderContext } from '@kysera/executor';
158
+ import { createExecutor, getRawDb } from '@kysera/executor'
159
+ import type { Plugin, QueryBuilderContext } from '@kysera/executor'
135
160
 
136
- // Plugin implements the Plugin interface
137
- const plugin = softDeletePlugin();
161
+ // Step 1: Register plugin with createExecutor() - Unified Execution Layer
162
+ const executor = await createExecutor(db, [
163
+ softDeletePlugin({
164
+ deletedAtColumn: 'deleted_at',
165
+ includeDeleted: false
166
+ })
167
+ ])
138
168
 
139
- // Executor wraps Kysely with plugin interception
140
- const executor = await createExecutor(db, [plugin]);
169
+ // Step 2: Plugin interceptQuery hook adds WHERE clause automatically
170
+ const users = await executor.selectFrom('users').selectAll().execute()
171
+ // SQL: SELECT * FROM users WHERE users.deleted_at IS NULL
141
172
 
142
- // All queries through executor have soft-delete filter applied
143
- const users = await executor
144
- .selectFrom('users')
145
- .selectAll()
146
- .execute(); // WHERE users.deleted_at IS NULL (added automatically)
173
+ // Step 3: Works with both Repository and DAL patterns
174
+ const orm = await createORM(executor, [])
175
+ const ctx = createContext(executor)
147
176
  ```
148
177
 
149
178
  ### Plugin Interface
@@ -152,10 +181,10 @@ The plugin implements the `Plugin` interface from `@kysera/executor`:
152
181
 
153
182
  ```typescript
154
183
  interface Plugin {
155
- name: string;
156
- version: string;
157
- interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB;
158
- extendRepository<T extends object>(repo: T): T;
184
+ name: string
185
+ version: string
186
+ interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB
187
+ extendRepository<T extends object>(repo: T): T
159
188
  }
160
189
  ```
161
190
 
@@ -199,17 +228,14 @@ extendRepository<T extends object>(repo: T): T {
199
228
  The plugin uses `getRawDb()` from `@kysera/executor` to bypass interceptors when needed:
200
229
 
201
230
  ```typescript
202
- import { getRawDb } from '@kysera/executor';
231
+ import { getRawDb } from '@kysera/executor'
203
232
 
204
233
  // Inside plugin's extendRepository method
205
- const rawDb = getRawDb(repo.executor);
234
+ const rawDb = getRawDb(repo.executor)
206
235
 
207
236
  // Use rawDb to bypass soft-delete filter
208
237
  // (needed for findWithDeleted, restore, etc.)
209
- const allRecords = await rawDb
210
- .selectFrom('users')
211
- .selectAll()
212
- .execute(); // No soft-delete filter applied
238
+ const allRecords = await rawDb.selectFrom('users').selectAll().execute() // No soft-delete filter applied
213
239
  ```
214
240
 
215
241
  This is critical for methods like `findWithDeleted()` and `restore()` that need to access soft-deleted records.
@@ -224,35 +250,35 @@ interface SoftDeleteOptions {
224
250
  * Column name for soft delete timestamp.
225
251
  * @default 'deleted_at'
226
252
  */
227
- deletedAtColumn?: string;
253
+ deletedAtColumn?: string
228
254
 
229
255
  /**
230
256
  * Include deleted records by default in queries.
231
257
  * When false, soft-deleted records are automatically filtered out.
232
258
  * @default false
233
259
  */
234
- includeDeleted?: boolean;
260
+ includeDeleted?: boolean
235
261
 
236
262
  /**
237
263
  * List of tables that support soft delete.
238
264
  * If not provided, all tables are assumed to support it.
239
265
  * @example ['users', 'posts', 'comments']
240
266
  */
241
- tables?: string[];
267
+ tables?: string[]
242
268
 
243
269
  /**
244
270
  * Primary key column name used for identifying records.
245
271
  * @default 'id'
246
272
  * @example 'uuid', 'user_id', 'post_id'
247
273
  */
248
- primaryKeyColumn?: string;
274
+ primaryKeyColumn?: string
249
275
 
250
276
  /**
251
277
  * Logger for plugin operations.
252
278
  * Uses KyseraLogger interface from @kysera/core.
253
279
  * @default silentLogger (no output)
254
280
  */
255
- logger?: KyseraLogger;
281
+ logger?: KyseraLogger
256
282
  }
257
283
  ```
258
284
 
@@ -260,35 +286,35 @@ interface SoftDeleteOptions {
260
286
 
261
287
  ```typescript
262
288
  // Default configuration
263
- softDeletePlugin();
289
+ softDeletePlugin()
264
290
 
265
291
  // Custom deleted column
266
292
  softDeletePlugin({
267
293
  deletedAtColumn: 'removed_at'
268
- });
294
+ })
269
295
 
270
296
  // Only specific tables
271
297
  softDeletePlugin({
272
298
  tables: ['users', 'posts'], // Only these tables support soft delete
273
299
  deletedAtColumn: 'deleted_at'
274
- });
300
+ })
275
301
 
276
302
  // Include deleted by default
277
303
  softDeletePlugin({
278
304
  includeDeleted: true // Don't filter deleted records
279
- });
305
+ })
280
306
 
281
307
  // Custom primary key
282
308
  softDeletePlugin({
283
309
  primaryKeyColumn: 'uuid' // For tables using 'uuid' instead of 'id'
284
- });
310
+ })
285
311
 
286
312
  // With logging
287
- import { consoleLogger } from '@kysera/core';
313
+ import { consoleLogger } from '@kysera/core'
288
314
 
289
315
  softDeletePlugin({
290
316
  logger: consoleLogger
291
- });
317
+ })
292
318
  ```
293
319
 
294
320
  ## Repository Methods
@@ -299,15 +325,15 @@ The plugin extends repositories with the following methods:
299
325
 
300
326
  ```typescript
301
327
  interface SoftDeleteMethods<T> {
302
- softDelete(id: number | string): Promise<T>;
303
- restore(id: number | string): Promise<T>;
304
- hardDelete(id: number | string): Promise<void>;
305
- findWithDeleted(id: number | string): Promise<T | null>;
306
- findAllWithDeleted(): Promise<T[]>;
307
- findDeleted(): Promise<T[]>;
308
- softDeleteMany(ids: (number | string)[]): Promise<T[]>;
309
- restoreMany(ids: (number | string)[]): Promise<T[]>;
310
- hardDeleteMany(ids: (number | string)[]): Promise<void>;
328
+ softDelete(id: number | string): Promise<T>
329
+ restore(id: number | string): Promise<T>
330
+ hardDelete(id: number | string): Promise<void>
331
+ findWithDeleted(id: number | string): Promise<T | null>
332
+ findAllWithDeleted(): Promise<T[]>
333
+ findDeleted(): Promise<T[]>
334
+ softDeleteMany(ids: (number | string)[]): Promise<T[]>
335
+ restoreMany(ids: (number | string)[]): Promise<T[]>
336
+ hardDeleteMany(ids: (number | string)[]): Promise<void>
311
337
  }
312
338
  ```
313
339
 
@@ -319,11 +345,11 @@ Marks a record as deleted by setting the `deleted_at` timestamp to `CURRENT_TIME
319
345
 
320
346
  ```typescript
321
347
  // Soft delete user with id 1
322
- const deletedUser = await userRepo.softDelete(1);
323
- console.log(deletedUser.deleted_at); // '2025-12-11T10:30:00Z'
348
+ const deletedUser = await userRepo.softDelete(1)
349
+ console.log(deletedUser.deleted_at) // '2025-12-11T10:30:00Z'
324
350
 
325
351
  // Record still exists in database but won't appear in findAll()
326
- const users = await userRepo.findAll(); // Excludes deleted user
352
+ const users = await userRepo.findAll() // Excludes deleted user
327
353
  ```
328
354
 
329
355
  **Returns**: `Promise<T>` - The soft-deleted record
@@ -335,11 +361,11 @@ Restores a soft-deleted record by setting `deleted_at` to `null`.
335
361
 
336
362
  ```typescript
337
363
  // Restore soft-deleted user
338
- const restoredUser = await userRepo.restore(1);
339
- console.log(restoredUser.deleted_at); // null
364
+ const restoredUser = await userRepo.restore(1)
365
+ console.log(restoredUser.deleted_at) // null
340
366
 
341
367
  // Record now appears in queries again
342
- const users = await userRepo.findAll(); // Includes restored user
368
+ const users = await userRepo.findAll() // Includes restored user
343
369
  ```
344
370
 
345
371
  **Returns**: `Promise<T>` - The restored record
@@ -351,10 +377,10 @@ Permanently deletes a record using real SQL DELETE. Cannot be restored.
351
377
 
352
378
  ```typescript
353
379
  // Permanently delete user
354
- await userRepo.hardDelete(1);
380
+ await userRepo.hardDelete(1)
355
381
 
356
382
  // Record is gone forever
357
- const user = await userRepo.findWithDeleted(1); // null
383
+ const user = await userRepo.findWithDeleted(1) // null
358
384
  ```
359
385
 
360
386
  **Returns**: `Promise<void>`
@@ -365,9 +391,9 @@ Finds a record by ID including soft-deleted records.
365
391
 
366
392
  ```typescript
367
393
  // Find user even if soft-deleted
368
- const user = await userRepo.findWithDeleted(1);
394
+ const user = await userRepo.findWithDeleted(1)
369
395
  if (user?.deleted_at) {
370
- console.log('User was soft-deleted');
396
+ console.log('User was soft-deleted')
371
397
  }
372
398
  ```
373
399
 
@@ -379,9 +405,9 @@ Returns all records including soft-deleted ones.
379
405
 
380
406
  ```typescript
381
407
  // Get all users including deleted
382
- const allUsers = await userRepo.findAllWithDeleted();
383
- const deletedCount = allUsers.filter(u => u.deleted_at !== null).length;
384
- console.log(`${deletedCount} deleted users`);
408
+ const allUsers = await userRepo.findAllWithDeleted()
409
+ const deletedCount = allUsers.filter(u => u.deleted_at !== null).length
410
+ console.log(`${deletedCount} deleted users`)
385
411
  ```
386
412
 
387
413
  **Returns**: `Promise<T[]>`
@@ -392,8 +418,8 @@ Returns only soft-deleted records.
392
418
 
393
419
  ```typescript
394
420
  // Get only deleted users
395
- const deletedUsers = await userRepo.findDeleted();
396
- console.log(`Found ${deletedUsers.length} deleted users`);
421
+ const deletedUsers = await userRepo.findDeleted()
422
+ console.log(`Found ${deletedUsers.length} deleted users`)
397
423
  ```
398
424
 
399
425
  **Returns**: `Promise<T[]>`
@@ -404,8 +430,8 @@ Soft deletes multiple records in a single operation (bulk operation).
404
430
 
405
431
  ```typescript
406
432
  // Soft delete multiple users at once
407
- const deletedUsers = await userRepo.softDeleteMany([1, 2, 3]);
408
- console.log(`Soft deleted ${deletedUsers.length} users`);
433
+ const deletedUsers = await userRepo.softDeleteMany([1, 2, 3])
434
+ console.log(`Soft deleted ${deletedUsers.length} users`)
409
435
  ```
410
436
 
411
437
  **Returns**: `Promise<T[]>` - Array of deleted records
@@ -417,8 +443,8 @@ Restores multiple soft-deleted records in a single operation.
417
443
 
418
444
  ```typescript
419
445
  // Restore multiple users at once
420
- const restoredUsers = await userRepo.restoreMany([1, 2, 3]);
421
- console.log(`Restored ${restoredUsers.length} users`);
446
+ const restoredUsers = await userRepo.restoreMany([1, 2, 3])
447
+ console.log(`Restored ${restoredUsers.length} users`)
422
448
  ```
423
449
 
424
450
  **Returns**: `Promise<T[]>` - Array of restored records
@@ -429,7 +455,7 @@ Permanently deletes multiple records in a single operation.
429
455
 
430
456
  ```typescript
431
457
  // Permanently delete multiple users
432
- await userRepo.hardDeleteMany([1, 2, 3]);
458
+ await userRepo.hardDeleteMany([1, 2, 3])
433
459
  ```
434
460
 
435
461
  **Returns**: `Promise<void>`
@@ -441,28 +467,22 @@ The soft-delete plugin works seamlessly with the DAL pattern through the executo
441
467
  ### Automatic Filtering in DAL Queries
442
468
 
443
469
  ```typescript
444
- import { createExecutor } from '@kysera/executor';
445
- import { createContext, createQuery } from '@kysera/dal';
470
+ import { createExecutor } from '@kysera/executor'
471
+ import { createContext, createQuery } from '@kysera/dal'
446
472
 
447
- const executor = await createExecutor(db, [softDeletePlugin()]);
473
+ const executor = await createExecutor(db, [softDeletePlugin()])
448
474
 
449
475
  // Define queries - filter applied automatically
450
- const getAllUsers = createQuery((ctx) =>
451
- ctx.db.selectFrom('users').selectAll().execute()
452
- );
476
+ const getAllUsers = createQuery(ctx => ctx.db.selectFrom('users').selectAll().execute())
453
477
 
454
478
  const getUserById = createQuery((ctx, id: number) =>
455
- ctx.db
456
- .selectFrom('users')
457
- .selectAll()
458
- .where('id', '=', id)
459
- .executeTakeFirst()
460
- );
479
+ ctx.db.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirst()
480
+ )
461
481
 
462
482
  // Execute queries
463
- const ctx = createContext(executor);
464
- const users = await getAllUsers(ctx); // Excludes deleted
465
- const user = await getUserById(ctx, 1);
483
+ const ctx = createContext(executor)
484
+ const users = await getAllUsers(ctx) // Excludes deleted
485
+ const user = await getUserById(ctx, 1)
466
486
  ```
467
487
 
468
488
  ### Query Interception
@@ -474,10 +494,7 @@ The plugin's `interceptQuery` method modifies SELECT query builders:
474
494
  ctx.db.selectFrom('users').selectAll()
475
495
 
476
496
  // After plugin interception
477
- ctx.db
478
- .selectFrom('users')
479
- .selectAll()
480
- .where('users.deleted_at', 'is', null) // Added automatically
497
+ ctx.db.selectFrom('users').selectAll().where('users.deleted_at', 'is', null) // Added automatically
481
498
  ```
482
499
 
483
500
  ### Operations Not Intercepted
@@ -492,44 +509,41 @@ The plugin uses Method Override pattern, not full query interception:
492
509
  To perform soft deletes, use the `softDelete()` method explicitly:
493
510
 
494
511
  ```typescript
495
- import { sql } from 'kysely';
512
+ import { sql } from 'kysely'
496
513
 
497
514
  // ❌ This performs a real DELETE (not soft delete)
498
- await ctx.db.deleteFrom('users').where('id', '=', 1).execute();
515
+ await ctx.db.deleteFrom('users').where('id', '=', 1).execute()
499
516
 
500
517
  // ✅ Use softDelete method instead (in Repository pattern)
501
- await userRepo.softDelete(1);
518
+ await userRepo.softDelete(1)
502
519
 
503
520
  // ✅ Or manual UPDATE in DAL pattern
504
521
  await ctx.db
505
522
  .updateTable('users')
506
523
  .set({ deleted_at: sql`CURRENT_TIMESTAMP` })
507
524
  .where('id', '=', 1)
508
- .execute();
525
+ .execute()
509
526
  ```
510
527
 
511
528
  ### DAL Transaction Support
512
529
 
513
530
  ```typescript
514
- import { withTransaction } from '@kysera/dal';
515
- import { sql } from 'kysely';
531
+ import { withTransaction } from '@kysera/dal'
532
+ import { sql } from 'kysely'
516
533
 
517
- await withTransaction(executor, async (txCtx) => {
534
+ await withTransaction(executor, async txCtx => {
518
535
  // Soft delete user
519
536
  await txCtx.db
520
537
  .updateTable('users')
521
538
  .set({ deleted_at: sql`CURRENT_TIMESTAMP` })
522
539
  .where('id', '=', 1)
523
- .execute();
540
+ .execute()
524
541
 
525
542
  // Query in same transaction sees deletion
526
- const users = await txCtx.db
527
- .selectFrom('users')
528
- .selectAll()
529
- .execute(); // User 1 excluded
543
+ const users = await txCtx.db.selectFrom('users').selectAll().execute() // User 1 excluded
530
544
 
531
545
  // If transaction rolls back, soft delete is also rolled back
532
- });
546
+ })
533
547
  ```
534
548
 
535
549
  ## Transaction Behavior
@@ -539,38 +553,38 @@ The soft-delete plugin respects ACID properties and works correctly with transac
539
553
  ### ACID Compliance
540
554
 
541
555
  ```typescript
542
- import { withTransaction } from '@kysera/dal';
556
+ import { withTransaction } from '@kysera/dal'
543
557
 
544
558
  // ✅ CORRECT: Soft delete commits with transaction
545
- await withTransaction(executor, async (txCtx) => {
546
- const repos = createRepositories(txCtx); // Use transaction executor
547
- await repos.users.softDelete(1);
548
- await repos.posts.softDeleteMany([1, 2, 3]);
559
+ await withTransaction(executor, async txCtx => {
560
+ const repos = createRepositories(txCtx) // Use transaction executor
561
+ await repos.users.softDelete(1)
562
+ await repos.posts.softDeleteMany([1, 2, 3])
549
563
  // If transaction commits, both operations commit
550
564
  // If transaction rolls back, both operations roll back
551
- });
565
+ })
552
566
  ```
553
567
 
554
568
  ### Rollback Behavior
555
569
 
556
570
  ```typescript
557
571
  try {
558
- await withTransaction(executor, async (txCtx) => {
559
- const repos = createRepositories(txCtx);
572
+ await withTransaction(executor, async txCtx => {
573
+ const repos = createRepositories(txCtx)
560
574
 
561
575
  // Soft delete user
562
- await repos.users.softDelete(1);
576
+ await repos.users.softDelete(1)
563
577
 
564
578
  // Force rollback
565
- throw new Error('Force rollback');
566
- });
579
+ throw new Error('Force rollback')
580
+ })
567
581
  } catch (error) {
568
582
  // Transaction rolled back
569
583
  }
570
584
 
571
585
  // Verify soft-delete was rolled back
572
- const user = await userRepo.findById(1);
573
- console.log(user?.deleted_at); // null (not deleted)
586
+ const user = await userRepo.findById(1)
587
+ console.log(user?.deleted_at) // null (not deleted)
574
588
  ```
575
589
 
576
590
  ### Cascade Soft Delete Pattern
@@ -579,31 +593,31 @@ The plugin does not automatically cascade soft deletes. You must implement casca
579
593
 
580
594
  ```typescript
581
595
  // Manual cascade soft delete
582
- await db.transaction().execute(async (trx) => {
583
- const repos = createRepositories(trx);
584
- const userId = 123;
596
+ await db.transaction().execute(async trx => {
597
+ const repos = createRepositories(trx)
598
+ const userId = 123
585
599
 
586
600
  // Step 1: Find related records
587
- const userPosts = await repos.posts.findBy({ user_id: userId });
588
- const postIds = userPosts.map(p => p.id);
601
+ const userPosts = await repos.posts.findBy({ user_id: userId })
602
+ const postIds = userPosts.map(p => p.id)
589
603
 
590
604
  // Step 2: Soft delete children first
591
605
  if (postIds.length > 0) {
592
606
  const postComments = await repos.comments.findBy({
593
607
  post_id: { in: postIds }
594
- });
595
- const commentIds = postComments.map(c => c.id);
608
+ })
609
+ const commentIds = postComments.map(c => c.id)
596
610
 
597
611
  if (commentIds.length > 0) {
598
- await repos.comments.softDeleteMany(commentIds);
612
+ await repos.comments.softDeleteMany(commentIds)
599
613
  }
600
614
 
601
- await repos.posts.softDeleteMany(postIds);
615
+ await repos.posts.softDeleteMany(postIds)
602
616
  }
603
617
 
604
618
  // Step 3: Soft delete parent
605
- await repos.users.softDelete(userId);
606
- });
619
+ await repos.users.softDelete(userId)
620
+ })
607
621
  ```
608
622
 
609
623
  ### Transaction Isolation
@@ -611,20 +625,20 @@ await db.transaction().execute(async (trx) => {
611
625
  Soft-delete operations within a transaction are immediately visible to subsequent queries in the same transaction:
612
626
 
613
627
  ```typescript
614
- await withTransaction(executor, async (txCtx) => {
615
- const repos = createRepositories(txCtx);
628
+ await withTransaction(executor, async txCtx => {
629
+ const repos = createRepositories(txCtx)
616
630
 
617
631
  // Before soft delete
618
- const usersBefore = await repos.users.findAll();
619
- console.log(usersBefore.length); // 10
632
+ const usersBefore = await repos.users.findAll()
633
+ console.log(usersBefore.length) // 10
620
634
 
621
635
  // Soft delete user
622
- await repos.users.softDelete(1);
636
+ await repos.users.softDelete(1)
623
637
 
624
638
  // Immediately visible in same transaction
625
- const usersAfter = await repos.users.findAll();
626
- console.log(usersAfter.length); // 9
627
- });
639
+ const usersAfter = await repos.users.findAll()
640
+ console.log(usersAfter.length) // 9
641
+ })
628
642
  ```
629
643
 
630
644
  ## Database Schema Requirements
@@ -659,7 +673,7 @@ CREATE TABLE posts (
659
673
  softDeletePlugin({
660
674
  deletedAtColumn: 'removed_at',
661
675
  tables: ['posts']
662
- });
676
+ })
663
677
  ```
664
678
 
665
679
  ### Custom Primary Key
@@ -677,31 +691,36 @@ CREATE TABLE comments (
677
691
  softDeletePlugin({
678
692
  primaryKeyColumn: 'comment_id',
679
693
  tables: ['comments']
680
- });
694
+ })
681
695
  ```
682
696
 
683
697
  ## Type Safety
684
698
 
685
- The plugin maintains full type safety with TypeScript:
699
+ The plugin maintains full type safety with TypeScript. The `SoftDeleteRepository` type uses `Record<string, never>` for the database type parameter by default:
686
700
 
687
701
  ```typescript
688
- import type { SoftDeleteRepository } from '@kysera/soft-delete';
702
+ import type { SoftDeleteRepository } from '@kysera/soft-delete'
689
703
 
690
704
  // Extend repository type with soft delete methods
691
- type UserRepository = SoftDeleteRepository<User>;
705
+ // Default: SoftDeleteRepository<User, Record<string, never>>
706
+ type UserRepository = SoftDeleteRepository<User>
692
707
 
693
- const userRepo: UserRepository = orm.createRepository((executor) => {
694
- const base = createRepositoryFactory(executor);
708
+ const userRepo: UserRepository = orm.createRepository(executor => {
709
+ const base = createRepositoryFactory(executor)
695
710
  return base.create({
696
711
  tableName: 'users',
697
- mapRow: (row) => row as User
698
- });
699
- });
712
+ mapRow: row => row as User
713
+ })
714
+ })
700
715
 
701
716
  // TypeScript knows about soft delete methods
702
- const deletedUser: User = await userRepo.softDelete(1);
703
- const allUsers: User[] = await userRepo.findAllWithDeleted();
704
- const deletedUsers: User[] = await userRepo.findDeleted();
717
+ const deletedUser: User = await userRepo.softDelete(1)
718
+ const allUsers: User[] = await userRepo.findAllWithDeleted()
719
+ const deletedUsers: User[] = await userRepo.findDeleted()
720
+
721
+ // Batch operations are also typed
722
+ const deleted: User[] = await userRepo.softDeleteMany([1, 2, 3])
723
+ const restored: User[] = await userRepo.restoreMany([1, 2, 3])
705
724
  ```
706
725
 
707
726
  ## Error Handling
@@ -709,22 +728,22 @@ const deletedUsers: User[] = await userRepo.findDeleted();
709
728
  The plugin uses error types from `@kysera/core`:
710
729
 
711
730
  ```typescript
712
- import { NotFoundError } from '@kysera/core';
731
+ import { NotFoundError } from '@kysera/core'
713
732
 
714
733
  try {
715
- await userRepo.softDelete(999); // Non-existent ID
734
+ await userRepo.softDelete(999) // Non-existent ID
716
735
  } catch (error) {
717
736
  if (error instanceof NotFoundError) {
718
- console.error('User not found:', error.metadata);
737
+ console.error('User not found:', error.metadata)
719
738
  // error.metadata = { id: 999 }
720
739
  }
721
740
  }
722
741
 
723
742
  try {
724
- await userRepo.softDeleteMany([1, 2, 999]); // One ID doesn't exist
743
+ await userRepo.softDeleteMany([1, 2, 999]) // One ID doesn't exist
725
744
  } catch (error) {
726
745
  if (error instanceof NotFoundError) {
727
- console.error('Some users not found:', error.metadata);
746
+ console.error('Some users not found:', error.metadata)
728
747
  // error.metadata = { ids: [999] }
729
748
  }
730
749
  }
@@ -761,11 +780,11 @@ Use bulk methods for better performance when operating on multiple records:
761
780
  ```typescript
762
781
  // ❌ Inefficient: N queries
763
782
  for (const id of userIds) {
764
- await userRepo.softDelete(id);
783
+ await userRepo.softDelete(id)
765
784
  }
766
785
 
767
786
  // ✅ Efficient: Single query
768
- await userRepo.softDeleteMany(userIds);
787
+ await userRepo.softDeleteMany(userIds)
769
788
  ```
770
789
 
771
790
  ## Architecture Notes
@@ -798,16 +817,13 @@ The plugin uses `getRawDb()` to access the underlying Kysely instance without pl
798
817
  - `softDelete()`, `restore()`: Need to fetch records after update
799
818
 
800
819
  ```typescript
801
- import { getRawDb } from '@kysera/executor';
820
+ import { getRawDb } from '@kysera/executor'
802
821
 
803
822
  // Inside plugin
804
- const rawDb = getRawDb(repo.executor);
823
+ const rawDb = getRawDb(repo.executor)
805
824
 
806
825
  // Bypass soft-delete filter
807
- const allRecords = await rawDb
808
- .selectFrom('users')
809
- .selectAll()
810
- .execute();
826
+ const allRecords = await rawDb.selectFrom('users').selectAll().execute()
811
827
  ```
812
828
 
813
829
  ## Testing
@@ -829,6 +845,7 @@ pnpm test dal-integration.test.ts
829
845
  ```
830
846
 
831
847
  Test files:
848
+
832
849
  - `test/dal-integration.test.ts` - DAL pattern with createQuery and withTransaction
833
850
  - `test/soft-delete-comprehensive.test.ts` - All 9 methods + configuration options
834
851
  - `test/soft-delete-repository.test.ts` - Repository pattern core functionality