prisma-generator-effect 1.1.2-beta.1 → 1.1.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@ A Prisma generator that creates a fully-typed, Effect-based service wrapper for
8
8
  - 🛡️ **Type Safety**: Full TypeScript support with generated types matching your Prisma schema.
9
9
  - 🧩 **Dependency Injection**: Integrates seamlessly with Effect's `Layer` and `Context` system.
10
10
  - 🔍 **Error Handling**: Automatically catches and wraps Prisma errors into typed `PrismaError` variants.
11
+ - 🔀 **Read Replicas**: Optional `PrismaReplicas` service automatically routes reads to replicas, with `$primary()` / `$replica()` escape hatches for explicit routing.
11
12
 
12
13
  ## Installation
13
14
 
@@ -605,3 +606,104 @@ await Effect.runPromise(
605
606
  ```
606
607
 
607
608
  For long-running applications (like servers), you typically provide the layer once at startup and it stays connected for the lifetime of the application.
609
+
610
+ ### Read Replicas
611
+
612
+ The generator emits an optional `PrismaReplicas` service tag that, when provided in
613
+ the layer context, causes read operations to be routed to a randomly selected replica
614
+ instead of the primary client. This is inspired by
615
+ [`@prisma/extension-read-replicas`](https://github.com/prisma/extension-read-replicas)
616
+ but implemented natively at the Effect service level (no extension runtime dependency).
617
+
618
+ #### Layer setup
619
+
620
+ Provide replicas alongside the primary `Prisma.layer(...)`:
621
+
622
+ ```typescript
623
+ import { Layer } from "effect"
624
+ import { Prisma, PrismaReplicas } from "./generated/effect"
625
+
626
+ const AppLayer = Layer.mergeAll(
627
+ // Primary - handles writes, transactions, raw SQL
628
+ Prisma.layer({ adapter: primaryAdapter }),
629
+ // Replicas - handle reads (optional)
630
+ PrismaReplicas.layer([
631
+ { adapter: replica1Adapter },
632
+ { adapter: replica2Adapter },
633
+ ]),
634
+ )
635
+ ```
636
+
637
+ `PrismaReplicas` also exposes a `layerEffect` constructor (for when the replica configs
638
+ come from an Effect, e.g. a config service) and a `layerFromClients` helper (for passing
639
+ already-constructed `PrismaClient` instances whose lifecycle you manage yourself).
640
+
641
+ Each replica client created by `PrismaReplicas.layer` is automatically disconnected when
642
+ the layer scope ends (identical to `PrismaClient.layer`).
643
+
644
+ #### Routing rules
645
+
646
+ With `PrismaReplicas` present:
647
+
648
+ | Operation | Routed to |
649
+ |---|---|
650
+ | `findUnique`, `findUniqueOrThrow`, `findFirst`, `findFirstOrThrow`, `findMany` | Random replica |
651
+ | `count`, `aggregate`, `groupBy` | Random replica |
652
+ | `create`, `createMany`, `createManyAndReturn`, `upsert` | Primary |
653
+ | `update`, `updateMany`, `updateManyAndReturn` | Primary |
654
+ | `delete`, `deleteMany` | Primary |
655
+ | `$transaction`, `$transactionWith`, `$isolatedTransaction*` | Primary |
656
+ | `$executeRaw*`, `$queryRaw*` | Primary (by default - SQL intent cannot be detected automatically) |
657
+ | Any operation inside an active `$transaction` | Transaction client (replicas ignored) |
658
+
659
+ Without `PrismaReplicas`, every operation is routed to the primary - behavior is identical
660
+ to a service constructed without replicas, so you can adopt this feature incrementally.
661
+
662
+ #### `$primary()` / `$replica()` escape hatches
663
+
664
+ Two escape hatches are available on the generated service to force routing:
665
+
666
+ - `prisma.$primary()` returns a sub-service where **all** operations (including reads)
667
+ are pinned to the primary client. Useful for read-your-own-writes scenarios or when you
668
+ need strong consistency.
669
+ - `prisma.$replica()` returns a sub-service where **all** operations (including writes
670
+ and raw SQL) are pinned to a random replica. If no replicas are configured, falls back
671
+ to the primary.
672
+
673
+ ```typescript
674
+ const program = Effect.gen(function* () {
675
+ const prisma = yield* Prisma
676
+
677
+ // Auto-routed: reads go to a random replica
678
+ const users = yield* prisma.user.findMany()
679
+
680
+ // Force the primary (e.g. after a recent write where replication lag matters)
681
+ const fresh = yield* prisma.$primary().user.findMany()
682
+
683
+ // Force a replica, even for raw SQL
684
+ const stats = yield* prisma.$replica().$queryRawUnsafe<Array<{ n: number }>>(
685
+ "SELECT count(*) as n FROM User",
686
+ )
687
+ })
688
+ ```
689
+
690
+ Sub-services returned by `$primary()` / `$replica()` implement the full `IPrismaService`
691
+ interface and themselves expose `$primary()` / `$replica()`, so chaining type-checks:
692
+
693
+ ```typescript
694
+ // These all type-check and resolve to the expected pinned sub-service:
695
+ prisma.$primary().$replica().user.findMany()
696
+ prisma.$replica().$primary().$transaction(/* ... */)
697
+ ```
698
+
699
+ #### Notes
700
+
701
+ - Transactions always execute against the primary - replicas cannot participate in a
702
+ multi-statement transaction. Inside a `$transaction` callback, every operation (reads
703
+ included) uses the transaction client.
704
+ - Raw SQL (`$executeRaw*` / `$queryRaw*`) defaults to the primary because the generator
705
+ cannot inspect the SQL to decide whether it is a read or a write. Use `$replica()` to
706
+ opt a specific raw query into replica routing.
707
+ - Replica selection is uniformly random. If you need weighted or round-robin selection,
708
+ provide your own `PrismaClient` instances via `PrismaReplicas.layerFromClients` and wrap
709
+ the selection in your own logic, or open an issue.
package/dist/index.js CHANGED
@@ -115,7 +115,7 @@ function generateRawSqlOperations(customError, enableTelemetry) {
115
115
  : `Effect.fnUntraced(${generatorFn})`;
116
116
  return `
117
117
  $executeRaw: ${wrapTelemetry("Prisma.$executeRaw", `function* (args) {
118
- const actualClient = yield* clientOrTx(client);
118
+ const actualClient = yield* writeClient(client, pin);
119
119
  return yield* Effect.tryPromise<any, ${errorType}>({
120
120
  try: () => (Array.isArray(args) ? actualClient.$executeRaw(args[0], ...args.slice(1)) : actualClient.$executeRaw(args)) as any,
121
121
  catch: (error) => mapError(error, "$executeRaw", "Prisma")
@@ -123,7 +123,7 @@ function generateRawSqlOperations(customError, enableTelemetry) {
123
123
  }`)},
124
124
 
125
125
  $executeRawUnsafe: ${wrapTelemetry("Prisma.$executeRawUnsafe", `function* (query, ...values) {
126
- const actualClient = yield* clientOrTx(client);
126
+ const actualClient = yield* writeClient(client, pin);
127
127
  return yield* Effect.tryPromise<any, ${errorType}>({
128
128
  try: () => actualClient.$executeRawUnsafe(query, ...values) as any,
129
129
  catch: (error) => mapError(error, "$executeRawUnsafe", "Prisma")
@@ -131,7 +131,7 @@ function generateRawSqlOperations(customError, enableTelemetry) {
131
131
  }`)},
132
132
 
133
133
  $queryRaw: ${wrapTelemetry("Prisma.$queryRaw", `function* (args) {
134
- const actualClient = yield* clientOrTx(client);
134
+ const actualClient = yield* writeClient(client, pin);
135
135
  return yield* Effect.tryPromise<any, ${errorType}>({
136
136
  try: () => (Array.isArray(args) ? actualClient.$queryRaw(args[0], ...args.slice(1)) : actualClient.$queryRaw(args)) as any,
137
137
  catch: (error) => mapError(error, "$queryRaw", "Prisma")
@@ -139,7 +139,7 @@ function generateRawSqlOperations(customError, enableTelemetry) {
139
139
  }`)},
140
140
 
141
141
  $queryRawUnsafe: ${wrapTelemetry("Prisma.$queryRawUnsafe", `function* (query, ...values) {
142
- const actualClient = yield* clientOrTx(client);
142
+ const actualClient = yield* writeClient(client, pin);
143
143
  return yield* Effect.tryPromise<any, ${errorType}>({
144
144
  try: () => actualClient.$queryRawUnsafe(query, ...values) as any,
145
145
  catch: (error) => mapError(error, "$queryRawUnsafe", "Prisma")
@@ -306,6 +306,17 @@ function generatePrismaInterface(models, customError, supportsManyAndReturn) {
306
306
  */
307
307
  export interface IPrismaService {
308
308
  client: BasePrismaClient
309
+ // Read-replica routing escape hatches
310
+ /**
311
+ * Return a sub-service where all operations (including reads) are pinned to the primary client.
312
+ * Useful when you need strong consistency for a specific read (e.g. read-your-own-writes).
313
+ */
314
+ $primary: () => IPrismaService
315
+ /**
316
+ * Return a sub-service where all operations (including writes and raw queries) are pinned to a random replica.
317
+ * If no replicas are configured, falls back to the primary client.
318
+ */
319
+ $replica: () => IPrismaService
309
320
  // Transaction operations
310
321
  $transaction: <R, E, A>(
311
322
  effect: EffectType<A, E, R>
@@ -352,6 +363,150 @@ ${modelInterfaces}
352
363
  function capitalize(str) {
353
364
  return str.charAt(0).toUpperCase() + str.slice(1);
354
365
  }
366
+ /**
367
+ * Generate the PrismaReplicas Context.Service class declaration.
368
+ * This is an optional service that, when provided in the layer context, causes read
369
+ * operations to be routed to a randomly selected replica instead of the primary client.
370
+ */
371
+ function generatePrismaReplicasClass() {
372
+ return `/**
373
+ * Context tag for read replica clients.
374
+ *
375
+ * When provided alongside \`PrismaClient\`, read operations (\`findUnique\`, \`findFirst\`,
376
+ * \`findMany\`, \`count\`, \`aggregate\`, \`groupBy\`, etc.) are routed to a random replica.
377
+ * Write operations, transactions, and raw SQL always use the primary client by default.
378
+ *
379
+ * Use \`prisma.$primary()\` / \`prisma.$replica()\` on the service to force routing.
380
+ *
381
+ * @example
382
+ * const AppLayer = Layer.mergeAll(
383
+ * Prisma.layer({ adapter: primaryAdapter }),
384
+ * PrismaReplicas.layer([{ adapter: replica1Adapter }, { adapter: replica2Adapter }])
385
+ * )
386
+ */
387
+ export class PrismaReplicas extends Context.Service<PrismaReplicas, ReadonlyArray<BasePrismaClient>>()("PrismaReplicas") {
388
+ /**
389
+ * Create a PrismaReplicas layer from an array of PrismaClient constructor options.
390
+ * Each replica client will be automatically disconnected when the layer scope ends.
391
+ */
392
+ static layer: (
393
+ configs: ReadonlyArray<ConstructorParameters<typeof BasePrismaClient>[0]>
394
+ ) => Layer.Layer<PrismaReplicas, never, never> = (configs) => Layer.effect(
395
+ PrismaReplicas,
396
+ Effect.gen(function* () {
397
+ const clients: ReadonlyArray<BasePrismaClient> = configs.map((c) => new BasePrismaClient(c))
398
+ yield* Effect.addFinalizer(() =>
399
+ Effect.promise(() =>
400
+ Promise.all(clients.map((c) => c.$disconnect())).then(() => undefined)
401
+ )
402
+ )
403
+ return clients
404
+ })
405
+ )
406
+
407
+ /**
408
+ * Create a PrismaReplicas layer from an Effect that yields the replica configs.
409
+ * Useful when replica configuration depends on an Effect service (e.g. config lookup).
410
+ */
411
+ static layerEffect: <R, E>(
412
+ configsEffect: EffectType<ReadonlyArray<ConstructorParameters<typeof BasePrismaClient>[0]>, E, R>
413
+ ) => Layer.Layer<PrismaReplicas, E, Exclude<R, Scope.Scope>> = (configsEffect) => Layer.effect(
414
+ PrismaReplicas,
415
+ Effect.gen(function* () {
416
+ const configs = yield* configsEffect
417
+ const clients: ReadonlyArray<BasePrismaClient> = configs.map((c) => new BasePrismaClient(c))
418
+ yield* Effect.addFinalizer(() =>
419
+ Effect.promise(() =>
420
+ Promise.all(clients.map((c) => c.$disconnect())).then(() => undefined)
421
+ )
422
+ )
423
+ return clients
424
+ })
425
+ )
426
+
427
+ /**
428
+ * Create a PrismaReplicas layer from an array of already-constructed PrismaClient instances.
429
+ * The caller is responsible for managing the lifecycle of the provided clients.
430
+ */
431
+ static layerFromClients: (
432
+ clients: ReadonlyArray<BasePrismaClient>
433
+ ) => Layer.Layer<PrismaReplicas, never, never> = (clients) => Layer.succeed(PrismaReplicas, clients)
434
+ }`;
435
+ }
436
+ /**
437
+ * Generate the read/write routing helpers.
438
+ * These helpers consult the current transaction context and the optional PrismaReplicas service
439
+ * to decide which underlying client should handle a given operation.
440
+ */
441
+ function generateRoutingHelpers() {
442
+ return `/** Pin parameter controlling replica routing for a sub-service. */
443
+ type PrismaRoutingPin = "primary" | "replica" | undefined
444
+
445
+ /** Pick a random replica from the provided array. Caller ensures non-empty. */
446
+ const pickReplica = (replicas: ReadonlyArray<BasePrismaClient>): BasePrismaClient =>
447
+ replicas[Math.floor(Math.random() * replicas.length)] as BasePrismaClient
448
+
449
+ /**
450
+ * Resolve the client to use for a READ operation.
451
+ *
452
+ * Priority:
453
+ * 1. Transaction client (if inside \`$transaction\`)
454
+ * 2. Primary client (if pin === "primary")
455
+ * 3. Random replica (if replicas are configured and non-empty)
456
+ * 4. Primary client (fallback)
457
+ */
458
+ const readClient = (client: BasePrismaClient, pin: PrismaRoutingPin) => Effect.gen(function* () {
459
+ const tx = yield* Effect.serviceOption(PrismaTransactionClientService)
460
+ if (Option.isSome(tx)) return tx.value as BasePrismaClient
461
+ if (pin === "primary") return client
462
+ const replicas = yield* Effect.serviceOption(PrismaReplicas)
463
+ if (Option.isNone(replicas) || replicas.value.length === 0) return client
464
+ return pickReplica(replicas.value)
465
+ })
466
+
467
+ /**
468
+ * Resolve the client to use for a WRITE or raw SQL operation.
469
+ *
470
+ * Priority:
471
+ * 1. Transaction client (if inside \`$transaction\`)
472
+ * 2. Random replica (only if pin === "replica" AND replicas are configured)
473
+ * 3. Primary client (default - writes/raw always go here unless explicitly pinned to replica)
474
+ */
475
+ const writeClient = (client: BasePrismaClient, pin: PrismaRoutingPin) => Effect.gen(function* () {
476
+ const tx = yield* Effect.serviceOption(PrismaTransactionClientService)
477
+ if (Option.isSome(tx)) return tx.value as BasePrismaClient
478
+ if (pin === "replica") {
479
+ const replicas = yield* Effect.serviceOption(PrismaReplicas)
480
+ if (Option.isSome(replicas) && replicas.value.length > 0) {
481
+ return pickReplica(replicas.value)
482
+ }
483
+ }
484
+ return client
485
+ })`;
486
+ }
487
+ /**
488
+ * Wrap a service body string in the buildService(pin) closure that also constructs
489
+ * the primary and replica sub-services. This is shared between both error-service variants.
490
+ *
491
+ * The generated scaffolding:
492
+ * 1. Declares \`primaryService\` / \`replicaService\` with definite-assignment assertions.
493
+ * 2. Defines \`buildService(pin)\` - captures \`pin\` so every read/write helper call
494
+ * resolves to the correct client at runtime.
495
+ * 3. Builds the primary and replica sub-services first, then returns the main service
496
+ * with pin=undefined (auto-routing).
497
+ */
498
+ function wrapBuildService(body) {
499
+ return `let primaryService!: IPrismaService;
500
+ let replicaService!: IPrismaService;
501
+
502
+ const buildService = (pin: PrismaRoutingPin): IPrismaService => ({
503
+ ${body}
504
+ });
505
+
506
+ primaryService = buildService("primary");
507
+ replicaService = buildService("replica");
508
+ return buildService(undefined);`;
509
+ }
355
510
  function generateModelOperations(models, customError, enableTelemetry, supportsManyAndReturn) {
356
511
  return models
357
512
  .map((model) => {
@@ -383,7 +538,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
383
538
  // Implementation without explicit types - the IPrismaService interface provides all type checking
384
539
  return ` ${modelNameCamel}: {
385
540
  findUnique: ${wrapTelemetry(`Prisma.${modelNameCamel}.findUnique`, `function* (args) {
386
- const actualClient = yield* clientOrTx(client);
541
+ const actualClient = yield* readClient(client, pin);
387
542
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindError")}>({
388
543
  try: () => actualClient.${modelNameCamel}.findUnique(args as any)${promiseCast()},
389
544
  catch: (error) => ${mapperFn("mapFindError")}(error, "findUnique", "${modelName}")
@@ -391,7 +546,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
391
546
  }`)},
392
547
 
393
548
  findUniqueOrThrow: ${wrapTelemetry(`Prisma.${modelNameCamel}.findUniqueOrThrow`, `function* (args) {
394
- const actualClient = yield* clientOrTx(client);
549
+ const actualClient = yield* readClient(client, pin);
395
550
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindOrThrowError")}>({
396
551
  try: () => actualClient.${modelNameCamel}.findUniqueOrThrow(args as any)${promiseCast()},
397
552
  catch: (error) => ${mapperFn("mapFindOrThrowError")}(error, "findUniqueOrThrow", "${modelName}")
@@ -399,7 +554,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
399
554
  }`)},
400
555
 
401
556
  findFirst: ${wrapTelemetry(`Prisma.${modelNameCamel}.findFirst`, `function* (args) {
402
- const actualClient = yield* clientOrTx(client);
557
+ const actualClient = yield* readClient(client, pin);
403
558
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindError")}>({
404
559
  try: () => actualClient.${modelNameCamel}.findFirst(args as any)${promiseCast()},
405
560
  catch: (error) => ${mapperFn("mapFindError")}(error, "findFirst", "${modelName}")
@@ -407,7 +562,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
407
562
  }`)},
408
563
 
409
564
  findFirstOrThrow: ${wrapTelemetry(`Prisma.${modelNameCamel}.findFirstOrThrow`, `function* (args) {
410
- const actualClient = yield* clientOrTx(client);
565
+ const actualClient = yield* readClient(client, pin);
411
566
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindOrThrowError")}>({
412
567
  try: () => actualClient.${modelNameCamel}.findFirstOrThrow(args as any)${promiseCast()},
413
568
  catch: (error) => ${mapperFn("mapFindOrThrowError")}(error, "findFirstOrThrow", "${modelName}")
@@ -415,7 +570,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
415
570
  }`)},
416
571
 
417
572
  findMany: ${wrapTelemetry(`Prisma.${modelNameCamel}.findMany`, `function* (args) {
418
- const actualClient = yield* clientOrTx(client);
573
+ const actualClient = yield* readClient(client, pin);
419
574
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindError")}>({
420
575
  try: () => actualClient.${modelNameCamel}.findMany(args as any)${promiseCast()},
421
576
  catch: (error) => ${mapperFn("mapFindError")}(error, "findMany", "${modelName}")
@@ -423,7 +578,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
423
578
  }`)},
424
579
 
425
580
  create: ${wrapTelemetry(`Prisma.${modelNameCamel}.create`, `function* (args) {
426
- const actualClient = yield* clientOrTx(client);
581
+ const actualClient = yield* writeClient(client, pin);
427
582
  return yield* Effect.tryPromise<any, ${errorType("PrismaCreateError")}>({
428
583
  try: () => actualClient.${modelNameCamel}.create(args as any)${promiseCast()},
429
584
  catch: (error) => ${mapperFn("mapCreateError")}(error, "create", "${modelName}")
@@ -431,7 +586,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
431
586
  }`)},
432
587
 
433
588
  createMany: ${wrapTelemetry(`Prisma.${modelNameCamel}.createMany`, `function* (args) {
434
- const actualClient = yield* clientOrTx(client);
589
+ const actualClient = yield* writeClient(client, pin);
435
590
  return yield* Effect.tryPromise<any, ${errorType("PrismaCreateError")}>({
436
591
  try: () => actualClient.${modelNameCamel}.createMany(args as any),
437
592
  catch: (error) => ${mapperFn("mapCreateError")}(error, "createMany", "${modelName}")
@@ -440,7 +595,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
440
595
  ? `
441
596
 
442
597
  createManyAndReturn: ${wrapTelemetry(`Prisma.${modelNameCamel}.createManyAndReturn`, `function* (args) {
443
- const actualClient = yield* clientOrTx(client);
598
+ const actualClient = yield* writeClient(client, pin);
444
599
  return yield* Effect.tryPromise<any, ${errorType("PrismaCreateError")}>({
445
600
  try: () => actualClient.${modelNameCamel}.createManyAndReturn(args as any)${promiseCast()},
446
601
  catch: (error) => ${mapperFn("mapCreateError")}(error, "createManyAndReturn", "${modelName}")
@@ -449,7 +604,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
449
604
  : ""}
450
605
 
451
606
  delete: ${wrapTelemetry(`Prisma.${modelNameCamel}.delete`, `function* (args) {
452
- const actualClient = yield* clientOrTx(client);
607
+ const actualClient = yield* writeClient(client, pin);
453
608
  return yield* Effect.tryPromise<any, ${errorType("PrismaDeleteError")}>({
454
609
  try: () => actualClient.${modelNameCamel}.delete(args as any)${promiseCast()},
455
610
  catch: (error) => ${mapperFn("mapDeleteError")}(error, "delete", "${modelName}")
@@ -457,7 +612,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
457
612
  }`)},
458
613
 
459
614
  update: ${wrapTelemetry(`Prisma.${modelNameCamel}.update`, `function* (args) {
460
- const actualClient = yield* clientOrTx(client);
615
+ const actualClient = yield* writeClient(client, pin);
461
616
  return yield* Effect.tryPromise<any, ${errorType("PrismaUpdateError")}>({
462
617
  try: () => actualClient.${modelNameCamel}.update(args as any)${promiseCast()},
463
618
  catch: (error) => ${mapperFn("mapUpdateError")}(error, "update", "${modelName}")
@@ -465,7 +620,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
465
620
  }`)},
466
621
 
467
622
  deleteMany: ${wrapTelemetry(`Prisma.${modelNameCamel}.deleteMany`, `function* (args) {
468
- const actualClient = yield* clientOrTx(client);
623
+ const actualClient = yield* writeClient(client, pin);
469
624
  return yield* Effect.tryPromise<any, ${errorType("PrismaDeleteManyError")}>({
470
625
  try: () => actualClient.${modelNameCamel}.deleteMany(args as any),
471
626
  catch: (error) => ${mapperFn("mapDeleteManyError")}(error, "deleteMany", "${modelName}")
@@ -473,7 +628,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
473
628
  }`)},
474
629
 
475
630
  updateMany: ${wrapTelemetry(`Prisma.${modelNameCamel}.updateMany`, `function* (args) {
476
- const actualClient = yield* clientOrTx(client);
631
+ const actualClient = yield* writeClient(client, pin);
477
632
  return yield* Effect.tryPromise<any, ${errorType("PrismaUpdateManyError")}>({
478
633
  try: () => actualClient.${modelNameCamel}.updateMany(args as any),
479
634
  catch: (error) => ${mapperFn("mapUpdateManyError")}(error, "updateMany", "${modelName}")
@@ -482,7 +637,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
482
637
  ? `
483
638
 
484
639
  updateManyAndReturn: ${wrapTelemetry(`Prisma.${modelNameCamel}.updateManyAndReturn`, `function* (args) {
485
- const actualClient = yield* clientOrTx(client);
640
+ const actualClient = yield* writeClient(client, pin);
486
641
  return yield* Effect.tryPromise<any, ${errorType("PrismaUpdateManyError")}>({
487
642
  try: () => actualClient.${modelNameCamel}.updateManyAndReturn(args as any)${promiseCast()},
488
643
  catch: (error) => ${mapperFn("mapUpdateManyError")}(error, "updateManyAndReturn", "${modelName}")
@@ -491,7 +646,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
491
646
  : ""}
492
647
 
493
648
  upsert: ${wrapTelemetry(`Prisma.${modelNameCamel}.upsert`, `function* (args) {
494
- const actualClient = yield* clientOrTx(client);
649
+ const actualClient = yield* writeClient(client, pin);
495
650
  return yield* Effect.tryPromise<any, ${errorType("PrismaCreateError")}>({
496
651
  try: () => actualClient.${modelNameCamel}.upsert(args as any)${promiseCast()},
497
652
  catch: (error) => ${mapperFn("mapCreateError")}(error, "upsert", "${modelName}")
@@ -500,7 +655,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
500
655
 
501
656
  // Aggregation operations
502
657
  count: ${wrapTelemetry(`Prisma.${modelNameCamel}.count`, `function* (args) {
503
- const actualClient = yield* clientOrTx(client);
658
+ const actualClient = yield* readClient(client, pin);
504
659
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindError")}>({
505
660
  try: () => actualClient.${modelNameCamel}.count(args as any)${promiseCast()},
506
661
  catch: (error) => ${mapperFn("mapFindError")}(error, "count", "${modelName}")
@@ -508,7 +663,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
508
663
  }`)},
509
664
 
510
665
  aggregate: ${wrapTelemetry(`Prisma.${modelNameCamel}.aggregate`, `function* (args) {
511
- const actualClient = yield* clientOrTx(client);
666
+ const actualClient = yield* readClient(client, pin);
512
667
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindError")}>({
513
668
  try: () => actualClient.${modelNameCamel}.aggregate(args as any)${strongPromiseCast()},
514
669
  catch: (error) => ${mapperFn("mapFindError")}(error, "aggregate", "${modelName}")
@@ -516,7 +671,7 @@ function generateModelOperations(models, customError, enableTelemetry, supportsM
516
671
  }`)},
517
672
 
518
673
  groupBy: ${wrapTelemetry(`Prisma.${modelNameCamel}.groupBy`, `function* (args) {
519
- const actualClient = yield* clientOrTx(client);
674
+ const actualClient = yield* readClient(client, pin);
520
675
  return yield* Effect.tryPromise<any, ${errorType("PrismaFindError")}>({
521
676
  try: () => actualClient.${modelNameCamel}.groupBy(args as any)${strongPromiseCast()},
522
677
  catch: (error) => ${mapperFn("mapFindError")}(error, "groupBy", "${modelName}")
@@ -681,20 +836,15 @@ export class PrismaClient extends Context.Service<PrismaClient, BasePrismaClient
681
836
  */
682
837
  export class PrismaTransactionClientService extends Context.Service<PrismaTransactionClientService, PrismaNamespace.TransactionClient>()("PrismaTransactionClientService") {}
683
838
 
839
+ ${generatePrismaReplicasClass()}
840
+
684
841
  // Re-export the custom error type for convenience
685
842
  export { ${customError.className} }
686
843
 
687
844
  // Use the user-provided error mapper
688
845
  const mapError = mapPrismaError
689
846
 
690
- /**
691
- * Helper to get the current client - either the transaction client if in a transaction,
692
- * or the root client if not. Uses Effect.serviceOption to detect transaction context.
693
- */
694
- const clientOrTx = (client: BasePrismaClient) => Effect.map(
695
- Effect.serviceOption(PrismaTransactionClientService),
696
- Option.getOrElse(() => client),
697
- );
847
+ ${generateRoutingHelpers()}
698
848
 
699
849
  /**
700
850
  * Like Effect.acquireUseRelease, but allows the release function to fail.
@@ -804,8 +954,13 @@ const $begin = (
804
954
  const makePrismaService = Effect.gen(function* () {
805
955
  const client = yield* PrismaClient;
806
956
 
807
- const prismaService: IPrismaService = {
957
+ let primaryService!: IPrismaService;
958
+ let replicaService!: IPrismaService;
959
+
960
+ const buildService = (pin: PrismaRoutingPin): IPrismaService => ({
808
961
  client,
962
+ $primary: () => primaryService,
963
+ $replica: () => replicaService,
809
964
  /**
810
965
  * Execute an effect within a database transaction.
811
966
  * All operations within the effect will be atomic - they either all succeed or all fail.
@@ -1018,9 +1173,11 @@ const makePrismaService = Effect.gen(function* () {
1018
1173
  ${rawSqlOperations}
1019
1174
 
1020
1175
  ${modelOperations}
1021
- };
1176
+ });
1022
1177
 
1023
- return prismaService;
1178
+ primaryService = buildService("primary");
1179
+ replicaService = buildService("replica");
1180
+ return buildService(undefined);
1024
1181
  });
1025
1182
 
1026
1183
  export class Prisma extends Context.Service<Prisma, IPrismaService>()("Prisma") {
@@ -1221,6 +1378,8 @@ export class PrismaClient extends Context.Service<PrismaClient, BasePrismaClient
1221
1378
  */
1222
1379
  export class PrismaTransactionClientService extends Context.Service<PrismaTransactionClientService, PrismaNamespace.TransactionClient>()("PrismaTransactionClientService") {}
1223
1380
 
1381
+ ${generatePrismaReplicasClass()}
1382
+
1224
1383
  export class PrismaUniqueConstraintError extends Data.TaggedError("PrismaUniqueConstraintError")<{
1225
1384
  cause: PrismaNamespace.PrismaClientKnownRequestError
1226
1385
  operation: string
@@ -1572,14 +1731,7 @@ const mapUpdateManyError = (error: unknown, operation: string, model: string): P
1572
1731
  throw error;
1573
1732
  }
1574
1733
 
1575
- /**
1576
- * Helper to get the current client - either the transaction client if in a transaction,
1577
- * or the root client if not. Uses Effect.serviceOption to detect transaction context.
1578
- */
1579
- const clientOrTx = (client: BasePrismaClient) => Effect.map(
1580
- Effect.serviceOption(PrismaTransactionClientService),
1581
- Option.getOrElse(() => client),
1582
- );
1734
+ ${generateRoutingHelpers()}
1583
1735
 
1584
1736
  /**
1585
1737
  * Like Effect.acquireUseRelease, but allows the release function to fail.
@@ -1689,8 +1841,13 @@ const $begin = (
1689
1841
  const makePrismaService = Effect.gen(function* () {
1690
1842
  const client = yield* PrismaClient;
1691
1843
 
1692
- const prismaService: IPrismaService = {
1844
+ let primaryService!: IPrismaService;
1845
+ let replicaService!: IPrismaService;
1846
+
1847
+ const buildService = (pin: PrismaRoutingPin): IPrismaService => ({
1693
1848
  client,
1849
+ $primary: () => primaryService,
1850
+ $replica: () => replicaService,
1694
1851
  /**
1695
1852
  * Execute an effect within a database transaction.
1696
1853
  * All operations within the effect will be atomic - they either all succeed or all fail.
@@ -1902,9 +2059,11 @@ const makePrismaService = Effect.gen(function* () {
1902
2059
  ${rawSqlOperations}
1903
2060
 
1904
2061
  ${modelOperations}
1905
- };
2062
+ });
1906
2063
 
1907
- return prismaService;
2064
+ primaryService = buildService("primary");
2065
+ replicaService = buildService("replica");
2066
+ return buildService(undefined);
1908
2067
  });
1909
2068
 
1910
2069
  export class Prisma extends Context.Service<Prisma, IPrismaService>()("Prisma") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prisma-generator-effect",
3
- "version": "1.1.2-beta.1",
3
+ "version": "1.1.3-beta.1",
4
4
  "description": "Prisma generator for Effect",
5
5
  "main": "dist/index.js",
6
6
  "bin": {