prisma-generator-express 1.62.4 → 1.63.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
@@ -12,7 +12,7 @@ Running `npx prisma generate` produces:
12
12
  - Handler functions for all Prisma operations (findMany, create, update, delete, etc.)
13
13
  - Schema-level `findManyPaginated` execution mode selection (`Promise.all` or interactive transaction)
14
14
  - Per-route and per-endpoint pagination config, including optional materialized-view count sources
15
- - Router generator with middleware support (before/after hooks per operation)
15
+ - Router generator with operation-wide and per-variant before/after hooks
16
16
  - POST read endpoints for all read operations (for complex queries exceeding URL length limits)
17
17
  - Express-only progressive read streaming over Server-Sent Events (SSE), using manual stages or auto-include splitting for supported relation reads, including unguarded deep `findMany` / `findManyPaginated` auto-include paths and guarded single-record auto-include paths
18
18
  - Express-only standalone materialized view router for read-only access to registered PostgreSQL materialized views
@@ -503,13 +503,27 @@ app.route('/', UserRouter(userConfig))
503
503
 
504
504
  Hono route hooks return `void` to continue. Return a `Response` (e.g. `c.json({...}, 403)`) or throw `HTTPException` to short-circuit — subsequent hooks and the handler will not run.
505
505
 
506
+ ### Hook execution contract
507
+
508
+ Operation-level hooks run for every successfully routed variant. Variant hooks run only for the selected declared variant key.
509
+
510
+ ```text
511
+ operation before-hooks
512
+ variant before-hooks
513
+ generated handler
514
+ variant after-hooks
515
+ operation after-hooks
516
+ ```
517
+
518
+ A terminal response or an error stops the remaining hooks in that phase. Operation after-hooks are not cleanup or `finally` handlers and are not guaranteed to run. Caller-routing failures are surfaced after operation before-hooks and before variant before-hooks, preserving operation-wide logging and authentication timing.
519
+
506
520
  Only operations listed in the config (or all when `enableAll: true`) are registered. Operations not listed produce no routes.
507
521
 
508
522
  ## Guard shapes (prisma-guard integration)
509
523
 
510
- prisma-generator-express integrates with [prisma-guard](https://github.com/multipliedtwice/prisma-guard) to enforce input validation, query shape restrictions, and tenant isolation on generated routes. When a `shape` is configured on an operation, the handler calls `prisma.model.guard(shape, caller).method(args)` instead of `prisma.model.method(args)`.
524
+ prisma-generator-express integrates with [prisma-guard](https://github.com/multipliedtwice/prisma-guard) to enforce input validation, query shape restrictions, and tenant isolation on generated routes. Configure either `shape` for the existing guard API or `variants` to co-locate named shapes with per-variant hooks. The handler passes the extracted guard shape map to `prisma.model.guard(shape, caller).method(args)` instead of calling Prisma directly.
511
525
 
512
- Guard shapes work identically across all three targets. The only difference is the type of the `resolveVariant` callback parameter (`Request` for Express, `FastifyRequest` for Fastify, `Context` for Hono).
526
+ Guard routing works identically across Express, Fastify, and Hono. The only target-specific differences are hook types and the type of the `resolveVariant` callback parameter (`Request` for Express, `FastifyRequest` for Fastify, `Context` for Hono).
513
527
 
514
528
  ### Guard setup
515
529
 
@@ -579,71 +593,81 @@ If prisma-guard is not installed or the client is not extended with the guard ex
579
593
 
580
594
  ### How guard integration works
581
595
 
582
- Each operation config accepts an optional `shape` property. When present, the generated handler:
596
+ Each operation accepts either the existing `shape` property or the new `variants` property:
583
597
 
584
- 1. Stores the shape on the request context via middleware (Express: `res.locals.guardShape = shape`, Fastify: `request.guardShape = shape`, Hono: `c.set('guardShape', shape)`)
585
- 2. Resolves the caller from `config.guard.resolveVariant(req)`, then from the configured header (default `x-api-variant`), falling back to `undefined`
586
- 3. Calls `prisma.model.guard(shape, caller).method(args)` instead of `prisma.model.method(args)`
598
+ - `shape` accepts one direct shape, one context-dependent shape function, or a named shape map.
599
+ - `variants` is a named map whose entries contain one shape plus optional per-variant hooks.
600
+ - `shape` and `variants` cannot both be defined on the same operation.
601
+ - Both may be absent when the operation only needs operation-wide hooks or pagination.
587
602
 
588
- The downstream handler reads these values (`res.locals.guardShape`, `request.guardShape`, `c.get('guardShape')`) when constructing the Prisma call.
603
+ At router construction, the generated router validates and normalizes the descriptor. Variant descriptors are removed before anything reaches prisma-guard, dropped-guard projection logic, progressive planning, or OpenAPI extraction.
589
604
 
590
- When `shape` is absent, the handler calls Prisma directly with no guard enforcement.
605
+ For each request, the generated router:
591
606
 
592
- For Express auto-include SSE on supported single-record reads, the router can also resolve the guard shape before planning the stream. It still executes the root query and every relation-stage query through prisma-guard. See [Guarded auto-include mode](#guarded-auto-include-mode).
607
+ 1. Resolves the raw caller from `config.guard.resolveVariant(request)`, then from the configured header (default `x-api-variant`), falling back to `undefined`.
608
+ 2. Resolves the declared guard variant key using the same exact/default/parameterized precedence as prisma-guard.
609
+ 3. Applies dropped-guard projection defaults immediately for successful routing when guard is globally dropped, preserving what operation before-hooks observe.
610
+ 4. Runs operation before-hooks.
611
+ 5. Converts a stored routing failure into HTTP 400 before any variant hook or generated handler runs.
612
+ 6. Runs hooks for the matched declared key, calls the generated handler, then runs after-hooks in reverse scope order.
593
613
 
594
- Generated route config types treat `shape` as a named shape map. Use `default` for the normal single-shape case, and add other keys only when you need caller-based variants. The runtime still passes the map to `prisma-guard`; the `default` variant is selected when no caller is provided or no variant matches.
614
+ The normalized guard shape is stored on request context (`res.locals.guardShape`, `request.guardShape`, or `c.get('guardShape')`). The declared matched key is stored separately from the raw caller. For example, caller `customer/123` matching `customer/:id` stores `customer/:id` as the variant key.
595
615
 
596
- ### Default shape per operation
616
+ When neither `shape` nor `variants` is configured, the generated handler calls Prisma directly with no guard enforcement.
597
617
 
598
- In generated route configs, `shape` is always a named shape map. Use the `default` key when an operation has one normal shape and no caller-specific variants.
618
+ ### Default shape per operation
599
619
 
600
- `default` is used when no caller is provided or when the caller does not match a named variant. If you do not want fallback behavior, omit `default` and define only explicit variants.
620
+ For one normal shape, use a direct shape object:
601
621
 
602
622
  ```ts
603
623
  const userConfig = {
604
624
  findMany: {
605
625
  shape: {
606
- default: {
607
- where: { email: { contains: true }, role: { equals: true } },
608
- orderBy: { createdAt: true },
609
- take: { max: 100, default: 25 },
610
- skip: true,
611
- },
626
+ where: { email: { contains: true }, role: { equals: true } },
627
+ orderBy: { createdAt: true },
628
+ take: { max: 100, default: 25 },
629
+ skip: true,
612
630
  },
613
631
  },
614
- create: {
615
- shape: {
616
- default: {
617
- data: { email: true, name: true, role: 'user' },
632
+ }
633
+ ```
634
+
635
+ A context-dependent shape function is also valid:
636
+
637
+ ```ts
638
+ const userConfig = {
639
+ findMany: {
640
+ shape: (ctx) => ({
641
+ where: {
642
+ companyId: { equals: ctx.companyId },
643
+ name: { contains: true },
618
644
  },
619
- },
645
+ take: { max: 50 },
646
+ }),
620
647
  },
621
- update: {
648
+ }
649
+ ```
650
+
651
+ Use a named shape map when only shape routing differs and no per-variant route hooks are needed:
652
+
653
+ ```ts
654
+ const userConfig = {
655
+ findMany: {
622
656
  shape: {
623
- default: {
624
- data: { name: true },
625
- where: { id: { equals: true } },
657
+ admin: {
658
+ where: { email: { contains: true }, role: { equals: true } },
659
+ take: { max: 100 },
626
660
  },
627
- },
628
- },
629
- delete: {
630
- shape: {
631
661
  default: {
632
- where: { id: { equals: true } },
662
+ where: { name: { contains: true } },
663
+ take: { max: 20, default: 10 },
633
664
  },
634
665
  },
635
666
  },
636
667
  }
637
-
638
- app.use('/', UserRouter(userConfig))
639
668
  ```
640
669
 
641
- In this example:
642
-
643
- - `findMany` allows filtering by `email` (contains) and `role` (equals), sorting by `createdAt`, pagination via `take`/`skip`. All other where fields, orderBy fields, and include/select are rejected.
644
- - `create` accepts `email` and `name` from the client. `role` is forced to `'user'` regardless of what the client sends.
645
- - `update` only allows changing `name`, and requires a unique `id` in `where`.
646
- - `delete` requires a unique `id` in `where`.
670
+ `default` is selected when the caller is missing, blank, or unmatched. Without `default`, the request returns 400. Use `variants` instead when hooks must differ by the matched shape.
647
671
 
648
672
  ### Shape value types in data
649
673
 
@@ -728,6 +752,62 @@ fetch('/user', {
728
752
 
729
753
  If the caller is missing or doesn't match any key, the request is rejected with 400 (`CallerError`).
730
754
 
755
+ ### Variant-specific hooks
756
+
757
+ Use `variants` to keep each named shape beside the hooks that belong to it:
758
+
759
+ ```ts
760
+ const orderConfig = {
761
+ create: {
762
+ before: [authenticate],
763
+ after: [auditOrderCreation],
764
+
765
+ variants: {
766
+ customer: {
767
+ shape: customerCreateShape,
768
+ before: [bindCustomer],
769
+ after: [sendCustomerConfirmation],
770
+ },
771
+
772
+ seller: {
773
+ shape: sellerCreateShape,
774
+ before: [bindSeller],
775
+ },
776
+
777
+ default: {
778
+ shape: internalCreateShape,
779
+ },
780
+ },
781
+ },
782
+
783
+ guard: {
784
+ resolveVariant: (req) => req.user?.role,
785
+ },
786
+ }
787
+ ```
788
+
789
+ The execution order for `customer` is:
790
+
791
+ ```text
792
+ authenticate
793
+ bindCustomer
794
+ generated create handler
795
+ sendCustomerConfirmation
796
+ auditOrderCreation
797
+ ```
798
+
799
+ Important rules:
800
+
801
+ - Operation-level `before` and `after` hooks run for every successfully resolved variant.
802
+ - Variant hooks run only for the selected declared key.
803
+ - `variants[key].shape` is one direct operation shape or one context-dependent shape function. It cannot contain another named map.
804
+ - `shape` and `variants` are mutually exclusive on one operation, but both may be absent.
805
+ - `default` is allowed as a variant key and receives its own hooks when fallback selects it.
806
+ - Reserved guard shape keys such as `where`, `data`, `include`, and `select` cannot be variant names.
807
+ - OpenAPI generation extracts only the shapes; hook descriptors are never exposed as guard configuration.
808
+
809
+ `updateEach` does not support `variants`; it keeps operation-wide hooks only.
810
+
731
811
  ### Custom caller resolution
732
812
 
733
813
  Use `resolveVariant` for caller logic beyond a simple header. The callback parameter type depends on the target.
@@ -798,14 +878,20 @@ Caller keys support parameterized path patterns:
798
878
  ```ts
799
879
  const projectConfig = {
800
880
  update: {
801
- shape: {
881
+ variants: {
802
882
  '/admin/projects/:id': {
803
- data: { title: true, status: true, priority: true },
804
- where: { id: { equals: true } },
883
+ shape: {
884
+ data: { title: true, status: true, priority: true },
885
+ where: { id: true },
886
+ },
887
+ before: [requireAdmin],
805
888
  },
806
889
  '/editor/projects/:id': {
807
- data: { title: true },
808
- where: { id: { equals: true } },
890
+ shape: {
891
+ data: { title: true },
892
+ where: { id: true },
893
+ },
894
+ before: [requireEditor],
809
895
  },
810
896
  },
811
897
  },
@@ -815,7 +901,7 @@ const projectConfig = {
815
901
  }
816
902
  ```
817
903
 
818
- The client sends the full path:
904
+ The client sends the concrete caller:
819
905
 
820
906
  ```ts
821
907
  fetch('/project', {
@@ -825,13 +911,39 @@ fetch('/project', {
825
911
  'Content-Type': 'application/json',
826
912
  },
827
913
  body: JSON.stringify({
828
- where: { id: { equals: 'abc123' } },
914
+ where: { id: 'abc123' },
829
915
  data: { title: 'Updated', status: 'active' },
830
916
  }),
831
917
  })
832
918
  ```
833
919
 
834
- Exact matches are checked first. Parameters (`:id`) are routing-only and are not extracted.
920
+ Exact non-blank keys are checked first. Parameter segments match one path segment and are not extracted. Blank and whitespace-only callers do not exact-match or pattern-match; they use `default` or return 400.
921
+
922
+ The router stores the declared matched key, not the concrete caller. In this example:
923
+
924
+ ```text
925
+ raw caller: /admin/projects/abc123
926
+ matched key: /admin/projects/:id
927
+ selected hooks: variants['/admin/projects/:id']
928
+ ```
929
+
930
+ Express progressive configuration should also use the declared matched key:
931
+
932
+ ```ts
933
+ update: {
934
+ variants: {
935
+ '/admin/projects/:id': { shape: adminShape },
936
+ },
937
+ progressive: {
938
+ '/admin/projects/:id': {
939
+ enabled: true,
940
+ mode: 'autoInclude',
941
+ },
942
+ },
943
+ }
944
+ ```
945
+
946
+ For backward compatibility, a concrete raw-caller progressive key may be used only when no declared-key entry exists. That fallback emits a deduplicated deprecation warning identifying the declared key to use. Declared-key configuration always takes precedence.
835
947
 
836
948
  ### Forced where conditions
837
949
 
@@ -1181,7 +1293,11 @@ Guard errors are mapped to HTTP status codes by the generated error handler:
1181
1293
  | `CallerError` | 400 | Missing/unknown/ambiguous caller, caller in body |
1182
1294
  | `PolicyError` | 403 | Scope denied, missing tenant context, rejected findUnique |
1183
1295
 
1184
- All errors return `{ "message": "..." }` in the response body.
1296
+ Generated caller routing preserves the same 400 messages for existing named `shape` maps. Missing, unknown, ambiguous, and reserved-key routing failures are stored during shape preparation, operation before-hooks run, and the failure is then surfaced before variant hooks and the handler.
1297
+
1298
+ The new `variants` descriptor is validated when the router is constructed. Invalid configuration such as an empty variants map, a missing entry shape, a reserved variant name, or defining both `shape` and `variants` throws immediately instead of registering the route.
1299
+
1300
+ All request errors return `{ "message": "..." }` in the response body.
1185
1301
 
1186
1302
  ### Complete guard example
1187
1303
 
@@ -2343,17 +2459,26 @@ Auto-include does not require `resolveContext` or `progressiveStages`.
2343
2459
 
2344
2460
  ### Hooks and guard behavior
2345
2461
 
2346
- For SSE requests:
2462
+ For SSE requests, routing and before-hooks run in the same order as JSON requests:
2463
+
2464
+ ```text
2465
+ operation before-hooks
2466
+ variant before-hooks
2467
+ progressive SSE middleware
2468
+ ```
2469
+
2470
+ If the progressive middleware starts or completes an SSE response, it does not continue to the normal generated handler. Consequently, variant after-hooks and operation after-hooks do not run for that request. They are not cleanup handlers.
2471
+
2472
+ Additional behavior:
2347
2473
 
2348
- - `before` hooks run before streaming starts
2349
- - `after` hooks do not run, because the SSE middleware handles the response and does not continue to the normal handler
2350
2474
  - manual progressive stages receive `req.prisma` directly
2351
2475
  - manual progressive stages do not automatically use guard shapes
2352
- - auto-include mode does not run when an operation has a guard `shape`; it falls back to single-result SSE or emits an SSE error depending on `fallback`
2476
+ - auto-include mode does not run when an operation has a guard shape; it falls back to single-result SSE or emits an SSE error depending on `fallback`
2477
+ - variants without progressive config use the single-result SSE fallback through the normal generated core read handler, so guard shape behavior matches the JSON endpoint
2353
2478
 
2354
- Manual stage authors are responsible for using the resolved context and enforcing ownership or tenant constraints in their stage queries.
2479
+ Progressive lookup uses the declared matched variant key. A caller such as `/user/123` matched by `/user/:id` should configure `progressive['/user/:id']`. If only a legacy concrete raw-caller key exists, the router may use it as a compatibility fallback and emits a deduplicated deprecation warning.
2355
2480
 
2356
- For variants without progressive config, the single-result SSE fallback uses the normal generated core read handler, so guard shape behavior matches the JSON endpoint.
2481
+ Manual stage authors are responsible for using the resolved context and enforcing ownership or tenant constraints in their stage queries.
2357
2482
 
2358
2483
  ### Client-side usage
2359
2484
 
@@ -2523,7 +2648,7 @@ app.use('/', UserRouter({
2523
2648
  }))
2524
2649
  ```
2525
2650
 
2526
- Only `before` and `after` hooks are configurable. It has no `shape`, `pagination`, or progressive config. In development, enabling `updateEach` without a `before` hook may print a warning because this route bypasses guard shapes and should be protected explicitly.
2651
+ Only operation-wide `before` and `after` hooks are configurable. It has no `shape`, `variants`, pagination, or progressive config. In development, enabling `updateEach` without a `before` hook may print a warning because this route bypasses guard shapes and should be protected explicitly.
2527
2652
 
2528
2653
  When enabled, `updateEach` is included in generated OpenAPI output as `POST /{modelname}/each`. It remains excluded from `enableAll: true`.
2529
2654
 
@@ -2531,7 +2656,7 @@ When enabled, `updateEach` is included in generated OpenAPI output as `POST /{mo
2531
2656
 
2532
2657
  `updateEach` does **not** apply prisma-guard shapes on any target, by design. It is intended as a trusted internal batch path, for example worker-to-backend bulk updates, not a public endpoint. Because it bypasses guard, it can write any field the underlying `update` allows.
2533
2658
 
2534
- Caller resolution still runs before hooks on all three targets, so a `before` hook can read the resolved caller for its own authorization logic. This is separate from guard shapes and does not enable guard enforcement for `updateEach`.
2659
+ Raw caller resolution still runs before hooks on all three targets, so a `before` hook can read the caller for its own authorization logic. No variant key is selected and no variant hook gate runs for `updateEach`. This is separate from guard shapes and does not enable guard enforcement.
2535
2660
 
2536
2661
  Protect it yourself with route middleware (`before`) enforcing authentication or network-level restrictions. A guard `variantHeader` such as `x-api-variant` is **not** a security boundary — it only selects a caller value for hooks to read and is trivially spoofable. Do not expose `/each` to untrusted callers.
2537
2662
 
@@ -3055,14 +3180,39 @@ interface RouteConfig<TCtx = unknown> {
3055
3180
  updateEach?: UpdateEachConfig
3056
3181
  }
3057
3182
 
3058
- interface OperationConfig {
3183
+ type ShapeOrFn<TShape, TCtx> = TShape | ((ctx: TCtx) => TShape)
3184
+
3185
+ type VariantEntry<TShape, TCtx, TBefore, TAfter = TBefore> = {
3186
+ shape: ShapeOrFn<TShape, TCtx>
3187
+ before?: TBefore[]
3188
+ after?: TAfter[]
3189
+ }
3190
+
3191
+ type OperationConfig<
3192
+ TShape = Record<string, any>,
3193
+ TCtx = unknown,
3194
+ > = {
3059
3195
  before?: RequestHandler[]
3060
3196
  after?: RequestHandler[]
3061
- shape?: Record<string, any>
3062
3197
  pagination?: Partial<PaginationConfig>
3063
- }
3198
+ } & (
3199
+ | {
3200
+ shape?: ShapeOrFn<TShape, TCtx> | Record<string, ShapeOrFn<TShape, TCtx>>
3201
+ variants?: never
3202
+ }
3203
+ | {
3204
+ shape?: never
3205
+ variants: Record<
3206
+ string,
3207
+ VariantEntry<TShape, TCtx, RequestHandler>
3208
+ >
3209
+ }
3210
+ )
3064
3211
 
3065
- interface ReadOperationConfig<TCtx = unknown> extends OperationConfig {
3212
+ type ReadOperationConfig<
3213
+ TCtx = unknown,
3214
+ TShape = Record<string, any>,
3215
+ > = OperationConfig<TShape, TCtx> & {
3066
3216
  progressive?: Record<string, ProgressiveVariantConfig>
3067
3217
  progressiveStages?: Record<string, ProgressiveStage<TCtx>>
3068
3218
  }
@@ -3132,12 +3282,14 @@ interface QueryBuilderConfig {
3132
3282
  The Fastify config is identical except for hook and resolver types:
3133
3283
 
3134
3284
  ```ts
3135
- interface OperationConfig {
3285
+ type OperationConfig<TShape = Record<string, any>> = {
3136
3286
  before?: FastifyHookHandler[]
3137
3287
  after?: FastifyHookHandler[]
3138
- shape?: Record<string, any>
3139
3288
  pagination?: Partial<PaginationConfig>
3140
- }
3289
+ } & (
3290
+ | { shape?: TShape | ((ctx: unknown) => TShape) | Record<string, TShape | ((ctx: unknown) => TShape)>; variants?: never }
3291
+ | { shape?: never; variants: Record<string, { shape: TShape | ((ctx: unknown) => TShape); before?: FastifyHookHandler[]; after?: FastifyHookHandler[] }> }
3292
+ )
3141
3293
 
3142
3294
  type FastifyHookHandler = (
3143
3295
  request: FastifyRequest,
@@ -3152,12 +3304,14 @@ The `guard.resolveVariant` callback receives `FastifyRequest` instead of `Reques
3152
3304
  The Hono config is identical except for hook and resolver types:
3153
3305
 
3154
3306
  ```ts
3155
- interface OperationConfig {
3307
+ type OperationConfig<TShape = Record<string, any>> = {
3156
3308
  before?: HonoBeforeHook[]
3157
3309
  after?: HonoAfterHook[]
3158
- shape?: Record<string, any>
3159
3310
  pagination?: Partial<PaginationConfig>
3160
- }
3311
+ } & (
3312
+ | { shape?: TShape | ((ctx: unknown) => TShape) | Record<string, TShape | ((ctx: unknown) => TShape)>; variants?: never }
3313
+ | { shape?: never; variants: Record<string, { shape: TShape | ((ctx: unknown) => TShape); before?: HonoBeforeHook[]; after?: HonoAfterHook[] }> }
3314
+ )
3161
3315
 
3162
3316
  type HonoBeforeHook<Env extends { Variables: any } = any> = (
3163
3317
  c: Context<Env>,
@@ -3233,19 +3387,11 @@ Generated routers use this effective guard-drop flag:
3233
3387
  const DROP_GUARD = GENERATOR_DROP_GUARD || process.env.E2E === 'true'
3234
3388
  ```
3235
3389
 
3236
- When `DROP_GUARD` is true:
3237
-
3238
- ```ts
3239
- delegate.findMany(args)
3240
- ```
3390
+ When `DROP_GUARD` is true, the generated router calls Prisma directly instead of calling `delegate.guard(...)`. Before doing so, it still applies forced values and default projection behavior using vendored, dependency-free generated runtime helpers.
3241
3391
 
3242
- is used instead of:
3392
+ Caller routing is not dropped. Active and dropped modes use the same exact/default/parameterized resolver and the same declared matched key. Missing, unknown, ambiguous, and reserved-key routing failures return 400 after operation before-hooks in both modes.
3243
3393
 
3244
- ```ts
3245
- delegate.guard(shape, caller).findMany(args)
3246
- ```
3247
-
3248
- This is global for generated routers. No per-router config is required.
3394
+ Generated dropped/E2E router output has no runtime import from `prisma-guard`, the configured guard runtime import path, or a generated guard bridge. Type-only imports of generated guard shape declarations are allowed because they are erased at build time.
3249
3395
 
3250
3396
  ### Production safety
3251
3397
 
@@ -3362,27 +3508,20 @@ SQL extension receives normal id filters
3362
3508
 
3363
3509
  ### Required implementation notes
3364
3510
 
3365
- Generated router code should compute:
3511
+ Generated router code computes:
3366
3512
 
3367
3513
  ```ts
3368
- const DROP_GUARD = ${dropGuard} || process.env.E2E === 'true'
3514
+ const DROP_GUARD = GENERATOR_DROP_GUARD || process.env.E2E === 'true'
3369
3515
  ```
3370
3516
 
3371
- Then only assign guard shape when guard is not dropped:
3517
+ The router always resolves caller routing first. On a successful match:
3372
3518
 
3373
- ```ts
3374
- if (opConfig.shape && !DROP_GUARD) {
3375
- locals.guardShape = opConfig.shape
3376
- }
3377
- ```
3519
+ - active mode stores the normalized guard shape for `delegate.guard(...)`
3520
+ - dropped mode applies the same matched shape locally before operation before-hooks
3378
3521
 
3379
- The same behavior should apply to all generated targets:
3522
+ On a routing failure, the router stores the failure, runs operation before-hooks, then returns HTTP 400 before variant hooks or the handler. This ordering is identical across Express, Fastify, and Hono.
3380
3523
 
3381
- ```text
3382
- Express
3383
- Fastify
3384
- Hono
3385
- ```
3524
+ The dropped runtime must remain self-contained. Generated code must not require `prisma-guard` to be installed or resolvable when `dropGuard=true` or `E2E=true`.
3386
3525
 
3387
3526
  ## Environment variables
3388
3527
 
@@ -3394,4 +3533,4 @@ Hono
3394
3533
 
3395
3534
  ## License
3396
3535
 
3397
- MIT
3536
+ MIT
@@ -59,26 +59,46 @@ function generateRouteConfigType(modelName, hookHandlerType, guardShapesImport,
59
59
  }
60
60
  const shapeOps = Array.from(new Set(ROUTER_OPERATIONS));
61
61
  const opShapeImports = shapeOps
62
- .map((op) => `${m}${capitalize(op)}ShapeInput`)
62
+ .flatMap((op) => {
63
+ const prefix = `${m}${capitalize(op)}Shape`;
64
+ return [prefix, `${prefix}Input`];
65
+ })
63
66
  .join(',\n ');
67
+ const shapeOrFnAliases = shapeOps
68
+ .map((op) => {
69
+ const prefix = `${m}${capitalize(op)}Shape`;
70
+ return (`type ${prefix}OrFn<TCtx = unknown> =\n` +
71
+ ` | ${prefix}\n` +
72
+ ` | ((ctx: TCtx) => ${prefix})`);
73
+ })
74
+ .join('\n\n');
64
75
  const overrides = ROUTER_OPERATIONS.map((routerOp) => {
65
76
  const c = capitalize(routerOp);
66
77
  const isRead = operationDefinitions_1.READ_OPERATION_NAMES.has(routerOp);
67
- const lines = [
78
+ const commonLines = [
68
79
  ` before?: ${beforeRef}[]`,
69
80
  ` after?: ${afterRef}[]`,
70
- ` shape?: ${m}${c}ShapeInput<TCtx>`,
71
81
  ` pagination?: Partial<PaginationConfig>`,
72
82
  ];
73
83
  if (isRead && supportsProgressive) {
74
- lines.push(` progressive?: Record<string, ProgressiveVariantConfig>`);
75
- lines.push(` progressiveStages?: Record<string, ProgressiveStage<TCtx, TPrisma>>`);
84
+ commonLines.push(` progressive?: Record<string, ProgressiveVariantConfig>`);
85
+ commonLines.push(` progressiveStages?: Record<string, ProgressiveStage<TCtx, TPrisma>>`);
76
86
  }
77
- return ` ${routerOp}?: {\n${lines.join('\n')}\n } | false`;
87
+ const commonConfig = `{\n${commonLines.join('\n')}\n }`;
88
+ const variantsConfig = `Record<string, {\n` +
89
+ ` shape: ${m}${c}ShapeOrFn<TCtx>\n` +
90
+ ` before?: ${beforeRef}[]\n` +
91
+ ` after?: ${afterRef}[]\n` +
92
+ ` }>`;
93
+ return (` ${routerOp}?: (${commonConfig} & (\n` +
94
+ ` | { shape?: ${m}${c}ShapeInput<TCtx>; variants?: never }\n` +
95
+ ` | { shape?: never; variants: ${variantsConfig} }\n` +
96
+ ` )) | false`);
78
97
  }).join('\n');
79
98
  const omitKeys = ROUTER_OPERATIONS.map((k) => `'${k}'`).join('\n | ');
80
99
  return (progressiveTypeImport +
81
100
  `import type {\n ${opShapeImports}\n} from '${guardShapesImport}${ext}'\n\n` +
101
+ `${shapeOrFnAliases}\n\n` +
82
102
  `export type ${m}RouteConfig${generics} = Omit<\n` +
83
103
  ` ${baseConfig},\n` +
84
104
  ` | ${omitKeys}\n` +
@@ -1 +1 @@
1
- {"version":3,"file":"generateRouteConfigType.js","sourceRoot":"","sources":["../../src/generators/generateRouteConfigType.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generateRouteConfigType.js","sourceRoot":"","sources":["../../src/generators/generateRouteConfigType.ts"],"names":[],"mappings":"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"}