@nest-toolbox/typeorm-soft-delete 1.8.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/README.md +540 -0
- package/dist/decorators.d.ts +47 -0
- package/dist/decorators.js +55 -0
- package/dist/decorators.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/soft-delete-operations.d.ts +107 -0
- package/dist/soft-delete-operations.js +227 -0
- package/dist/soft-delete-operations.js.map +1 -0
- package/dist/soft-delete-paginate.d.ts +82 -0
- package/dist/soft-delete-paginate.js +119 -0
- package/dist/soft-delete-paginate.js.map +1 -0
- package/dist/soft-delete-repository.d.ts +42 -0
- package/dist/soft-delete-repository.js +76 -0
- package/dist/soft-delete-repository.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.js +53 -0
- package/dist/utils.js.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# @nest-toolbox/typeorm-soft-delete
|
|
2
|
+
|
|
3
|
+
Soft delete utilities for TypeORM with enhanced developer experience.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Function-based API** following TypeORM patterns
|
|
8
|
+
- 🔧 **Optional method-based wrapper** for enhanced DX
|
|
9
|
+
- 🔍 **Query utilities** for finding deleted records
|
|
10
|
+
- 📊 **Pagination integration** with `@nest-toolbox/typeorm-paginate`
|
|
11
|
+
- 🎨 **TypeScript-first** with full type safety
|
|
12
|
+
- ✅ **Well-tested** with 90%+ coverage
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @nest-toolbox/typeorm-soft-delete typeorm reflect-metadata
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Setup Your Entity
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';
|
|
26
|
+
import { SoftDeletable } from '@nest-toolbox/typeorm-soft-delete';
|
|
27
|
+
|
|
28
|
+
@Entity()
|
|
29
|
+
@SoftDeletable() // Optional decorator for metadata
|
|
30
|
+
export class User {
|
|
31
|
+
@PrimaryGeneratedColumn()
|
|
32
|
+
id: number;
|
|
33
|
+
|
|
34
|
+
@Column()
|
|
35
|
+
name: string;
|
|
36
|
+
|
|
37
|
+
@Column()
|
|
38
|
+
email: string;
|
|
39
|
+
|
|
40
|
+
@DeleteDateColumn() // Required - TypeORM's built-in soft delete column
|
|
41
|
+
deletedAt?: Date;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Use Soft Delete Functions (Recommended)
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { softDelete, restore, findOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';
|
|
49
|
+
import { Repository } from 'typeorm';
|
|
50
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
51
|
+
|
|
52
|
+
@Injectable()
|
|
53
|
+
export class UserService {
|
|
54
|
+
constructor(
|
|
55
|
+
@InjectRepository(User)
|
|
56
|
+
private userRepository: Repository<User>,
|
|
57
|
+
) {}
|
|
58
|
+
|
|
59
|
+
async deleteUser(id: number) {
|
|
60
|
+
// Soft delete by ID
|
|
61
|
+
const result = await softDelete(this.userRepository, id);
|
|
62
|
+
console.log(`Soft deleted ${result.affected} user(s)`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async restoreUser(id: number) {
|
|
66
|
+
// Restore soft-deleted user
|
|
67
|
+
await restore(this.userRepository, id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async getDeletedUsers() {
|
|
71
|
+
// Find only deleted users
|
|
72
|
+
return findOnlyDeleted(this.userRepository);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Alternative: Method-Based API (Optional)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { withSoftDelete } from '@nest-toolbox/typeorm-soft-delete';
|
|
81
|
+
|
|
82
|
+
@Injectable()
|
|
83
|
+
export class UserService {
|
|
84
|
+
private userRepo: SoftDeleteRepository<User>;
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
@InjectRepository(User)
|
|
88
|
+
private userRepository: Repository<User>,
|
|
89
|
+
) {
|
|
90
|
+
this.userRepo = withSoftDelete(userRepository);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async deleteUser(id: number) {
|
|
94
|
+
await this.userRepo.softDelete(id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async restoreUser(id: number) {
|
|
98
|
+
await this.userRepo.restore(id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
### Core Functions
|
|
106
|
+
|
|
107
|
+
#### `softDelete(repository, criteria, options?)`
|
|
108
|
+
|
|
109
|
+
Soft delete entities by setting `deletedAt` to current timestamp.
|
|
110
|
+
|
|
111
|
+
**Parameters:**
|
|
112
|
+
- `repository: Repository<T>` - TypeORM repository
|
|
113
|
+
- `criteria: string | number | array | FindOptionsWhere<T>` - Entity ID(s) or where clause
|
|
114
|
+
- `options?: { validateExists?: boolean }` - Optional validation
|
|
115
|
+
|
|
116
|
+
**Returns:** `Promise<{ affected: number }>`
|
|
117
|
+
|
|
118
|
+
**Examples:**
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Single ID
|
|
122
|
+
await softDelete(repo, 123);
|
|
123
|
+
|
|
124
|
+
// Multiple IDs
|
|
125
|
+
await softDelete(repo, [1, 2, 3]);
|
|
126
|
+
|
|
127
|
+
// Where clause
|
|
128
|
+
await softDelete(repo, { email: 'test@example.com' });
|
|
129
|
+
|
|
130
|
+
// With validation
|
|
131
|
+
await softDelete(repo, 123, { validateExists: true });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `restore(repository, criteria, options?)`
|
|
135
|
+
|
|
136
|
+
Restore soft-deleted entities by setting `deletedAt` back to null.
|
|
137
|
+
|
|
138
|
+
**Parameters:** Same as `softDelete()`
|
|
139
|
+
|
|
140
|
+
**Returns:** `Promise<{ affected: number }>`
|
|
141
|
+
|
|
142
|
+
**Examples:**
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Restore single user
|
|
146
|
+
await restore(userRepo, 123);
|
|
147
|
+
|
|
148
|
+
// Restore multiple users
|
|
149
|
+
await restore(userRepo, [1, 2, 3]);
|
|
150
|
+
|
|
151
|
+
// Restore with validation
|
|
152
|
+
await restore(userRepo, 123, { validateExists: true });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `forceDelete(repository, criteria)`
|
|
156
|
+
|
|
157
|
+
Permanently delete soft-deleted entities (hard delete). Only deletes records where `deletedAt IS NOT NULL`.
|
|
158
|
+
|
|
159
|
+
**Safety:** This prevents accidentally hard-deleting active records.
|
|
160
|
+
|
|
161
|
+
**Examples:**
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Permanently delete soft-deleted user
|
|
165
|
+
await forceDelete(userRepo, 123);
|
|
166
|
+
|
|
167
|
+
// Permanently delete multiple
|
|
168
|
+
await forceDelete(userRepo, [1, 2, 3]);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `findWithDeleted(repository, options?)`
|
|
172
|
+
|
|
173
|
+
Find entities including soft-deleted ones.
|
|
174
|
+
|
|
175
|
+
**Examples:**
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Find all users including deleted
|
|
179
|
+
const allUsers = await findWithDeleted(userRepo);
|
|
180
|
+
|
|
181
|
+
// Find with additional options
|
|
182
|
+
const users = await findWithDeleted(userRepo, {
|
|
183
|
+
where: { role: 'admin' },
|
|
184
|
+
take: 10,
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### `findOnlyDeleted(repository, options?)`
|
|
189
|
+
|
|
190
|
+
Find only soft-deleted entities.
|
|
191
|
+
|
|
192
|
+
**Examples:**
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Find all deleted users
|
|
196
|
+
const deletedUsers = await findOnlyDeleted(userRepo);
|
|
197
|
+
|
|
198
|
+
// Find deleted with filter
|
|
199
|
+
const deletedAdmins = await findOnlyDeleted(userRepo, {
|
|
200
|
+
where: { role: 'admin' },
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `count(repository, options?)`
|
|
205
|
+
|
|
206
|
+
Count entities with optional inclusion of soft-deleted.
|
|
207
|
+
|
|
208
|
+
**Examples:**
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Count active users
|
|
212
|
+
const activeCount = await count(userRepo);
|
|
213
|
+
|
|
214
|
+
// Count all users including deleted
|
|
215
|
+
const totalCount = await count(userRepo, { includeDeleted: true });
|
|
216
|
+
|
|
217
|
+
// Count with filter
|
|
218
|
+
const adminCount = await count(userRepo, {
|
|
219
|
+
where: { role: 'admin' },
|
|
220
|
+
includeDeleted: false,
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### `isSoftDeleted(repository, id)`
|
|
225
|
+
|
|
226
|
+
Check if entity is soft-deleted.
|
|
227
|
+
|
|
228
|
+
**Examples:**
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
if (await isSoftDeleted(userRepo, 123)) {
|
|
232
|
+
console.log('User is deleted');
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Pagination Integration
|
|
237
|
+
|
|
238
|
+
Works seamlessly with `@nest-toolbox/typeorm-paginate`:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { rowsWithDeleted, rowsOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';
|
|
242
|
+
|
|
243
|
+
// Paginate excluding deleted (default)
|
|
244
|
+
for await (const row of rowsWithDeleted({
|
|
245
|
+
repository: userRepo,
|
|
246
|
+
where: {},
|
|
247
|
+
limit: 100,
|
|
248
|
+
})) {
|
|
249
|
+
console.log(row.data, row.progress);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Paginate including deleted
|
|
253
|
+
for await (const row of rowsWithDeleted({
|
|
254
|
+
repository: userRepo,
|
|
255
|
+
where: {},
|
|
256
|
+
includeDeleted: true,
|
|
257
|
+
})) {
|
|
258
|
+
console.log(row.data);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Paginate only deleted
|
|
262
|
+
for await (const row of rowsOnlyDeleted({
|
|
263
|
+
repository: userRepo,
|
|
264
|
+
where: {},
|
|
265
|
+
})) {
|
|
266
|
+
console.log('Deleted:', row.data);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Optional Repository Wrapper
|
|
271
|
+
|
|
272
|
+
For those who prefer method-based API:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { withSoftDelete, SoftDeleteRepository } from '@nest-toolbox/typeorm-soft-delete';
|
|
276
|
+
|
|
277
|
+
const userRepo: SoftDeleteRepository<User> = withSoftDelete(userRepository);
|
|
278
|
+
|
|
279
|
+
// All soft delete methods
|
|
280
|
+
await userRepo.softDelete(123);
|
|
281
|
+
await userRepo.restore(123);
|
|
282
|
+
await userRepo.forceDelete(123);
|
|
283
|
+
const deleted = await userRepo.findOnlyDeleted();
|
|
284
|
+
const all = await userRepo.findWithDeleted();
|
|
285
|
+
const isDeleted = await userRepo.isSoftDeleted(123);
|
|
286
|
+
|
|
287
|
+
// Standard repository methods still work
|
|
288
|
+
const users = await userRepo.find({ where: { active: true } });
|
|
289
|
+
const user = await userRepo.findOne({ where: { id: 1 } });
|
|
290
|
+
await userRepo.save(newUser);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Database Setup
|
|
294
|
+
|
|
295
|
+
Ensure your database table has a `deletedAt` column:
|
|
296
|
+
|
|
297
|
+
```sql
|
|
298
|
+
CREATE TABLE users (
|
|
299
|
+
id SERIAL PRIMARY KEY,
|
|
300
|
+
name VARCHAR(255) NOT NULL,
|
|
301
|
+
email VARCHAR(255) NOT NULL,
|
|
302
|
+
deleted_at TIMESTAMP NULL, -- NULL = active, NOT NULL = deleted
|
|
303
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
304
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
-- Recommended: Index for performance
|
|
308
|
+
CREATE INDEX idx_users_deleted_at ON users(deleted_at);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
TypeORM will automatically handle the column when you use `@DeleteDateColumn()`:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
@Entity()
|
|
315
|
+
export class User {
|
|
316
|
+
@PrimaryGeneratedColumn()
|
|
317
|
+
id: number;
|
|
318
|
+
|
|
319
|
+
@Column()
|
|
320
|
+
name: string;
|
|
321
|
+
|
|
322
|
+
@DeleteDateColumn() // Creates nullable timestamp column
|
|
323
|
+
deletedAt?: Date;
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Best Practices
|
|
328
|
+
|
|
329
|
+
### 1. Always Use @DeleteDateColumn
|
|
330
|
+
|
|
331
|
+
The package requires entities to have `@DeleteDateColumn()` decorator:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
@Entity()
|
|
335
|
+
export class User {
|
|
336
|
+
@DeleteDateColumn()
|
|
337
|
+
deletedAt?: Date; // Required for soft delete functionality
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 2. Add Database Index
|
|
342
|
+
|
|
343
|
+
Index the `deletedAt` column for better query performance:
|
|
344
|
+
|
|
345
|
+
```sql
|
|
346
|
+
CREATE INDEX idx_table_deleted_at ON your_table(deleted_at);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 3. Use forceDelete() Carefully
|
|
350
|
+
|
|
351
|
+
Permanent deletion cannot be undone. The package only allows force deletion of already soft-deleted records as a safety measure:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Safe: Only permanently deletes records that are already soft-deleted
|
|
355
|
+
await forceDelete(repo, id);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 4. Consider Cascading
|
|
359
|
+
|
|
360
|
+
Soft delete related entities to maintain referential integrity:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
async deleteUserWithPosts(userId: number) {
|
|
364
|
+
// Soft delete user
|
|
365
|
+
await softDelete(userRepo, userId);
|
|
366
|
+
|
|
367
|
+
// Soft delete user's posts
|
|
368
|
+
await softDelete(postRepo, { userId });
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 5. Regular Cleanup
|
|
373
|
+
|
|
374
|
+
Schedule jobs to permanently delete old soft-deleted records:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
@Cron('0 0 * * *') // Daily at midnight
|
|
378
|
+
async cleanupOldDeleted() {
|
|
379
|
+
const thirtyDaysAgo = new Date();
|
|
380
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
381
|
+
|
|
382
|
+
const oldDeleted = await findOnlyDeleted(userRepo, {
|
|
383
|
+
where: { deletedAt: LessThan(thirtyDaysAgo) },
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
for (const user of oldDeleted) {
|
|
387
|
+
await forceDelete(userRepo, user.id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Migration from TypeORM Built-in
|
|
393
|
+
|
|
394
|
+
TypeORM has basic soft delete support via `repository.softDelete()`. This package provides enhanced functionality:
|
|
395
|
+
|
|
396
|
+
**TypeORM Built-in:**
|
|
397
|
+
- ✅ Basic soft delete with `softDelete(id)`
|
|
398
|
+
- ❌ No restore functionality
|
|
399
|
+
- ❌ No query utilities for deleted records
|
|
400
|
+
- ❌ No pagination integration
|
|
401
|
+
- ❌ Limited type safety
|
|
402
|
+
|
|
403
|
+
**This Package:**
|
|
404
|
+
- ✅ Enhanced soft delete operations
|
|
405
|
+
- ✅ Restore functionality
|
|
406
|
+
- ✅ Query utilities (`findOnlyDeleted`, `findWithDeleted`)
|
|
407
|
+
- ✅ Pagination integration
|
|
408
|
+
- ✅ Better type safety and validation
|
|
409
|
+
- ✅ Safety checks (forceDelete only deletes soft-deleted records)
|
|
410
|
+
|
|
411
|
+
**Before:**
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
await repository.softDelete(id);
|
|
415
|
+
const deleted = await repository.find({ withDeleted: true });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**After:**
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
await softDelete(repository, id);
|
|
422
|
+
const deleted = await findOnlyDeleted(repository);
|
|
423
|
+
|
|
424
|
+
// Or restore them
|
|
425
|
+
await restore(repository, id);
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Advanced Usage
|
|
429
|
+
|
|
430
|
+
### Batch Operations
|
|
431
|
+
|
|
432
|
+
Process large numbers of records efficiently:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
const userIds = [1, 2, 3, 4, 5, /* ... hundreds more */];
|
|
436
|
+
|
|
437
|
+
// Soft delete in bulk
|
|
438
|
+
const result = await softDelete(userRepo, userIds);
|
|
439
|
+
console.log(`Deleted ${result.affected} users`);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Conditional Soft Delete
|
|
443
|
+
|
|
444
|
+
Use `FindOptionsWhere` for complex conditions:
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Soft delete all inactive users
|
|
448
|
+
await softDelete(userRepo, { active: false });
|
|
449
|
+
|
|
450
|
+
// Soft delete users by email domain
|
|
451
|
+
await softDelete(userRepo, { email: Like('%@old-domain.com') });
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Validation
|
|
455
|
+
|
|
456
|
+
Ensure entities exist before deletion:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
try {
|
|
460
|
+
await softDelete(userRepo, 999, { validateExists: true });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error('User not found or already deleted');
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## TypeScript Support
|
|
467
|
+
|
|
468
|
+
Full TypeScript support with strict typing:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import type {
|
|
472
|
+
SoftDeleteCriteria,
|
|
473
|
+
SoftDeleteResult,
|
|
474
|
+
SoftDeleteOptions,
|
|
475
|
+
RestoreOptions,
|
|
476
|
+
CountOptions,
|
|
477
|
+
PaginatedRow,
|
|
478
|
+
} from '@nest-toolbox/typeorm-soft-delete';
|
|
479
|
+
|
|
480
|
+
// Type-safe operations
|
|
481
|
+
const result: SoftDeleteResult = await softDelete(repo, 1);
|
|
482
|
+
console.log(`Affected: ${result.affected}`);
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Error Handling
|
|
486
|
+
|
|
487
|
+
The package provides clear error messages:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// Missing @DeleteDateColumn
|
|
491
|
+
await softDelete(repoWithoutColumn, 1);
|
|
492
|
+
// Error: Entity User does not have @DeleteDateColumn.
|
|
493
|
+
// Soft delete requires a column decorated with @DeleteDateColumn.
|
|
494
|
+
|
|
495
|
+
// Entity not found (with validation)
|
|
496
|
+
await restore(repo, 999, { validateExists: true });
|
|
497
|
+
// Error: Entity not found or not soft-deleted
|
|
498
|
+
|
|
499
|
+
// Check before operations
|
|
500
|
+
if (!supportsSoftDelete(repo)) {
|
|
501
|
+
console.error('Repository does not support soft delete');
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Testing
|
|
506
|
+
|
|
507
|
+
Mock repositories in your tests:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import { vi } from 'vitest';
|
|
511
|
+
|
|
512
|
+
const mockRepo = {
|
|
513
|
+
metadata: {
|
|
514
|
+
deleteDateColumn: { databaseName: 'deletedAt' },
|
|
515
|
+
primaryColumns: [{ propertyName: 'id' }],
|
|
516
|
+
},
|
|
517
|
+
createQueryBuilder: vi.fn().mockReturnValue({
|
|
518
|
+
update: vi.fn().mockReturnThis(),
|
|
519
|
+
set: vi.fn().mockReturnThis(),
|
|
520
|
+
where: vi.fn().mockReturnThis(),
|
|
521
|
+
andWhere: vi.fn().mockReturnThis(),
|
|
522
|
+
execute: vi.fn().mockResolvedValue({ affected: 1 }),
|
|
523
|
+
}),
|
|
524
|
+
} as any;
|
|
525
|
+
|
|
526
|
+
const result = await softDelete(mockRepo, 1);
|
|
527
|
+
expect(result.affected).toBe(1);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Contributing
|
|
531
|
+
|
|
532
|
+
Contributions are welcome! Please check out the [main repository](https://github.com/lupu60/nestjs-toolbox) for contributing guidelines.
|
|
533
|
+
|
|
534
|
+
## License
|
|
535
|
+
|
|
536
|
+
MIT
|
|
537
|
+
|
|
538
|
+
## Support
|
|
539
|
+
|
|
540
|
+
For issues and questions, please visit the [GitHub Issues](https://github.com/lupu60/nestjs-toolbox/issues) page.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for soft delete behavior
|
|
4
|
+
*/
|
|
5
|
+
export interface SoftDeleteConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Name of the delete date column
|
|
8
|
+
* @default 'deletedAt'
|
|
9
|
+
*/
|
|
10
|
+
columnName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether to allow hard deletion of records
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
allowHardDelete?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Marks entity as soft-deletable
|
|
19
|
+
* Stores metadata for validation and auto-configuration
|
|
20
|
+
*
|
|
21
|
+
* @param config - Optional configuration for soft delete behavior
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* @Entity()
|
|
26
|
+
* @SoftDeletable({ columnName: 'deletedAt' })
|
|
27
|
+
* class User {
|
|
28
|
+
* @DeleteDateColumn()
|
|
29
|
+
* deletedAt?: Date;
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function SoftDeletable(config?: SoftDeleteConfig): ClassDecorator;
|
|
34
|
+
/**
|
|
35
|
+
* Get soft delete configuration from entity
|
|
36
|
+
*
|
|
37
|
+
* @param entity - Entity class or constructor function
|
|
38
|
+
* @returns Configuration object if entity is marked as soft-deletable, undefined otherwise
|
|
39
|
+
*/
|
|
40
|
+
export declare function getSoftDeleteConfig(entity: Function): SoftDeleteConfig | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Check if entity is marked as soft-deletable
|
|
43
|
+
*
|
|
44
|
+
* @param entity - Entity class or constructor function
|
|
45
|
+
* @returns true if entity has @SoftDeletable decorator, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
export declare function isSoftDeletable(entity: Function): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SoftDeletable = SoftDeletable;
|
|
4
|
+
exports.getSoftDeleteConfig = getSoftDeleteConfig;
|
|
5
|
+
exports.isSoftDeletable = isSoftDeletable;
|
|
6
|
+
require("reflect-metadata");
|
|
7
|
+
const SOFT_DELETABLE_KEY = Symbol('softDeletable');
|
|
8
|
+
/**
|
|
9
|
+
* Marks entity as soft-deletable
|
|
10
|
+
* Stores metadata for validation and auto-configuration
|
|
11
|
+
*
|
|
12
|
+
* @param config - Optional configuration for soft delete behavior
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* @Entity()
|
|
17
|
+
* @SoftDeletable({ columnName: 'deletedAt' })
|
|
18
|
+
* class User {
|
|
19
|
+
* @DeleteDateColumn()
|
|
20
|
+
* deletedAt?: Date;
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function SoftDeletable(config) {
|
|
25
|
+
// biome-ignore lint/complexity/noBannedTypes: ClassDecorator requires Function type
|
|
26
|
+
return (target) => {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
const finalConfig = {
|
|
29
|
+
columnName: (_a = config === null || config === void 0 ? void 0 : config.columnName) !== null && _a !== void 0 ? _a : 'deletedAt',
|
|
30
|
+
allowHardDelete: (_b = config === null || config === void 0 ? void 0 : config.allowHardDelete) !== null && _b !== void 0 ? _b : false,
|
|
31
|
+
};
|
|
32
|
+
Reflect.defineMetadata(SOFT_DELETABLE_KEY, finalConfig, target);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get soft delete configuration from entity
|
|
37
|
+
*
|
|
38
|
+
* @param entity - Entity class or constructor function
|
|
39
|
+
* @returns Configuration object if entity is marked as soft-deletable, undefined otherwise
|
|
40
|
+
*/
|
|
41
|
+
// biome-ignore lint/complexity/noBannedTypes: Reflect.getMetadata requires Function type
|
|
42
|
+
function getSoftDeleteConfig(entity) {
|
|
43
|
+
return Reflect.getMetadata(SOFT_DELETABLE_KEY, entity);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if entity is marked as soft-deletable
|
|
47
|
+
*
|
|
48
|
+
* @param entity - Entity class or constructor function
|
|
49
|
+
* @returns true if entity has @SoftDeletable decorator, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
// biome-ignore lint/complexity/noBannedTypes: Reflect.hasMetadata requires Function type
|
|
52
|
+
function isSoftDeletable(entity) {
|
|
53
|
+
return Reflect.hasMetadata(SOFT_DELETABLE_KEY, entity);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=decorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":";;AAqCA,sCASC;AASD,kDAEC;AASD,0CAEC;AApED,4BAA0B;AAE1B,MAAM,kBAAkB,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;AAmBnD;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,aAAa,CAAC,MAAyB;IACrD,oFAAoF;IACpF,OAAO,CAAC,MAAgB,EAAE,EAAE;;QAC1B,MAAM,WAAW,GAA+B;YAC9C,UAAU,EAAE,MAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,UAAU,mCAAI,WAAW;YAC7C,eAAe,EAAE,MAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,eAAe,mCAAI,KAAK;SAClD,CAAC;QACF,OAAO,CAAC,cAAc,CAAC,kBAAkB,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,yFAAyF;AACzF,SAAgB,mBAAmB,CAAC,MAAgB;IAClD,OAAO,OAAO,CAAC,WAAW,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,yFAAyF;AACzF,SAAgB,eAAe,CAAC,MAAgB;IAC9C,OAAO,OAAO,CAAC,WAAW,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;AACzD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { getSoftDeleteConfig, isSoftDeletable, SoftDeletable, type SoftDeleteConfig } from './decorators';
|
|
2
|
+
export { count, findOnlyDeleted, findWithDeleted, forceDelete, isSoftDeleted, restore, softDelete, } from './soft-delete-operations';
|
|
3
|
+
export { type PaginatedRow, type PaginationWithDeletedOptions, rowsOnlyDeleted, rowsWithDeleted, } from './soft-delete-paginate';
|
|
4
|
+
export { SoftDeleteRepository, withSoftDelete } from './soft-delete-repository';
|
|
5
|
+
export type { CountOptions, FindDeletedOptions, RestoreOptions, SoftDeleteCriteria, SoftDeleteOptions, SoftDeleteResult, } from './types';
|
|
6
|
+
export { getDeleteDateColumnName, supportsSoftDelete, validateSoftDeleteSupport } from './utils';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Core functions (Primary API)
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.validateSoftDeleteSupport = exports.supportsSoftDelete = exports.getDeleteDateColumnName = exports.withSoftDelete = exports.SoftDeleteRepository = exports.rowsWithDeleted = exports.rowsOnlyDeleted = exports.softDelete = exports.restore = exports.isSoftDeleted = exports.forceDelete = exports.findWithDeleted = exports.findOnlyDeleted = exports.count = exports.SoftDeletable = exports.isSoftDeletable = exports.getSoftDeleteConfig = void 0;
|
|
5
|
+
// Decorators
|
|
6
|
+
var decorators_1 = require("./decorators");
|
|
7
|
+
Object.defineProperty(exports, "getSoftDeleteConfig", { enumerable: true, get: function () { return decorators_1.getSoftDeleteConfig; } });
|
|
8
|
+
Object.defineProperty(exports, "isSoftDeletable", { enumerable: true, get: function () { return decorators_1.isSoftDeletable; } });
|
|
9
|
+
Object.defineProperty(exports, "SoftDeletable", { enumerable: true, get: function () { return decorators_1.SoftDeletable; } });
|
|
10
|
+
var soft_delete_operations_1 = require("./soft-delete-operations");
|
|
11
|
+
Object.defineProperty(exports, "count", { enumerable: true, get: function () { return soft_delete_operations_1.count; } });
|
|
12
|
+
Object.defineProperty(exports, "findOnlyDeleted", { enumerable: true, get: function () { return soft_delete_operations_1.findOnlyDeleted; } });
|
|
13
|
+
Object.defineProperty(exports, "findWithDeleted", { enumerable: true, get: function () { return soft_delete_operations_1.findWithDeleted; } });
|
|
14
|
+
Object.defineProperty(exports, "forceDelete", { enumerable: true, get: function () { return soft_delete_operations_1.forceDelete; } });
|
|
15
|
+
Object.defineProperty(exports, "isSoftDeleted", { enumerable: true, get: function () { return soft_delete_operations_1.isSoftDeleted; } });
|
|
16
|
+
Object.defineProperty(exports, "restore", { enumerable: true, get: function () { return soft_delete_operations_1.restore; } });
|
|
17
|
+
Object.defineProperty(exports, "softDelete", { enumerable: true, get: function () { return soft_delete_operations_1.softDelete; } });
|
|
18
|
+
// Pagination integration
|
|
19
|
+
var soft_delete_paginate_1 = require("./soft-delete-paginate");
|
|
20
|
+
Object.defineProperty(exports, "rowsOnlyDeleted", { enumerable: true, get: function () { return soft_delete_paginate_1.rowsOnlyDeleted; } });
|
|
21
|
+
Object.defineProperty(exports, "rowsWithDeleted", { enumerable: true, get: function () { return soft_delete_paginate_1.rowsWithDeleted; } });
|
|
22
|
+
// Optional wrapper (for method-based API)
|
|
23
|
+
var soft_delete_repository_1 = require("./soft-delete-repository");
|
|
24
|
+
Object.defineProperty(exports, "SoftDeleteRepository", { enumerable: true, get: function () { return soft_delete_repository_1.SoftDeleteRepository; } });
|
|
25
|
+
Object.defineProperty(exports, "withSoftDelete", { enumerable: true, get: function () { return soft_delete_repository_1.withSoftDelete; } });
|
|
26
|
+
// Utilities
|
|
27
|
+
var utils_1 = require("./utils");
|
|
28
|
+
Object.defineProperty(exports, "getDeleteDateColumnName", { enumerable: true, get: function () { return utils_1.getDeleteDateColumnName; } });
|
|
29
|
+
Object.defineProperty(exports, "supportsSoftDelete", { enumerable: true, get: function () { return utils_1.supportsSoftDelete; } });
|
|
30
|
+
Object.defineProperty(exports, "validateSoftDeleteSupport", { enumerable: true, get: function () { return utils_1.validateSoftDeleteSupport; } });
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,+BAA+B;;;AAE/B,aAAa;AACb,2CAA0G;AAAjG,iHAAA,mBAAmB,OAAA;AAAE,6GAAA,eAAe,OAAA;AAAE,2GAAA,aAAa,OAAA;AAC5D,mEAQkC;AAPhC,+GAAA,KAAK,OAAA;AACL,yHAAA,eAAe,OAAA;AACf,yHAAA,eAAe,OAAA;AACf,qHAAA,WAAW,OAAA;AACX,uHAAA,aAAa,OAAA;AACb,iHAAA,OAAO,OAAA;AACP,oHAAA,UAAU,OAAA;AAEZ,yBAAyB;AACzB,+DAKgC;AAF9B,uHAAA,eAAe,OAAA;AACf,uHAAA,eAAe,OAAA;AAEjB,0CAA0C;AAC1C,mEAAgF;AAAvE,8HAAA,oBAAoB,OAAA;AAAE,wHAAA,cAAc,OAAA;AAU7C,YAAY;AACZ,iCAAiG;AAAxF,gHAAA,uBAAuB,OAAA;AAAE,2GAAA,kBAAkB,OAAA;AAAE,kHAAA,yBAAyB,OAAA"}
|