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.
- package/LICENSE +21 -0
- package/README.md +607 -0
- package/dist/index.js +1493 -0
- 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.
|