prisma-generator-effect 0.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +607 -0
  3. package/dist/index.js +1493 -0
  4. package/package.json +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Niccolo' Di Chio
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,607 @@
1
+ # Effect Prisma Generator
2
+
3
+ A Prisma generator that creates a fully-typed, Effect-based service wrapper for your Prisma Client.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Effect Integration**: All Prisma operations are wrapped in `Effect` for robust error handling and composability.
8
+ - 🛡️ **Type Safety**: Full TypeScript support with generated types matching your Prisma schema.
9
+ - 🧩 **Dependency Injection**: Integrates seamlessly with Effect's `Layer` and `Context` system.
10
+ - 🔍 **Error Handling**: Automatically catches and wraps Prisma errors into typed `PrismaError` variants.
11
+
12
+ ## Installation
13
+
14
+ Install the generator as a development dependency:
15
+
16
+ ```bash
17
+ npm install -D prisma-generator-effect
18
+ # or
19
+ pnpm add -D prisma-generator-effect
20
+ # or
21
+ yarn add -D prisma-generator-effect
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ Add the generator to your `schema.prisma` file:
27
+
28
+ ```prisma
29
+ // prisma/schema.prisma
30
+ generator client {
31
+ provider = "prisma-client-js"
32
+ output = "./generated/client"
33
+ }
34
+
35
+ generator effect {
36
+ provider = "prisma-generator-effect"
37
+ output = "./generated/effect" // relative to the schema.prisma file, e.g. prisma/generated/effect
38
+ clientImportPath = "../client" // relative to the output path ^here (defaults to "@prisma/client")
39
+ }
40
+ ```
41
+
42
+ Then run `prisma generate` to generate the client and the Effect service.
43
+
44
+ ### Configuration Options
45
+
46
+ | Option | Description | Default |
47
+ |--------|-------------|---------|
48
+ | `output` | Output directory for generated code (relative to schema.prisma) | `../generated/effect` |
49
+ | `clientImportPath` | Import path for Prisma Client (relative to output) | `@prisma/client` |
50
+ | `errorImportPath` | Custom error module path (relative to schema.prisma), e.g. `./errors#MyError` | - |
51
+ | `importFileExtension` | File extension for relative imports (`js`, `ts`, or empty) | `""` |
52
+
53
+ ### ESM / Import Extensions
54
+
55
+ For ESM projects that require explicit file extensions in imports, use `importFileExtension`:
56
+
57
+ ```prisma
58
+ generator effect {
59
+ provider = "prisma-generator-effect"
60
+ output = "./generated/effect"
61
+ clientImportPath = "../client/index.js"
62
+ errorImportPath = "./errors#MyPrismaError" // No extension needed here
63
+ importFileExtension = "js" // Generator adds .js to relative imports
64
+ }
65
+ ```
66
+
67
+ This will generate imports like:
68
+ ```typescript
69
+ import { MyPrismaError, mapPrismaError } from "../../errors.js"
70
+ ```
71
+
72
+ ### Recommended
73
+
74
+ Add the following to your `tsconfig.json`:
75
+
76
+ ```json
77
+ {
78
+ "compilerOptions": {
79
+ "paths": {
80
+ "@prisma/*": ["./prisma/generated/*"]
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ Then you can import the generated types like this:
87
+
88
+ ```typescript
89
+ import { Prisma } from "@prisma/effect";
90
+ ```
91
+
92
+ Otherwise, you can import the generated types like this (adjust the path accordingly):
93
+
94
+ ```typescript
95
+ import { Prisma } from "../../prisma/generated/effect";
96
+ ```
97
+
98
+ ## Usage
99
+
100
+ ### Quick Start
101
+
102
+ ```typescript
103
+ import { Prisma } from "@prisma/effect";
104
+ import { Effect } from "effect";
105
+
106
+ const program = Effect.gen(function* () {
107
+ const prisma = yield* Prisma;
108
+
109
+ const users = yield* prisma.user.findMany({
110
+ where: { active: true },
111
+ });
112
+
113
+ return users;
114
+ });
115
+
116
+ // Run with the default layer (Prisma 6)
117
+ Effect.runPromise(program.pipe(Effect.provide(Prisma.Live)));
118
+ ```
119
+
120
+ ### Layer Options
121
+
122
+ The generator provides several ways to create layers:
123
+
124
+ ```typescript
125
+ import { Prisma, PrismaClient } from "@prisma/effect";
126
+ import { Effect, Layer } from "effect";
127
+
128
+ // 1. Default layer (Prisma 6, no options)
129
+ Prisma.Live
130
+
131
+ // 2. Layer with static options
132
+ Prisma.layer({ datasourceUrl: process.env.DATABASE_URL })
133
+
134
+ // 3. Layer with effectful options (for adapters, config services, etc.)
135
+ Prisma.layerEffect(
136
+ Effect.gen(function* () {
137
+ const config = yield* ConfigService;
138
+ return { datasourceUrl: config.databaseUrl };
139
+ })
140
+ )
141
+
142
+ // 4. For Prisma 7 with adapters
143
+ Prisma.layerEffect(
144
+ Effect.gen(function* () {
145
+ const pool = yield* PostgresPool;
146
+ const adapter = yield* Effect.sync(() => new PrismaNeon(pool));
147
+ return { adapter };
148
+ })
149
+ )
150
+ ```
151
+
152
+ ### Full Example
153
+
154
+ ```typescript
155
+ import { Prisma } from "./generated/effect";
156
+ import { Effect } from "effect";
157
+
158
+ const program = Effect.gen(function* () {
159
+ const prisma = yield* Prisma;
160
+
161
+ // All standard Prisma operations are available
162
+ const users = yield* prisma.user.findMany({
163
+ where: { active: true },
164
+ select: {
165
+ id: true,
166
+ accounts: {
167
+ select: {
168
+ id: true,
169
+ },
170
+ },
171
+ },
172
+ });
173
+ // users: { id: string, accounts: { id: string }[] }[]
174
+
175
+ return users;
176
+ });
177
+
178
+ // Run the program
179
+ Effect.runPromise(program.pipe(Effect.provide(Prisma.Live)));
180
+ ```
181
+
182
+ ## API
183
+
184
+ The generated `Prisma` service mirrors your Prisma Client API but returns `Effect<Success, PrismaError, Requirements>` instead of Promises.
185
+
186
+ ### Layer Constructors
187
+
188
+ | API | Description |
189
+ |-----|-------------|
190
+ | `Prisma.Live` | Complete default layer (Prisma 6, no options) |
191
+ | `Prisma.layer(opts)` | Complete layer with PrismaClient options |
192
+ | `Prisma.layerEffect(effect)` | Complete layer with effectful options |
193
+ | `Prisma.Default` | Just the service layer (for advanced composition) |
194
+ | `PrismaClient.layer(opts)` | Just the client layer (for advanced composition) |
195
+ | `PrismaClient.layerEffect(effect)` | Just the client layer with effectful options |
196
+ | `PrismaClient.Default` | Default client layer (for advanced composition) |
197
+
198
+ ### Error Handling
199
+
200
+ Operations return typed errors that you can handle with Effect's error handling utilities:
201
+
202
+ ```typescript
203
+ import {
204
+ Prisma,
205
+ PrismaUniqueConstraintError,
206
+ PrismaRecordNotFoundError,
207
+ PrismaForeignKeyConstraintError,
208
+ } from "@prisma/effect";
209
+
210
+ const program = Effect.gen(function* () {
211
+ const prisma = yield* Prisma;
212
+
213
+ // Handle specific error types with catchTag
214
+ const user = yield* prisma.user
215
+ .create({ data: { email: "alice@example.com" } })
216
+ .pipe(
217
+ Effect.catchTag("PrismaUniqueConstraintError", (error) => {
218
+ console.log(`Duplicate email: ${error.cause.code}`); // P2002
219
+ return Effect.succeed(null);
220
+ }),
221
+ );
222
+
223
+ // OrThrow methods return PrismaRecordNotFoundError
224
+ const found = yield* prisma.user
225
+ .findUniqueOrThrow({ where: { id: 999 } })
226
+ .pipe(
227
+ Effect.catchTag("PrismaRecordNotFoundError", () =>
228
+ Effect.succeed(null),
229
+ ),
230
+ );
231
+ });
232
+ ```
233
+
234
+ **Available error types:**
235
+
236
+ | Error Type | Prisma Code | When it occurs |
237
+ |------------|-------------|----------------|
238
+ | `PrismaUniqueConstraintError` | P2002 | Duplicate unique field |
239
+ | `PrismaRecordNotFoundError` | P2025 | `findUniqueOrThrow`, `findFirstOrThrow`, `update`, `delete` on non-existent |
240
+ | `PrismaForeignKeyConstraintError` | P2003 | Invalid foreign key reference |
241
+ | `PrismaValueTooLongError` | P2000 | Value exceeds column length |
242
+ | `PrismaDbConstraintError` | P2004 | Database constraint violation |
243
+ | `PrismaInputValidationError` | P2005, P2006, P2019 | Invalid input value |
244
+ | `PrismaMissingRequiredValueError` | P2011, P2012 | Required field is null |
245
+ | `PrismaRelationViolationError` | P2014 | Relation constraint violation |
246
+ | `PrismaRelatedRecordNotFoundError` | P2015, P2018 | Related record not found |
247
+ | `PrismaValueOutOfRangeError` | P2020 | Value out of range |
248
+ | `PrismaConnectionError` | P2024 | Connection pool timeout |
249
+ | `PrismaTransactionConflictError` | P2034 | Transaction conflict (retry) |
250
+
251
+ ### Custom Error Mapping
252
+
253
+ If you want to use your own error type instead of the built-in tagged errors, you can configure `errorImportPath` in your schema:
254
+
255
+ ```prisma
256
+ generator effect {
257
+ provider = "prisma-generator-effect"
258
+ output = "./generated/effect"
259
+ clientImportPath = "../client"
260
+ errorImportPath = "./errors#MyPrismaError" // relative to schema.prisma
261
+ }
262
+ ```
263
+
264
+ > **Note:** The `errorImportPath` is relative to your `schema.prisma` file location, not the output directory. The generator automatically calculates the correct import path for the generated code.
265
+
266
+ Your error module must export:
267
+ 1. **The error class** - Your custom error type
268
+ 2. **A mapper function** named `mapPrismaError` - Maps raw errors to your type
269
+
270
+ ```typescript
271
+ // errors.ts
272
+ import { Data } from "effect";
273
+ import { Prisma } from "@prisma/client";
274
+
275
+ export class MyPrismaError extends Data.TaggedError("MyPrismaError")<{
276
+ cause: unknown;
277
+ operation: string;
278
+ model: string;
279
+ code?: string; // You can add custom fields
280
+ }> {}
281
+
282
+ export const mapPrismaError = (
283
+ error: unknown,
284
+ operation: string,
285
+ model: string
286
+ ): MyPrismaError => {
287
+ // You can inspect the error and add custom handling
288
+ const code = error instanceof Prisma.PrismaClientKnownRequestError
289
+ ? error.code
290
+ : undefined;
291
+
292
+ // Option: throw unknown errors as defects
293
+ // if (!(error instanceof Prisma.PrismaClientKnownRequestError)) {
294
+ // throw error;
295
+ // }
296
+
297
+ return new MyPrismaError({ cause: error, operation, model, code });
298
+ };
299
+ ```
300
+
301
+ Now all operations will use your `MyPrismaError` type:
302
+
303
+ ```typescript
304
+ import { Prisma, MyPrismaError } from "./generated/effect";
305
+
306
+ const program = Effect.gen(function* () {
307
+ const prisma = yield* Prisma;
308
+
309
+ // All errors are now MyPrismaError
310
+ yield* prisma.user
311
+ .create({ data: { email: "alice@example.com" } })
312
+ .pipe(
313
+ Effect.catchTag("MyPrismaError", (error) => {
314
+ console.log(`Operation: ${error.operation}, Code: ${error.code}`);
315
+ return Effect.succeed(null);
316
+ }),
317
+ );
318
+ });
319
+ ```
320
+
321
+ This is useful when:
322
+ - Migrating from an existing codebase that uses a single error type
323
+ - You want to add custom fields/metadata to errors
324
+ - You want control over which errors are recoverable vs defects
325
+
326
+ ### Transactions
327
+
328
+ The generated service includes a `$transaction` method that allows you to run multiple operations within a database transaction.
329
+
330
+ ```typescript
331
+ const program = Effect.gen(function* () {
332
+ const prisma = yield* Prisma;
333
+
334
+ const result = yield* prisma.$transaction(
335
+ Effect.gen(function* () {
336
+ const user = yield* prisma.user.create({ data: { name: "Alice" } });
337
+ const post = yield* prisma.post.create({
338
+ data: { title: "Hello", authorId: user.id },
339
+ });
340
+ return { user, post };
341
+ }),
342
+ );
343
+ });
344
+ ```
345
+
346
+ #### Transaction Rollback Behavior
347
+
348
+ **Any uncaught error in the Effect error channel triggers a rollback:**
349
+
350
+ ```typescript
351
+ // Rollback on Effect.fail()
352
+ yield* prisma.$transaction(
353
+ Effect.gen(function* () {
354
+ yield* prisma.user.create({ data: { email: "alice@example.com" } });
355
+ yield* Effect.fail("Something went wrong"); // Triggers rollback
356
+ }),
357
+ );
358
+ // User is NOT created
359
+
360
+ // Rollback on Prisma errors (e.g., findUniqueOrThrow)
361
+ yield* prisma.$transaction(
362
+ Effect.gen(function* () {
363
+ yield* prisma.user.create({ data: { email: "bob@example.com" } });
364
+ yield* prisma.user.findUniqueOrThrow({ where: { id: 999 } }); // Throws!
365
+ }),
366
+ );
367
+ // User is NOT created
368
+ ```
369
+
370
+ **Catching errors prevents rollback:**
371
+
372
+ ```typescript
373
+ yield* prisma.$transaction(
374
+ Effect.gen(function* () {
375
+ yield* prisma.user.create({ data: { email: "alice@example.com" } });
376
+
377
+ // Catch the error - transaction continues
378
+ yield* prisma.user
379
+ .findUniqueOrThrow({ where: { id: 999 } })
380
+ .pipe(Effect.catchAll(() => Effect.succeed(null)));
381
+
382
+ yield* prisma.user.create({ data: { email: "bob@example.com" } });
383
+ }),
384
+ );
385
+ // Both users ARE created
386
+ ```
387
+
388
+ **Custom error types are preserved:**
389
+
390
+ ```typescript
391
+ class MyError extends Data.TaggedError("MyError")<{ message: string }> {}
392
+
393
+ const error = yield* prisma
394
+ .$transaction(Effect.fail(new MyError({ message: "oops" })))
395
+ .pipe(Effect.flip);
396
+
397
+ expect(error).toBeInstanceOf(MyError); // Type is preserved!
398
+ ```
399
+
400
+ ### Nested Transactions
401
+
402
+ Nested `$transaction` calls share the **same underlying database transaction**. There are no savepoints - all operations run in a single transaction that commits or rolls back together.
403
+
404
+ ```typescript
405
+ yield* prisma.$transaction(
406
+ Effect.gen(function* () {
407
+ yield* prisma.user.create({ data: { name: "Outer" } });
408
+
409
+ yield* prisma.$transaction(
410
+ Effect.gen(function* () {
411
+ yield* prisma.user.create({ data: { name: "Inner" } });
412
+ }),
413
+ );
414
+
415
+ yield* Effect.fail("Outer failure");
416
+ }),
417
+ );
418
+ // BOTH users are rolled back
419
+ ```
420
+
421
+ #### Key Behaviors
422
+
423
+ | Scenario | Result |
424
+ |----------|--------|
425
+ | Both succeed | All committed |
426
+ | Inner fails (uncaught) | All rollback |
427
+ | Inner succeeds, outer fails | All rollback |
428
+ | Inner fails (caught), outer succeeds | **All committed** (including inner's data!) |
429
+
430
+ > **Important:** When you catch an inner transaction's error, its writes are NOT rolled back because there are no savepoints. All operations share the same database transaction.
431
+
432
+ #### Composable Service Functions
433
+
434
+ Functions that use `$transaction` internally work seamlessly when called from an outer transaction:
435
+
436
+ ```typescript
437
+ // Service function with its own transaction
438
+ const UserService = {
439
+ createWithProfile: (email: string) =>
440
+ Effect.gen(function* () {
441
+ const prisma = yield* Prisma;
442
+ return yield* prisma.$transaction(
443
+ Effect.gen(function* () {
444
+ const user = yield* prisma.user.create({ data: { email } });
445
+ yield* prisma.profile.create({ data: { userId: user.id } });
446
+ return user;
447
+ }),
448
+ );
449
+ }),
450
+ };
451
+
452
+ // Called standalone - creates its own transaction
453
+ yield* UserService.createWithProfile("alice@example.com");
454
+
455
+ // Called inside outer transaction - joins it
456
+ yield* prisma.$transaction(
457
+ Effect.gen(function* () {
458
+ yield* UserService.createWithProfile("alice@example.com");
459
+ yield* UserService.createWithProfile("bob@example.com");
460
+ // If anything fails, both users are rolled back
461
+ }),
462
+ );
463
+ ```
464
+
465
+ This pattern allows you to:
466
+ 1. Write self-contained service functions that are safe to call standalone
467
+ 2. Compose them in outer transactions for end-to-end atomicity
468
+ 3. Functions don't need to know if they're inside another transaction
469
+
470
+ ### Building Effect Services with Prisma
471
+
472
+ You can build layered Effect services that wrap `Prisma`. Transactions work correctly through any level of service composition.
473
+
474
+ ```typescript
475
+ // Level 1: Repository layer
476
+ class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
477
+ effect: Effect.gen(function* () {
478
+ const db = yield* Prisma;
479
+ return {
480
+ create: (email: string, name: string) =>
481
+ db.user.create({ data: { email, name } }),
482
+ findById: (id: number) =>
483
+ db.user.findUnique({ where: { id } }),
484
+ };
485
+ }),
486
+ }) {}
487
+
488
+ class PostRepo extends Effect.Service<PostRepo>()("PostRepo", {
489
+ effect: Effect.gen(function* () {
490
+ const db = yield* Prisma;
491
+ return {
492
+ create: (title: string, authorId: number) =>
493
+ db.post.create({ data: { title, authorId } }),
494
+ };
495
+ }),
496
+ }) {}
497
+
498
+ // Level 2: Domain service composing repositories
499
+ class BlogService extends Effect.Service<BlogService>()("BlogService", {
500
+ effect: Effect.gen(function* () {
501
+ const users = yield* UserRepo;
502
+ const posts = yield* PostRepo;
503
+ const db = yield* Prisma;
504
+
505
+ return {
506
+ createAuthorWithPost: (email: string, name: string, title: string) =>
507
+ db.$transaction(
508
+ Effect.gen(function* () {
509
+ const user = yield* users.create(email, name);
510
+ const post = yield* posts.create(title, user.id);
511
+ return { user, post };
512
+ }),
513
+ ),
514
+ };
515
+ }),
516
+ }) {}
517
+
518
+ // Wire up the layers
519
+ const RepoLayer = Layer.merge(UserRepo.Default, PostRepo.Default).pipe(
520
+ Layer.provide(Prisma.Live),
521
+ );
522
+ const ServiceLayer = BlogService.Default.pipe(
523
+ Layer.provide(RepoLayer),
524
+ Layer.provide(Prisma.Live),
525
+ );
526
+
527
+ // Use it
528
+ const program = Effect.gen(function* () {
529
+ const blog = yield* BlogService;
530
+ return yield* blog.createAuthorWithPost("alice@example.com", "Alice", "Hello World");
531
+ });
532
+
533
+ Effect.runPromise(program.pipe(Effect.provide(ServiceLayer)));
534
+ ```
535
+
536
+ #### Why This Works
537
+
538
+ You might wonder: if `Prisma` is captured at layer construction time, how do transactions work?
539
+
540
+ The key is **deferred execution**. When you call `db.user.create({ data })`, it doesn't execute immediately—it returns an **Effect** that describes what to do:
541
+
542
+ ```typescript
543
+ // Generated code (simplified)
544
+ user: {
545
+ create: (args) => Effect.flatMap(PrismaClient, ({ tx: client }) =>
546
+ Effect.tryPromise({ try: () => client.user.create(args), ... })
547
+ )
548
+ }
549
+ ```
550
+
551
+ The `Effect.flatMap(PrismaClient, ...)` defers the lookup of `PrismaClient` until the Effect actually runs. When `$transaction` executes an inner effect, it provides a new `PrismaClient` with the transaction client:
552
+
553
+ ```typescript
554
+ // Inside $transaction (simplified)
555
+ effect.pipe(Effect.provideService(PrismaClient, { tx: transactionClient, client }))
556
+ ```
557
+
558
+ So even though you capture `db` (the `Prisma` service) at layer construction, the actual database client lookup happens at execution time—inside the transaction scope.
559
+
560
+ This means:
561
+ - ✅ Services can store references to `Prisma` at construction
562
+ - ✅ Services can store effect-returning methods (e.g., `const createUser = db.user.create`)
563
+ - ✅ Transactions work correctly through any number of service layers
564
+ - ✅ Nested `$transaction` calls properly join the outer transaction
565
+
566
+ ### Resource Management
567
+
568
+ The `PrismaClient.layer` function uses `Layer.scoped` with a finalizer to ensure the PrismaClient is properly disconnected when the layer scope ends:
569
+
570
+ ```typescript
571
+ // Generated code (simplified)
572
+ export class PrismaClient extends Context.Tag("PrismaClient")<...>() {
573
+ static layer = <T extends ConstructorParameters<typeof BasePrismaClient>[0]>(options: T) =>
574
+ Layer.scoped(
575
+ PrismaClient,
576
+ Effect.gen(function* () {
577
+ const prisma = new BasePrismaClient(options)
578
+ yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
579
+ return { tx: prisma, client: prisma }
580
+ })
581
+ )
582
+ }
583
+ ```
584
+
585
+ This means:
586
+ - The connection is automatically cleaned up when the program completes
587
+ - The connection is cleaned up even if the program fails
588
+ - Each scoped usage gets its own PrismaClient instance
589
+
590
+ ```typescript
591
+ // Connection is automatically managed
592
+ const program = Effect.gen(function* () {
593
+ const prisma = yield* Prisma;
594
+ yield* prisma.user.findMany();
595
+ // ... more operations
596
+ });
597
+
598
+ // $disconnect is called automatically when this completes
599
+ await Effect.runPromise(
600
+ program.pipe(
601
+ Effect.provide(Prisma.Live),
602
+ Effect.scoped,
603
+ )
604
+ );
605
+ ```
606
+
607
+ For long-running applications (like servers), you typically provide the layer once at startup and it stays connected for the lifetime of the application.