@riordanpawley/effect-prisma-generator 0.5.0 → 0.5.2

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 (3) hide show
  1. package/README.md +40 -0
  2. package/dist/index.js +296 -50
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -343,6 +343,46 @@ const program = Effect.gen(function* () {
343
343
  });
344
344
  ```
345
345
 
346
+ #### Custom Transaction Options
347
+
348
+ For transactions that need custom options (isolation level, timeout, etc.), use `$transactionWith`:
349
+
350
+ ```typescript
351
+ // Custom isolation level
352
+ yield* prisma.$transactionWith(
353
+ Effect.gen(function* () {
354
+ const user = yield* prisma.user.create({ data: { name: "Alice" } });
355
+ return user;
356
+ }),
357
+ { isolationLevel: "Serializable", timeout: 10000 }
358
+ );
359
+
360
+ // Point-free style for cleaner composition
361
+ import { pipe } from "effect";
362
+
363
+ const myEffect = Effect.gen(function* () {
364
+ const prisma = yield* Prisma;
365
+ return yield* prisma.user.findMany();
366
+ });
367
+
368
+ // Clean, functional composition!
369
+ pipe(
370
+ myEffect,
371
+ prisma.$transaction // No options? Use $transaction directly!
372
+ );
373
+
374
+ // With options, use $transactionWith
375
+ const withSerializable = (eff: Effect.Effect<any, any, any>) =>
376
+ prisma.$transactionWith(eff, { isolationLevel: "Serializable" });
377
+
378
+ pipe(myEffect, withSerializable);
379
+ ```
380
+
381
+ Available transaction options:
382
+ - `isolationLevel`: `"ReadUncommitted" | "ReadCommitted" | "RepeatableRead" | "Serializable"`
383
+ - `maxWait`: Maximum time (ms) Prisma Client will wait to acquire a transaction
384
+ - `timeout`: Maximum time (ms) the transaction can run before being canceled
385
+
346
386
  #### Transaction Rollback Behavior
347
387
 
348
388
  **Any uncaught error in the Effect error channel triggers a rollback:**
package/dist/index.js CHANGED
@@ -126,13 +126,42 @@ function generateRawSqlOperations(customError) {
126
126
  })
127
127
  ),`;
128
128
  }
129
+ /**
130
+ * Generate type aliases for a model to reduce redundant type computation.
131
+ * TypeScript performance is significantly improved when complex types are
132
+ * computed once and reused via aliases rather than inline.
133
+ */
134
+ function generateModelTypeAliases(models) {
135
+ return models
136
+ .map((model) => {
137
+ const modelName = model.name;
138
+ const modelNameCamel = toCamelCase(modelName);
139
+ // Operations that need Args/Result type aliases
140
+ const operations = [
141
+ 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow',
142
+ 'findMany', 'create', 'createMany', 'createManyAndReturn',
143
+ 'delete', 'update', 'deleteMany', 'updateMany', 'updateManyAndReturn',
144
+ 'upsert', 'count', 'aggregate', 'groupBy'
145
+ ];
146
+ const argsAliases = operations
147
+ .map(op => `type ${modelName}${capitalize(op)}Args = PrismaNamespace.Args<BasePrismaClient['${modelNameCamel}'], '${op}'>`)
148
+ .join('\n');
149
+ return argsAliases;
150
+ })
151
+ .join('\n\n');
152
+ }
153
+ function capitalize(str) {
154
+ return str.charAt(0).toUpperCase() + str.slice(1);
155
+ }
129
156
  function generateModelOperations(models, customError) {
130
157
  return models
131
158
  .map((model) => {
132
159
  const modelName = model.name;
133
160
  const modelNameCamel = toCamelCase(modelName);
134
- // Type alias for the model delegate (e.g., BasePrismaClient['user'])
161
+ // Use pre-computed type alias for delegate
135
162
  const delegate = `BasePrismaClient['${modelNameCamel}']`;
163
+ // Helper to get pre-computed Args type alias
164
+ const argsType = (op) => `${modelName}${capitalize(op)}Args`;
136
165
  // Cast Promise results to ensure consistent typing across Prisma versions
137
166
  // This handles Prisma 7's GlobalOmitConfig and works fine with Prisma 6 too
138
167
  const promiseCast = (op, nullable = false) => {
@@ -153,9 +182,13 @@ function generateModelOperations(models, customError) {
153
182
  // Without custom error: use per-operation error types and mappers
154
183
  const errorType = (opErrorType) => customError ? customError.className : opErrorType;
155
184
  const mapperFn = (defaultMapper) => customError ? "mapError" : defaultMapper;
185
+ // Optimized signatures:
186
+ // - Use pre-computed Args type aliases instead of inline PrismaNamespace.Args
187
+ // - Remove Exact wrapper - the extends constraint already provides type safety
188
+ // - This reduces TypeScript's type computation workload significantly
156
189
  return ` ${modelNameCamel}: {
157
- findUnique: <A extends PrismaNamespace.Args<${delegate}, 'findUnique'>>(
158
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findUnique'>>
190
+ findUnique: <A extends ${argsType('findUnique')}>(
191
+ args: A
159
192
  ): Effect.Effect<${resultType('findUnique', true)}, ${errorType('PrismaFindError')}, PrismaClient> =>
160
193
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
161
194
  Effect.tryPromise({
@@ -164,8 +197,8 @@ function generateModelOperations(models, customError) {
164
197
  })
165
198
  ),
166
199
 
167
- findUniqueOrThrow: <A extends PrismaNamespace.Args<${delegate}, 'findUniqueOrThrow'>>(
168
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findUniqueOrThrow'>>
200
+ findUniqueOrThrow: <A extends ${argsType('findUniqueOrThrow')}>(
201
+ args: A
169
202
  ): Effect.Effect<${resultType('findUniqueOrThrow')}, ${errorType('PrismaFindOrThrowError')}, PrismaClient> =>
170
203
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
171
204
  Effect.tryPromise({
@@ -174,8 +207,8 @@ function generateModelOperations(models, customError) {
174
207
  })
175
208
  ),
176
209
 
177
- findFirst: <A extends PrismaNamespace.Args<${delegate}, 'findFirst'> = {}>(
178
- args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findFirst'>>
210
+ findFirst: <A extends ${argsType('findFirst')} = {}>(
211
+ args?: A
179
212
  ): Effect.Effect<${resultType('findFirst', true)}, ${errorType('PrismaFindError')}, PrismaClient> =>
180
213
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
181
214
  Effect.tryPromise({
@@ -184,8 +217,8 @@ function generateModelOperations(models, customError) {
184
217
  })
185
218
  ),
186
219
 
187
- findFirstOrThrow: <A extends PrismaNamespace.Args<${delegate}, 'findFirstOrThrow'> = {}>(
188
- args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findFirstOrThrow'>>
220
+ findFirstOrThrow: <A extends ${argsType('findFirstOrThrow')} = {}>(
221
+ args?: A
189
222
  ): Effect.Effect<${resultType('findFirstOrThrow')}, ${errorType('PrismaFindOrThrowError')}, PrismaClient> =>
190
223
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
191
224
  Effect.tryPromise({
@@ -194,8 +227,8 @@ function generateModelOperations(models, customError) {
194
227
  })
195
228
  ),
196
229
 
197
- findMany: <A extends PrismaNamespace.Args<${delegate}, 'findMany'> = {}>(
198
- args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findMany'>>
230
+ findMany: <A extends ${argsType('findMany')} = {}>(
231
+ args?: A
199
232
  ): Effect.Effect<${resultType('findMany')}, ${errorType('PrismaFindError')}, PrismaClient> =>
200
233
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
201
234
  Effect.tryPromise({
@@ -204,8 +237,8 @@ function generateModelOperations(models, customError) {
204
237
  })
205
238
  ),
206
239
 
207
- create: <A extends PrismaNamespace.Args<${delegate}, 'create'>>(
208
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'create'>>
240
+ create: <A extends ${argsType('create')}>(
241
+ args: A
209
242
  ): Effect.Effect<${resultType('create')}, ${errorType('PrismaCreateError')}, PrismaClient> =>
210
243
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
211
244
  Effect.tryPromise({
@@ -215,7 +248,7 @@ function generateModelOperations(models, customError) {
215
248
  ),
216
249
 
217
250
  createMany: (
218
- args?: PrismaNamespace.Args<${delegate}, 'createMany'>
251
+ args?: ${argsType('createMany')}
219
252
  ): Effect.Effect<PrismaNamespace.BatchPayload, ${errorType('PrismaCreateError')}, PrismaClient> =>
220
253
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
221
254
  Effect.tryPromise({
@@ -224,8 +257,8 @@ function generateModelOperations(models, customError) {
224
257
  })
225
258
  ),
226
259
 
227
- createManyAndReturn: <A extends PrismaNamespace.Args<${delegate}, 'createManyAndReturn'>>(
228
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'createManyAndReturn'>>
260
+ createManyAndReturn: <A extends ${argsType('createManyAndReturn')}>(
261
+ args: A
229
262
  ): Effect.Effect<${resultType('createManyAndReturn')}, ${errorType('PrismaCreateError')}, PrismaClient> =>
230
263
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
231
264
  Effect.tryPromise({
@@ -234,8 +267,8 @@ function generateModelOperations(models, customError) {
234
267
  })
235
268
  ),
236
269
 
237
- delete: <A extends PrismaNamespace.Args<${delegate}, 'delete'>>(
238
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'delete'>>
270
+ delete: <A extends ${argsType('delete')}>(
271
+ args: A
239
272
  ): Effect.Effect<${resultType('delete')}, ${errorType('PrismaDeleteError')}, PrismaClient> =>
240
273
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
241
274
  Effect.tryPromise({
@@ -244,8 +277,8 @@ function generateModelOperations(models, customError) {
244
277
  })
245
278
  ),
246
279
 
247
- update: <A extends PrismaNamespace.Args<${delegate}, 'update'>>(
248
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'update'>>
280
+ update: <A extends ${argsType('update')}>(
281
+ args: A
249
282
  ): Effect.Effect<${resultType('update')}, ${errorType('PrismaUpdateError')}, PrismaClient> =>
250
283
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
251
284
  Effect.tryPromise({
@@ -255,7 +288,7 @@ function generateModelOperations(models, customError) {
255
288
  ),
256
289
 
257
290
  deleteMany: (
258
- args?: PrismaNamespace.Args<${delegate}, 'deleteMany'>
291
+ args?: ${argsType('deleteMany')}
259
292
  ): Effect.Effect<PrismaNamespace.BatchPayload, ${errorType('PrismaDeleteManyError')}, PrismaClient> =>
260
293
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
261
294
  Effect.tryPromise({
@@ -265,7 +298,7 @@ function generateModelOperations(models, customError) {
265
298
  ),
266
299
 
267
300
  updateMany: (
268
- args: PrismaNamespace.Args<${delegate}, 'updateMany'>
301
+ args: ${argsType('updateMany')}
269
302
  ): Effect.Effect<PrismaNamespace.BatchPayload, ${errorType('PrismaUpdateManyError')}, PrismaClient> =>
270
303
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
271
304
  Effect.tryPromise({
@@ -274,8 +307,8 @@ function generateModelOperations(models, customError) {
274
307
  })
275
308
  ),
276
309
 
277
- updateManyAndReturn: <A extends PrismaNamespace.Args<${delegate}, 'updateManyAndReturn'>>(
278
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'updateManyAndReturn'>>
310
+ updateManyAndReturn: <A extends ${argsType('updateManyAndReturn')}>(
311
+ args: A
279
312
  ): Effect.Effect<${resultType('updateManyAndReturn')}, ${errorType('PrismaUpdateManyError')}, PrismaClient> =>
280
313
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
281
314
  Effect.tryPromise({
@@ -284,8 +317,8 @@ function generateModelOperations(models, customError) {
284
317
  })
285
318
  ),
286
319
 
287
- upsert: <A extends PrismaNamespace.Args<${delegate}, 'upsert'>>(
288
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'upsert'>>
320
+ upsert: <A extends ${argsType('upsert')}>(
321
+ args: A
289
322
  ): Effect.Effect<${resultType('upsert')}, ${errorType('PrismaCreateError')}, PrismaClient> =>
290
323
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
291
324
  Effect.tryPromise({
@@ -295,8 +328,8 @@ function generateModelOperations(models, customError) {
295
328
  ),
296
329
 
297
330
  // Aggregation operations
298
- count: <A extends PrismaNamespace.Args<${delegate}, 'count'> = {}>(
299
- args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'count'>>
331
+ count: <A extends ${argsType('count')} = {}>(
332
+ args?: A
300
333
  ): Effect.Effect<${resultType('count')}, ${errorType('PrismaFindError')}, PrismaClient> =>
301
334
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
302
335
  Effect.tryPromise({
@@ -305,8 +338,8 @@ function generateModelOperations(models, customError) {
305
338
  })
306
339
  ),
307
340
 
308
- aggregate: <A extends PrismaNamespace.Args<${delegate}, 'aggregate'>>(
309
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'aggregate'>>
341
+ aggregate: <A extends ${argsType('aggregate')}>(
342
+ args: A
310
343
  ): Effect.Effect<${resultType('aggregate')}, ${errorType('PrismaFindError')}, PrismaClient> =>
311
344
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
312
345
  Effect.tryPromise({
@@ -315,8 +348,8 @@ function generateModelOperations(models, customError) {
315
348
  })
316
349
  ),
317
350
 
318
- groupBy: <A extends PrismaNamespace.Args<${delegate}, 'groupBy'>>(
319
- args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'groupBy'>>
351
+ groupBy: <A extends ${argsType('groupBy')}>(
352
+ args: A
320
353
  ): Effect.Effect<${resultType('groupBy')}, ${errorType('PrismaFindError')}, PrismaClient> =>
321
354
  Effect.flatMap(PrismaClient, ({ tx: client }) =>
322
355
  Effect.tryPromise({
@@ -341,24 +374,32 @@ function parseErrorImportPath(errorImportPath) {
341
374
  async function generateUnifiedService(models, outputDir, clientImportPath, errorImportPath) {
342
375
  const customError = parseErrorImportPath(errorImportPath);
343
376
  const rawSqlOperations = generateRawSqlOperations(customError);
377
+ const modelTypeAliases = generateModelTypeAliases(models);
344
378
  const modelOperations = generateModelOperations(models, customError);
345
379
  // Generate different content based on whether custom error is configured
346
380
  const serviceContent = customError
347
- ? generateCustomErrorService(customError, clientImportPath, rawSqlOperations, modelOperations)
348
- : generateDefaultErrorService(clientImportPath, rawSqlOperations, modelOperations);
381
+ ? generateCustomErrorService(customError, clientImportPath, rawSqlOperations, modelTypeAliases, modelOperations)
382
+ : generateDefaultErrorService(clientImportPath, rawSqlOperations, modelTypeAliases, modelOperations);
349
383
  await promises_1.default.writeFile(node_path_1.default.join(outputDir, "index.ts"), serviceContent);
350
384
  }
351
385
  /**
352
386
  * Generate service with custom user-provided error class.
353
387
  * All operations use a single error type and a simple mapError function.
354
388
  */
355
- function generateCustomErrorService(customError, clientImportPath, rawSqlOperations, modelOperations) {
389
+ function generateCustomErrorService(customError, clientImportPath, rawSqlOperations, modelTypeAliases, modelOperations) {
356
390
  return `${header}
357
391
  import { Context, Effect, Exit, Layer } from "effect"
358
392
  import { Service } from "effect/Effect"
359
393
  import { Prisma as PrismaNamespace, PrismaClient as BasePrismaClient } from "${clientImportPath}"
360
394
  import { ${customError.className}, mapPrismaError } from "${customError.path}"
361
395
 
396
+ // ============================================================================
397
+ // Type aliases for model operations (performance optimization)
398
+ // These are computed once and reused, reducing TypeScript's type-checking workload
399
+ // ============================================================================
400
+
401
+ ${modelTypeAliases}
402
+
362
403
  // Symbol used to identify intentional rollbacks vs actual errors
363
404
  const ROLLBACK = Symbol.for("prisma.effect.rollback")
364
405
 
@@ -557,7 +598,8 @@ export class Prisma extends Service<Prisma>()("Prisma", {
557
598
  * This implementation uses a callback-free transaction pattern that keeps the effect
558
599
  * running in the same fiber as the parent, preserving Ref, FiberRef, and Context access.
559
600
  *
560
- * Options passed here override any defaults set via TransactionConfig layer.
601
+ * Uses default transaction options from PrismaClient constructor.
602
+ * For custom options, use \`$transactionWith\`.
561
603
  *
562
604
  * @example
563
605
  * const result = yield* prisma.$transaction(
@@ -567,16 +609,63 @@ export class Prisma extends Service<Prisma>()("Prisma", {
567
609
  * return user
568
610
  * })
569
611
  * )
612
+ */
613
+ $transaction: <R, E, A>(
614
+ effect: Effect.Effect<A, E, R>
615
+ ) =>
616
+ Effect.flatMap(
617
+ PrismaClient,
618
+ ({ client, tx }): Effect.Effect<A, E | ${customError.className}, R> => {
619
+ // If we're already in a transaction, just run the effect directly (no nesting)
620
+ const isRootClient = "$transaction" in tx
621
+ if (!isRootClient) {
622
+ return effect
623
+ }
624
+
625
+ // Use acquireUseRelease to manage the transaction lifecycle
626
+ // This keeps everything in the same fiber, preserving Ref/FiberRef/Context
627
+ return Effect.acquireUseRelease(
628
+ // Acquire: begin a new transaction with default options
629
+ $begin(client),
630
+
631
+ // Use: run the effect with the transaction client injected
632
+ (txClient) =>
633
+ effect.pipe(
634
+ Effect.provideService(PrismaClient, { tx: txClient, client })
635
+ ),
636
+
637
+ // Release: commit on success, rollback on failure/interruption
638
+ (txClient, exit) =>
639
+ Exit.isSuccess(exit)
640
+ ? Effect.promise(() => txClient.$commit())
641
+ : Effect.promise(() => txClient.$rollback())
642
+ )
643
+ }
644
+ ),
645
+
646
+ /**
647
+ * Execute an effect within a database transaction with custom options.
648
+ * All operations within the effect will be atomic - they either all succeed or all fail.
649
+ *
650
+ * This implementation uses a callback-free transaction pattern that keeps the effect
651
+ * running in the same fiber as the parent, preserving Ref, FiberRef, and Context access.
652
+ *
653
+ * Options passed here override any defaults set in PrismaClient constructor.
570
654
  *
571
655
  * @example
572
656
  * // Override default isolation level for this transaction
573
- * const result = yield* prisma.$transaction(myEffect, {
574
- * isolationLevel: "ReadCommitted"
575
- * })
657
+ * const result = yield* prisma.$transactionWith(
658
+ * Effect.gen(function* () {
659
+ * const user = yield* prisma.user.create({ data: { name: "Alice" } })
660
+ * yield* prisma.post.create({ data: { title: "Hello", authorId: user.id } })
661
+ * return user
662
+ * }),
663
+ * { isolationLevel: "ReadCommitted", timeout: 10000 }
664
+ * )
576
665
  */
577
- $transaction: <R, E, A>(
666
+ $transactionWith: <R, E, A>(
578
667
  effect: Effect.Effect<A, E, R>,
579
- options?: TransactionOptions
668
+ options: TransactionOptions
580
669
  ) =>
581
670
  Effect.flatMap(
582
671
  PrismaClient,
@@ -621,6 +710,9 @@ export class Prisma extends Service<Prisma>()("Prisma", {
621
710
  * ⚠️ WARNING: The isolated transaction can commit while the parent rolls back,
622
711
  * or vice versa. Use carefully to avoid data inconsistencies.
623
712
  *
713
+ * Uses default transaction options from PrismaClient constructor.
714
+ * For custom options, use \`$isolatedTransactionWith\`.
715
+ *
624
716
  * @example
625
717
  * yield* prisma.$transaction(
626
718
  * Effect.gen(function* () {
@@ -634,8 +726,56 @@ export class Prisma extends Service<Prisma>()("Prisma", {
634
726
  * )
635
727
  */
636
728
  $isolatedTransaction: <R, E, A>(
729
+ effect: Effect.Effect<A, E, R>
730
+ ) =>
731
+ Effect.flatMap(
732
+ PrismaClient,
733
+ ({ client }): Effect.Effect<A, E | ${customError.className}, R> => {
734
+ // Always use the root client to create a fresh transaction
735
+ return Effect.acquireUseRelease(
736
+ $begin(client),
737
+ (txClient) =>
738
+ effect.pipe(
739
+ Effect.provideService(PrismaClient, { tx: txClient, client })
740
+ ),
741
+ (txClient, exit) =>
742
+ Exit.isSuccess(exit)
743
+ ? Effect.promise(() => txClient.$commit())
744
+ : Effect.promise(() => txClient.$rollback())
745
+ )
746
+ }
747
+ ),
748
+
749
+ /**
750
+ * Execute an effect in a NEW transaction with custom options, even if already inside a transaction.
751
+ * Unlike \`$transaction\`, this always creates a fresh, independent transaction.
752
+ *
753
+ * Use this for operations that should NOT be rolled back with the parent:
754
+ * - Audit logging that must persist even if main operation fails
755
+ * - Saga pattern where each step has independent commit/rollback
756
+ * - Background job queuing that should commit immediately
757
+ *
758
+ * ⚠️ WARNING: The isolated transaction can commit while the parent rolls back,
759
+ * or vice versa. Use carefully to avoid data inconsistencies.
760
+ *
761
+ * Options passed here override any defaults set in PrismaClient constructor.
762
+ *
763
+ * @example
764
+ * yield* prisma.$transaction(
765
+ * Effect.gen(function* () {
766
+ * // This audit log commits independently with custom isolation level
767
+ * yield* prisma.$isolatedTransactionWith(
768
+ * prisma.auditLog.create({ data: { action: "attempt", userId } }),
769
+ * { isolationLevel: "Serializable" }
770
+ * )
771
+ * // Main operation - if this fails, audit log is still committed
772
+ * yield* prisma.user.delete({ where: { id: userId } })
773
+ * })
774
+ * )
775
+ */
776
+ $isolatedTransactionWith: <R, E, A>(
637
777
  effect: Effect.Effect<A, E, R>,
638
- options?: TransactionOptions
778
+ options: TransactionOptions
639
779
  ) =>
640
780
  Effect.flatMap(
641
781
  PrismaClient,
@@ -749,12 +889,19 @@ export const makePrismaLayerEffect = PrismaClient.layerEffect
749
889
  * Generate service with default tagged error classes.
750
890
  * Operations have per-operation error types for fine-grained error handling.
751
891
  */
752
- function generateDefaultErrorService(clientImportPath, rawSqlOperations, modelOperations) {
892
+ function generateDefaultErrorService(clientImportPath, rawSqlOperations, modelTypeAliases, modelOperations) {
753
893
  return `${header}
754
894
  import { Context, Data, Effect, Exit, Layer } from "effect"
755
895
  import { Service } from "effect/Effect"
756
896
  import { Prisma as PrismaNamespace, PrismaClient as BasePrismaClient } from "${clientImportPath}"
757
897
 
898
+ // ============================================================================
899
+ // Type aliases for model operations (performance optimization)
900
+ // These are computed once and reused, reducing TypeScript's type-checking workload
901
+ // ============================================================================
902
+
903
+ ${modelTypeAliases}
904
+
758
905
  // Symbol used to identify intentional rollbacks vs actual errors
759
906
  const ROLLBACK = Symbol.for("prisma.effect.rollback")
760
907
 
@@ -1290,7 +1437,8 @@ export class Prisma extends Service<Prisma>()("Prisma", {
1290
1437
  * This implementation uses a callback-free transaction pattern that keeps the effect
1291
1438
  * running in the same fiber as the parent, preserving Ref, FiberRef, and Context access.
1292
1439
  *
1293
- * Options passed here override any defaults set via transactionOptions in the layer.
1440
+ * Uses default transaction options from PrismaClient constructor.
1441
+ * For custom options, use \`$transactionWith\`.
1294
1442
  *
1295
1443
  * @example
1296
1444
  * const result = yield* prisma.$transaction(
@@ -1300,16 +1448,63 @@ export class Prisma extends Service<Prisma>()("Prisma", {
1300
1448
  * return user
1301
1449
  * })
1302
1450
  * )
1451
+ */
1452
+ $transaction: <R, E, A>(
1453
+ effect: Effect.Effect<A, E, R>
1454
+ ) =>
1455
+ Effect.flatMap(
1456
+ PrismaClient,
1457
+ ({ client, tx }): Effect.Effect<A, E | PrismaError, R> => {
1458
+ // If we're already in a transaction, just run the effect directly (no nesting)
1459
+ const isRootClient = "$transaction" in tx
1460
+ if (!isRootClient) {
1461
+ return effect
1462
+ }
1463
+
1464
+ // Use acquireUseRelease to manage the transaction lifecycle
1465
+ // This keeps everything in the same fiber, preserving Ref/FiberRef/Context
1466
+ return Effect.acquireUseRelease(
1467
+ // Acquire: begin a new transaction with default options
1468
+ $begin(client),
1469
+
1470
+ // Use: run the effect with the transaction client injected
1471
+ (txClient) =>
1472
+ effect.pipe(
1473
+ Effect.provideService(PrismaClient, { tx: txClient, client })
1474
+ ),
1475
+
1476
+ // Release: commit on success, rollback on failure/interruption
1477
+ (txClient, exit) =>
1478
+ Exit.isSuccess(exit)
1479
+ ? Effect.promise(() => txClient.$commit())
1480
+ : Effect.promise(() => txClient.$rollback())
1481
+ )
1482
+ }
1483
+ ),
1484
+
1485
+ /**
1486
+ * Execute an effect within a database transaction with custom options.
1487
+ * All operations within the effect will be atomic - they either all succeed or all fail.
1488
+ *
1489
+ * This implementation uses a callback-free transaction pattern that keeps the effect
1490
+ * running in the same fiber as the parent, preserving Ref, FiberRef, and Context access.
1491
+ *
1492
+ * Options passed here override any defaults set in PrismaClient constructor.
1303
1493
  *
1304
1494
  * @example
1305
1495
  * // Override default isolation level for this transaction
1306
- * const result = yield* prisma.$transaction(myEffect, {
1307
- * isolationLevel: "ReadCommitted"
1308
- * })
1496
+ * const result = yield* prisma.$transactionWith(
1497
+ * Effect.gen(function* () {
1498
+ * const user = yield* prisma.user.create({ data: { name: "Alice" } })
1499
+ * yield* prisma.post.create({ data: { title: "Hello", authorId: user.id } })
1500
+ * return user
1501
+ * }),
1502
+ * { isolationLevel: "ReadCommitted", timeout: 10000 }
1503
+ * )
1309
1504
  */
1310
- $transaction: <R, E, A>(
1505
+ $transactionWith: <R, E, A>(
1311
1506
  effect: Effect.Effect<A, E, R>,
1312
- options?: TransactionOptions
1507
+ options: TransactionOptions
1313
1508
  ) =>
1314
1509
  Effect.flatMap(
1315
1510
  PrismaClient,
@@ -1354,6 +1549,9 @@ export class Prisma extends Service<Prisma>()("Prisma", {
1354
1549
  * ⚠️ WARNING: The isolated transaction can commit while the parent rolls back,
1355
1550
  * or vice versa. Use carefully to avoid data inconsistencies.
1356
1551
  *
1552
+ * Uses default transaction options from PrismaClient constructor.
1553
+ * For custom options, use \`$isolatedTransactionWith\`.
1554
+ *
1357
1555
  * @example
1358
1556
  * yield* prisma.$transaction(
1359
1557
  * Effect.gen(function* () {
@@ -1367,8 +1565,56 @@ export class Prisma extends Service<Prisma>()("Prisma", {
1367
1565
  * )
1368
1566
  */
1369
1567
  $isolatedTransaction: <R, E, A>(
1568
+ effect: Effect.Effect<A, E, R>
1569
+ ) =>
1570
+ Effect.flatMap(
1571
+ PrismaClient,
1572
+ ({ client }): Effect.Effect<A, E | PrismaError, R> => {
1573
+ // Always use the root client to create a fresh transaction
1574
+ return Effect.acquireUseRelease(
1575
+ $begin(client),
1576
+ (txClient) =>
1577
+ effect.pipe(
1578
+ Effect.provideService(PrismaClient, { tx: txClient, client })
1579
+ ),
1580
+ (txClient, exit) =>
1581
+ Exit.isSuccess(exit)
1582
+ ? Effect.promise(() => txClient.$commit())
1583
+ : Effect.promise(() => txClient.$rollback())
1584
+ )
1585
+ }
1586
+ ),
1587
+
1588
+ /**
1589
+ * Execute an effect in a NEW transaction with custom options, even if already inside a transaction.
1590
+ * Unlike \`$transaction\`, this always creates a fresh, independent transaction.
1591
+ *
1592
+ * Use this for operations that should NOT be rolled back with the parent:
1593
+ * - Audit logging that must persist even if main operation fails
1594
+ * - Saga pattern where each step has independent commit/rollback
1595
+ * - Background job queuing that should commit immediately
1596
+ *
1597
+ * ⚠️ WARNING: The isolated transaction can commit while the parent rolls back,
1598
+ * or vice versa. Use carefully to avoid data inconsistencies.
1599
+ *
1600
+ * Options passed here override any defaults set in PrismaClient constructor.
1601
+ *
1602
+ * @example
1603
+ * yield* prisma.$transaction(
1604
+ * Effect.gen(function* () {
1605
+ * // This audit log commits independently with custom isolation level
1606
+ * yield* prisma.$isolatedTransactionWith(
1607
+ * prisma.auditLog.create({ data: { action: "attempt", userId } }),
1608
+ * { isolationLevel: "Serializable" }
1609
+ * )
1610
+ * // Main operation - if this fails, audit log is still committed
1611
+ * yield* prisma.user.delete({ where: { id: userId } })
1612
+ * })
1613
+ * )
1614
+ */
1615
+ $isolatedTransactionWith: <R, E, A>(
1370
1616
  effect: Effect.Effect<A, E, R>,
1371
- options?: TransactionOptions
1617
+ options: TransactionOptions
1372
1618
  ) =>
1373
1619
  Effect.flatMap(
1374
1620
  PrismaClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riordanpawley/effect-prisma-generator",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Prisma generator for Effect (fork with Prisma 7 support)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "https://github.com/riordanpawley/effect-prisma-generator.git"
20
+ "url": "git+https://github.com/riordanpawley/effect-prisma-generator.git"
21
21
  },
22
22
  "keywords": [
23
23
  "prisma",