prisma-safe-delete 0.1.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +436 -0
  4. package/dist/bin.d.ts +9 -0
  5. package/dist/bin.d.ts.map +1 -0
  6. package/dist/bin.js +9 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/cascade-graph.d.ts +54 -0
  9. package/dist/cascade-graph.d.ts.map +1 -0
  10. package/dist/cascade-graph.js +123 -0
  11. package/dist/cascade-graph.js.map +1 -0
  12. package/dist/codegen/emit-cascade-graph.d.ts +6 -0
  13. package/dist/codegen/emit-cascade-graph.d.ts.map +1 -0
  14. package/dist/codegen/emit-cascade-graph.js +56 -0
  15. package/dist/codegen/emit-cascade-graph.js.map +1 -0
  16. package/dist/codegen/emit-index.d.ts +6 -0
  17. package/dist/codegen/emit-index.d.ts.map +1 -0
  18. package/dist/codegen/emit-index.js +26 -0
  19. package/dist/codegen/emit-index.js.map +1 -0
  20. package/dist/codegen/emit-runtime.d.ts +8 -0
  21. package/dist/codegen/emit-runtime.d.ts.map +1 -0
  22. package/dist/codegen/emit-runtime.js +873 -0
  23. package/dist/codegen/emit-runtime.js.map +1 -0
  24. package/dist/codegen/emit-types.d.ts +8 -0
  25. package/dist/codegen/emit-types.d.ts.map +1 -0
  26. package/dist/codegen/emit-types.js +109 -0
  27. package/dist/codegen/emit-types.js.map +1 -0
  28. package/dist/codegen/index.d.ts +5 -0
  29. package/dist/codegen/index.d.ts.map +1 -0
  30. package/dist/codegen/index.js +5 -0
  31. package/dist/codegen/index.js.map +1 -0
  32. package/dist/dmmf-parser.d.ts +62 -0
  33. package/dist/dmmf-parser.d.ts.map +1 -0
  34. package/dist/dmmf-parser.js +171 -0
  35. package/dist/dmmf-parser.js.map +1 -0
  36. package/dist/generator.d.ts +2 -0
  37. package/dist/generator.d.ts.map +1 -0
  38. package/dist/generator.js +71 -0
  39. package/dist/generator.js.map +1 -0
  40. package/package.json +80 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-02-07
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Type-safe soft deletion wrapper for Prisma 7
15
+ - Automatic filter injection on all read operations
16
+ - Deep relation filtering for `include`, `select`, `_count`, and relation filters (`some`/`every`/`none`)
17
+ - Cascade soft-delete following `onDelete: Cascade` relations
18
+ - Unique string field mangling to free values for reuse
19
+ - Transaction support with wrapped clients
20
+ - Compound primary key support
21
+ - Escape hatches: `$prisma`, `$includingDeleted`, `$onlyDeleted`
22
+ - `hardDelete` and `hardDeleteMany` methods
23
+ - `softDelete` and `softDeleteMany` methods
24
+ - Support for both `deleted_at` and `deletedAt` field names
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 EddieRydell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,436 @@
1
+ # prisma-safe-delete
2
+
3
+ [![npm version](https://img.shields.io/npm/v/prisma-safe-delete.svg)](https://www.npmjs.com/package/prisma-safe-delete)
4
+ [![npm downloads](https://img.shields.io/npm/dm/prisma-safe-delete.svg)](https://www.npmjs.com/package/prisma-safe-delete)
5
+ [![CI](https://github.com/EddieRydell/prisma-safe-delete/actions/workflows/ci.yml/badge.svg)](https://github.com/EddieRydell/prisma-safe-delete/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ A Prisma generator that creates a type-safe wrapper for soft deletion with automatic cascade support. Designed to be a drop-in replacement that you configure once and never think about again.
10
+
11
+ ## Why This Library?
12
+
13
+ Soft deletion is a common pattern where records are marked as deleted (typically with a timestamp) rather than being permanently removed. This preserves data for auditing, recovery, and maintaining referential integrity.
14
+
15
+ **The problem:** Implementing soft deletion correctly is tedious and error-prone. You need to remember to filter out deleted records in every query, handle cascading deletes manually, and deal with unique constraint conflicts when "deleted" records still occupy unique values.
16
+
17
+ **prisma-safe-delete solves this by:**
18
+ - Automatically filtering deleted records from all read operations
19
+ - Cascading soft-deletes through your relation tree (following `onDelete: Cascade`)
20
+ - Mangling unique string fields to free them for reuse
21
+ - Providing escape hatches when you need to access deleted data
22
+
23
+ ## Features
24
+
25
+ - **Automatic filter injection**: All read operations automatically exclude soft-deleted records
26
+ - **Deep relation filtering**: Filters applied to `include`, `select`, `_count`, and relation filters (`some`/`every`/`none`)
27
+ - **Cascade soft-delete**: Automatically cascades based on `onDelete: Cascade` relations
28
+ - **Unique constraint handling**: Automatically mangles unique string fields to free up values for reuse
29
+ - **Transaction support**: Interactive transactions receive wrapped clients with filtering
30
+ - **Compound key support**: Full support for compound primary keys and foreign keys
31
+ - **Escape hatches**: Access raw client, query deleted records, or hard delete when needed
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install prisma-safe-delete
37
+ # or
38
+ pnpm add prisma-safe-delete
39
+ # or
40
+ yarn add prisma-safe-delete
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### 1. Add the generator to your Prisma schema
46
+
47
+ ```prisma
48
+ generator client {
49
+ provider = "prisma-client"
50
+ output = "./generated/client"
51
+ }
52
+
53
+ generator softDelete {
54
+ provider = "prisma-safe-delete"
55
+ output = "./generated/soft-delete"
56
+ }
57
+
58
+ datasource db {
59
+ provider = "postgresql"
60
+ }
61
+ ```
62
+
63
+ ### 2. Add `deleted_at` to soft-deletable models
64
+
65
+ ```prisma
66
+ model User {
67
+ id String @id @default(cuid())
68
+ email String @unique
69
+ name String?
70
+ posts Post[]
71
+ deleted_at DateTime? // Makes this model soft-deletable
72
+ }
73
+
74
+ model Post {
75
+ id String @id @default(cuid())
76
+ title String
77
+ authorId String
78
+ author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
79
+ comments Comment[]
80
+ deleted_at DateTime?
81
+ }
82
+
83
+ model Comment {
84
+ id String @id @default(cuid())
85
+ content String
86
+ postId String
87
+ post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
88
+ deleted_at DateTime?
89
+ }
90
+ ```
91
+
92
+ ### 3. Generate and use
93
+
94
+ ```bash
95
+ npx prisma generate
96
+ ```
97
+
98
+ ```typescript
99
+ import { PrismaClient } from './generated/client';
100
+ import { PrismaPg } from '@prisma/adapter-pg';
101
+ import { Pool } from 'pg';
102
+ import { wrapPrismaClient } from './generated/soft-delete';
103
+
104
+ // Prisma 7 requires an adapter
105
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
106
+ const adapter = new PrismaPg(pool);
107
+
108
+ const prisma = new PrismaClient({ adapter });
109
+ const safePrisma = wrapPrismaClient(prisma);
110
+
111
+ // All queries automatically filter out soft-deleted records
112
+ const users = await safePrisma.user.findMany();
113
+
114
+ // Soft delete with automatic cascade
115
+ await safePrisma.user.softDelete({ where: { id: 'user-1' } });
116
+ // ^ This soft-deletes the user AND all their posts AND all comments on those posts
117
+ ```
118
+
119
+ ## API Reference
120
+
121
+ ### Read Operations (Auto-filtered)
122
+
123
+ All read operations automatically inject `deleted_at: null` filters:
124
+
125
+ ```typescript
126
+ // These all exclude soft-deleted records automatically
127
+ await safePrisma.user.findMany();
128
+ await safePrisma.user.findFirst({ where: { name: 'John' } });
129
+ await safePrisma.user.findUnique({ where: { id: 'user-1' } });
130
+ await safePrisma.user.findFirstOrThrow({ where: { email: 'john@example.com' } });
131
+ await safePrisma.user.findUniqueOrThrow({ where: { id: 'user-1' } });
132
+ await safePrisma.user.count();
133
+ await safePrisma.user.aggregate({ _count: true });
134
+ await safePrisma.user.groupBy({ by: ['name'], _count: true });
135
+ ```
136
+
137
+ ### Relation Queries (Auto-filtered)
138
+
139
+ Filters are automatically injected into relation queries:
140
+
141
+ ```typescript
142
+ // Posts in include are filtered
143
+ const user = await safePrisma.user.findUnique({
144
+ where: { id: 'user-1' },
145
+ include: { posts: true } // Only returns non-deleted posts
146
+ });
147
+
148
+ // Nested relations are filtered too
149
+ const user = await safePrisma.user.findUnique({
150
+ where: { id: 'user-1' },
151
+ include: {
152
+ posts: {
153
+ include: { comments: true } // Only non-deleted comments
154
+ }
155
+ }
156
+ });
157
+
158
+ // select works the same way
159
+ const user = await safePrisma.user.findUnique({
160
+ where: { id: 'user-1' },
161
+ select: {
162
+ email: true,
163
+ posts: { select: { title: true } } // Only non-deleted posts
164
+ }
165
+ });
166
+
167
+ // _count is filtered
168
+ const user = await safePrisma.user.findUnique({
169
+ where: { id: 'user-1' },
170
+ include: {
171
+ _count: { select: { posts: true } } // Counts only non-deleted posts
172
+ }
173
+ });
174
+ ```
175
+
176
+ ### Relation Filters (Auto-filtered)
177
+
178
+ The `some`, `every`, and `none` relation filters exclude soft-deleted records:
179
+
180
+ ```typescript
181
+ // Find users who have at least one active post
182
+ const users = await safePrisma.user.findMany({
183
+ where: {
184
+ posts: { some: { title: { contains: 'hello' } } } // Ignores deleted posts
185
+ }
186
+ });
187
+
188
+ // Find users where all their posts are published
189
+ const users = await safePrisma.user.findMany({
190
+ where: {
191
+ posts: { every: { published: true } } // Only considers non-deleted posts
192
+ }
193
+ });
194
+ ```
195
+
196
+ ### Write Operations
197
+
198
+ ```typescript
199
+ // Standard write operations pass through unchanged
200
+ await safePrisma.user.create({ data: { email: 'new@example.com' } });
201
+ await safePrisma.user.createMany({ data: [...] });
202
+ await safePrisma.user.update({ where: { id: 'user-1' }, data: { name: 'Jane' } });
203
+ await safePrisma.user.updateMany({ where: { ... }, data: { ... } });
204
+ await safePrisma.user.upsert({ where: { ... }, create: { ... }, update: { ... } });
205
+ ```
206
+
207
+ ### Soft Delete
208
+
209
+ ```typescript
210
+ // Soft delete a single record (with cascade)
211
+ await safePrisma.user.softDelete({ where: { id: 'user-1' } });
212
+
213
+ // Soft delete multiple records (with cascade)
214
+ const result = await safePrisma.user.softDeleteMany({ where: { name: 'Test' } });
215
+ console.log(result.count); // Number of records soft-deleted
216
+ ```
217
+
218
+ ### Hard Delete (Escape Hatch)
219
+
220
+ ```typescript
221
+ // Permanently delete when needed
222
+ await safePrisma.user.hardDelete({ where: { id: 'user-1' } });
223
+ await safePrisma.user.hardDeleteMany({ where: { createdAt: { lt: oldDate } } });
224
+ ```
225
+
226
+ ### Escape Hatches
227
+
228
+ ```typescript
229
+ // Access the raw Prisma client (no filtering)
230
+ const allUsers = await safePrisma.$prisma.user.findMany();
231
+
232
+ // Query including soft-deleted records
233
+ const allUsers = await safePrisma.$includingDeleted.user.findMany();
234
+
235
+ // Query only soft-deleted records
236
+ const deletedUsers = await safePrisma.$onlyDeleted.user.findMany();
237
+ ```
238
+
239
+ ### Transactions
240
+
241
+ Interactive transactions receive wrapped clients with filtering:
242
+
243
+ ```typescript
244
+ const result = await safePrisma.$transaction(async (tx) => {
245
+ // tx has the same filtering as safePrisma
246
+ const users = await tx.user.findMany(); // Excludes deleted
247
+ const posts = await tx.post.findMany(); // Excludes deleted
248
+ return { users, posts };
249
+ });
250
+ ```
251
+
252
+ ## Cascade Behavior
253
+
254
+ Soft-delete cascades follow `onDelete: Cascade` relations defined in your schema:
255
+
256
+ ```prisma
257
+ model User {
258
+ id String @id
259
+ posts Post[]
260
+ profile Profile?
261
+ deleted_at DateTime?
262
+ }
263
+
264
+ model Post {
265
+ id String @id
266
+ author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
267
+ authorId String
268
+ comments Comment[]
269
+ deleted_at DateTime?
270
+ }
271
+
272
+ model Comment {
273
+ id String @id
274
+ post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
275
+ postId String
276
+ deleted_at DateTime?
277
+ }
278
+ ```
279
+
280
+ ```typescript
281
+ await safePrisma.user.softDelete({ where: { id: 'user-1' } });
282
+ // Soft-deletes:
283
+ // 1. The user
284
+ // 2. All their posts
285
+ // 3. All comments on those posts
286
+ // All with the same timestamp (transactional)
287
+ ```
288
+
289
+ ### Cascade Rules
290
+
291
+ - Only follows `onDelete: Cascade` relations
292
+ - Only soft-deletes children that have a `deleted_at` field
293
+ - Children without `deleted_at` are left unchanged
294
+ - All cascaded records get the same `deleted_at` timestamp
295
+ - Entire operation is transactional (all-or-nothing)
296
+
297
+ ## Unique Constraint Handling
298
+
299
+ When you soft-delete a record with unique string fields, the values are automatically mangled to free them up for reuse:
300
+
301
+ ```typescript
302
+ // Before soft delete
303
+ { id: 'user-1', email: 'john@example.com', deleted_at: null }
304
+
305
+ // After soft delete
306
+ { id: 'user-1', email: 'john@example.com__deleted_user-1', deleted_at: '2024-...' }
307
+
308
+ // Now you can create a new user with the same email
309
+ await safePrisma.user.create({ data: { email: 'john@example.com' } }); // Works!
310
+ ```
311
+
312
+ ### Mangling Rules
313
+
314
+ - Only **string** fields with `@unique` or `@@unique` are mangled
315
+ - Suffix format: `__deleted_{primaryKey}`
316
+ - For compound PKs: `__deleted_{pk1}_{pk2}` (sorted alphabetically)
317
+ - NULL values are not mangled (already allow duplicates)
318
+ - Mangling is idempotent (won't double-mangle)
319
+ - Fails with clear error if mangled value would exceed max string length
320
+
321
+ ### Non-String Unique Fields
322
+
323
+ For non-string unique fields (Int, UUID, etc.), use a **partial unique index** in your database:
324
+
325
+ ```sql
326
+ -- PostgreSQL
327
+ CREATE UNIQUE INDEX user_employee_id_active ON "User"(employee_id) WHERE deleted_at IS NULL;
328
+
329
+ -- MySQL (8.0+)
330
+ -- Use a generated column + unique index
331
+
332
+ -- SQLite
333
+ CREATE UNIQUE INDEX user_employee_id_active ON User(employee_id) WHERE deleted_at IS NULL;
334
+ ```
335
+
336
+ ## Compound Primary Keys
337
+
338
+ Full support for compound primary keys:
339
+
340
+ ```prisma
341
+ model TenantUser {
342
+ tenantId String
343
+ userId String
344
+ email String
345
+ deleted_at DateTime?
346
+
347
+ @@id([tenantId, userId])
348
+ }
349
+ ```
350
+
351
+ ```typescript
352
+ await safePrisma.tenantUser.softDelete({
353
+ where: {
354
+ tenantId_userId: { tenantId: 'tenant-1', userId: 'user-1' }
355
+ }
356
+ });
357
+ ```
358
+
359
+ ## Soft Delete Detection
360
+
361
+ Models are automatically detected as soft-deletable if they have a nullable DateTime field named:
362
+ - `deleted_at` (snake_case)
363
+ - `deletedAt` (camelCase)
364
+
365
+ ```prisma
366
+ // Both of these work:
367
+ model User {
368
+ deleted_at DateTime? // snake_case
369
+ }
370
+
371
+ model Post {
372
+ deletedAt DateTime? // camelCase
373
+ }
374
+ ```
375
+
376
+ ## Known Limitations
377
+
378
+ ### Fluent API
379
+
380
+ The Prisma fluent API bypasses the wrapper. Use `include` instead:
381
+
382
+ ```typescript
383
+ // ❌ Does NOT filter deleted posts
384
+ const posts = await safePrisma.user.findUnique({ where: { id: '1' } }).posts();
385
+
386
+ // ✅ Correctly filters deleted posts
387
+ const user = await safePrisma.user.findUnique({
388
+ where: { id: '1' },
389
+ include: { posts: true }
390
+ });
391
+ const posts = user.posts;
392
+ ```
393
+
394
+ ### Raw Queries
395
+
396
+ Raw queries bypass the wrapper entirely (by design):
397
+
398
+ ```typescript
399
+ // No filtering applied - returns all records including deleted
400
+ const users = await safePrisma.$queryRaw`SELECT * FROM User`;
401
+ ```
402
+
403
+ ### Update/Upsert on Soft-Deleted Records
404
+
405
+ `update` and `upsert` can still modify soft-deleted records. This is intentional to allow restoration workflows:
406
+
407
+ ```typescript
408
+ // This works even if the user is soft-deleted
409
+ await safePrisma.user.update({
410
+ where: { id: 'deleted-user' },
411
+ data: { deleted_at: null } // Restore the user
412
+ });
413
+ ```
414
+
415
+ ## Requirements
416
+
417
+ - Node.js >= 18
418
+ - Prisma >= 7.0.0
419
+ - TypeScript >= 5.0 (recommended)
420
+
421
+ ## Development
422
+
423
+ ```bash
424
+ # Start Postgres
425
+ docker compose up -d
426
+
427
+ # Run tests
428
+ pnpm test
429
+
430
+ # Stop Postgres
431
+ docker compose down
432
+ ```
433
+
434
+ ## License
435
+
436
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for prisma-safe-delete generator
4
+ *
5
+ * This file is invoked by Prisma when running `prisma generate`.
6
+ * It simply re-exports the generator module.
7
+ */
8
+ import './generator.js';
9
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,gBAAgB,CAAC"}
package/dist/bin.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for prisma-safe-delete generator
4
+ *
5
+ * This file is invoked by Prisma when running `prisma generate`.
6
+ * It simply re-exports the generator module.
7
+ */
8
+ import './generator.js';
9
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,gBAAgB,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { ParsedModel, ParsedSchema } from './dmmf-parser.js';
2
+ /**
3
+ * Represents a child model in a cascade relationship
4
+ */
5
+ export interface CascadeChild {
6
+ /** The name of the child model */
7
+ model: string;
8
+ /** The foreign key field(s) on the child model */
9
+ foreignKey: string[];
10
+ /** The primary key field(s) on the parent model that the FK references */
11
+ parentKey: string[];
12
+ /** Whether the child model supports soft deletion */
13
+ isSoftDeletable: boolean;
14
+ /** The name of the deleted_at field if soft-deletable */
15
+ deletedAtField: string | null;
16
+ }
17
+ /**
18
+ * Maps parent model names to their cascade children
19
+ * When a parent is soft-deleted, all children in this map should be cascaded
20
+ */
21
+ export type CascadeGraph = Record<string, CascadeChild[]>;
22
+ /**
23
+ * Builds a cascade graph from a parsed schema
24
+ *
25
+ * The graph maps parent model names to lists of child models that should be
26
+ * soft-deleted when the parent is soft-deleted. Only relations with
27
+ * onDelete: Cascade are included.
28
+ *
29
+ * @param schema - The parsed Prisma schema
30
+ * @returns A cascade graph mapping parents to children
31
+ */
32
+ export declare function buildCascadeGraph(schema: ParsedSchema): CascadeGraph;
33
+ /**
34
+ * Gets all models that would be affected by cascading from a given model
35
+ * Returns models in depth-first order (leaf nodes first)
36
+ *
37
+ * @param graph - The cascade graph
38
+ * @param modelName - The starting model name
39
+ * @returns Array of model names in cascade order
40
+ */
41
+ export declare function getCascadeOrder(graph: CascadeGraph, modelName: string): string[];
42
+ /**
43
+ * Gets the direct children of a model in the cascade graph
44
+ */
45
+ export declare function getDirectChildren(graph: CascadeGraph, modelName: string): CascadeChild[];
46
+ /**
47
+ * Checks if a model has any cascade children
48
+ */
49
+ export declare function hasCascadeChildren(graph: CascadeGraph, modelName: string): boolean;
50
+ /**
51
+ * Gets all soft-deletable children (direct and indirect) of a model
52
+ */
53
+ export declare function getSoftDeletableDescendants(graph: CascadeGraph, schema: ParsedSchema, modelName: string): ParsedModel[];
54
+ //# sourceMappingURL=cascade-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cascade-graph.d.ts","sourceRoot":"","sources":["../src/cascade-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,qDAAqD;IACrD,eAAe,EAAE,OAAO,CAAC;IACzB,yDAAyD;IACzD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;AAE1D;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAyDpE;AASD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAsBV;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,GAChB,YAAY,EAAE,CAEhB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAGT;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,GAChB,WAAW,EAAE,CAiBf"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Builds a cascade graph from a parsed schema
3
+ *
4
+ * The graph maps parent model names to lists of child models that should be
5
+ * soft-deleted when the parent is soft-deleted. Only relations with
6
+ * onDelete: Cascade are included.
7
+ *
8
+ * @param schema - The parsed Prisma schema
9
+ * @returns A cascade graph mapping parents to children
10
+ */
11
+ export function buildCascadeGraph(schema) {
12
+ const graph = {};
13
+ // Initialize empty arrays for all models
14
+ for (const model of schema.models) {
15
+ graph[model.name] = [];
16
+ }
17
+ // For each model, look at its relations to find cascade relationships
18
+ for (const model of schema.models) {
19
+ for (const relation of model.relations) {
20
+ // We only care about relations that have onDelete: Cascade
21
+ if (relation.onDelete !== 'Cascade') {
22
+ continue;
23
+ }
24
+ // Skip list relations - they're the "one" side, we want the "many" side
25
+ // The relation with the foreign key is the child
26
+ if (relation.isList) {
27
+ continue;
28
+ }
29
+ // This model (with the FK) is the child, relation.type is the parent
30
+ const parentModelName = relation.type;
31
+ const parentModel = schema.modelMap.get(parentModelName);
32
+ if (parentModel === undefined) {
33
+ continue;
34
+ }
35
+ // Get the parent's primary key (what the FK references)
36
+ const parentKey = normalizeKey(parentModel.primaryKey);
37
+ const foreignKey = relation.foreignKey;
38
+ // Skip if we don't have FK information
39
+ if (foreignKey.length === 0) {
40
+ continue;
41
+ }
42
+ // Get or create the children array for this parent
43
+ let children = graph[parentModelName];
44
+ if (children === undefined) {
45
+ children = [];
46
+ graph[parentModelName] = children;
47
+ }
48
+ children.push({
49
+ model: model.name,
50
+ foreignKey,
51
+ parentKey,
52
+ isSoftDeletable: model.isSoftDeletable,
53
+ deletedAtField: model.deletedAtField,
54
+ });
55
+ }
56
+ }
57
+ return graph;
58
+ }
59
+ /**
60
+ * Normalizes a primary key to always be an array
61
+ */
62
+ function normalizeKey(key) {
63
+ return Array.isArray(key) ? key : [key];
64
+ }
65
+ /**
66
+ * Gets all models that would be affected by cascading from a given model
67
+ * Returns models in depth-first order (leaf nodes first)
68
+ *
69
+ * @param graph - The cascade graph
70
+ * @param modelName - The starting model name
71
+ * @returns Array of model names in cascade order
72
+ */
73
+ export function getCascadeOrder(graph, modelName) {
74
+ const visited = new Set();
75
+ const result = [];
76
+ function visit(name) {
77
+ if (visited.has(name)) {
78
+ return;
79
+ }
80
+ visited.add(name);
81
+ const children = graph[name];
82
+ if (children !== undefined) {
83
+ for (const child of children) {
84
+ visit(child.model);
85
+ }
86
+ }
87
+ result.push(name);
88
+ }
89
+ visit(modelName);
90
+ return result;
91
+ }
92
+ /**
93
+ * Gets the direct children of a model in the cascade graph
94
+ */
95
+ export function getDirectChildren(graph, modelName) {
96
+ return graph[modelName] ?? [];
97
+ }
98
+ /**
99
+ * Checks if a model has any cascade children
100
+ */
101
+ export function hasCascadeChildren(graph, modelName) {
102
+ const children = graph[modelName];
103
+ return children !== undefined && children.length > 0;
104
+ }
105
+ /**
106
+ * Gets all soft-deletable children (direct and indirect) of a model
107
+ */
108
+ export function getSoftDeletableDescendants(graph, schema, modelName) {
109
+ const order = getCascadeOrder(graph, modelName);
110
+ const descendants = [];
111
+ for (const name of order) {
112
+ // Skip the starting model itself
113
+ if (name === modelName) {
114
+ continue;
115
+ }
116
+ const model = schema.modelMap.get(name);
117
+ if (model?.isSoftDeletable === true) {
118
+ descendants.push(model);
119
+ }
120
+ }
121
+ return descendants;
122
+ }
123
+ //# sourceMappingURL=cascade-graph.js.map