prisma-guard 1.27.0 → 1.28.0

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
@@ -33,15 +33,18 @@ database
33
33
 
34
34
  * [Why this exists](#why-this-exists)
35
35
  * [What prisma-guard does](#what-prisma-guard-does)
36
+ * [Validation philosophy](#validation-philosophy)
36
37
  * [Architecture](#architecture)
37
38
  * [Install](#install)
38
39
  * [Quick start](#quick-start)
40
+ * [Generator configuration](#generator-configuration)
39
41
  * [Before / After prisma-guard](#before--after-prisma-guard)
40
42
  * [Schema annotations](#schema-annotations)
41
43
  * [The guard API](#the-guard-api)
42
44
  * [Relation writes in data shapes](#relation-writes-in-data-shapes)
43
45
  * [Logical combinators in where shapes](#logical-combinators-in-where-shapes)
44
46
  * [Relation filters in where shapes](#relation-filters-in-where-shapes)
47
+ * [Read projection auto-apply](#read-projection-auto-apply)
45
48
  * [Mutation return projection](#mutation-return-projection)
46
49
  * [Enforced projection mode](#enforced-projection-mode)
47
50
  * [Upsert](#upsert)
@@ -139,6 +142,61 @@ Role-based access control is intentionally out of scope.
139
142
 
140
143
  ---
141
144
 
145
+ ## Validation philosophy
146
+
147
+ `prisma-guard` is intentionally not a strict scalar-validation framework by default.
148
+
149
+ The default runtime behavior focuses on:
150
+
151
+ * enforcing the allowed query and data shape
152
+ * rejecting unknown fields and unsupported operations
153
+ * coercing common input values into Prisma-compatible types where reasonable
154
+ * preventing dangerous ambiguity, destructive unconstrained operations, and silent shape misconfiguration
155
+
156
+ This means prisma-guard defaults are practical and frontend-friendly. Inputs often arrive from forms, query strings, JSON bodies, and frontend state where values may need light coercion before reaching Prisma.
157
+
158
+ Strict business validation is opt-in. Use one of these when exact scalar rules matter:
159
+
160
+ * `@zod` directives in the Prisma schema
161
+ * inline refine functions in `data`, `create`, or `update` shapes
162
+ * `guard.input({ refine })`
163
+ * application-level validation before calling guarded Prisma methods
164
+
165
+ ```prisma
166
+ model User {
167
+ id String @id @default(cuid())
168
+ /// @zod .email().max(255)
169
+ email String
170
+ }
171
+ ```
172
+
173
+ ```ts
174
+ await prisma.user
175
+ .guard({
176
+ data: {
177
+ age: (base) => base.int().min(18).max(120),
178
+ },
179
+ })
180
+ .create({ data: req.body })
181
+ ```
182
+
183
+ ### Scalar coercion defaults
184
+
185
+ Default scalar validation is intentionally permissive where that is useful and Prisma-compatible.
186
+
187
+ Examples:
188
+
189
+ * `Int` accepts practical numeric input and coerces to an integer value.
190
+ * `Float` accepts JavaScript numeric input.
191
+ * `DateTime` accepts values that can be parsed into a JavaScript `Date`.
192
+ * `Decimal` accepts JavaScript `number`, decimal string, and Decimal-like objects unless [strict Decimal mode](#strict-decimal-mode) is enabled.
193
+
194
+ For stricter behavior, add `@zod` rules or a refine function. The guard should stay easy to use by default; exact business rules belong in explicit validation.
195
+
196
+ This leniency does not apply to boundary safety. prisma-guard should still reject unknown fields, invalid shape keys, unsafe projection behavior, unconstrained destructive nested writes, and shape configs that look restrictive but would otherwise be silently ignored.
197
+
198
+ ---
199
+
142
200
  ## Architecture
143
201
 
144
202
  `prisma-guard` sits between your application and Prisma Client. The `.guard(shape)` call defines the boundary; the chained Prisma method validates and executes in one step.
@@ -187,9 +245,9 @@ Peer dependencies:
187
245
 
188
246
  Both `zod` and `@prisma/client` must be installed before running `prisma generate`. The generator validates `@zod` directives against real Zod schemas at generation time.
189
247
 
190
- Generated output files (`index.ts`, `client.ts`) are TypeScript. A TypeScript-capable build pipeline is required.
248
+ Generated output files (`index.ts`, `client.ts`, and optionally `shapes.ts`) are TypeScript. A TypeScript-capable build pipeline is required.
191
249
 
192
- The generated output uses `.js` extension imports in TypeScript source (e.g. `import { ... } from './index.js'`). This requires ESM-aware module resolution in your TypeScript config either `"moduleResolution": "NodeNext"` or `"moduleResolution": "Bundler"` in `tsconfig.json`. The classic `"moduleResolution": "node"` setting is not compatible.
250
+ Generated internal imports follow the consuming project's TypeScript setup. By default, `importStyle = "auto"` reads the nearest `tsconfig.json` from the generator output directory, follows relative and package-style `extends`, and chooses extensionless, `.js`, or `.ts` imports based on `module`, `moduleResolution`, `allowImportingTsExtensions`, and nearest `package.json` `"type"`. This keeps CommonJS/classic TypeScript projects on extensionless imports and NodeNext/Node16 ESM projects on `.js` imports. If auto-detection does not match your build, set `importStyle` explicitly.
193
251
 
194
252
  ---
195
253
 
@@ -225,7 +283,19 @@ const prisma = new PrismaClient().$extends(
225
283
  )
226
284
  ```
227
285
 
228
- ### 4. Use it
286
+ ### 4. Provide request context
287
+
288
+ The context function reads from `AsyncLocalStorage`, so each request needs to run inside a store scope:
289
+
290
+ ```ts
291
+ await store.run({ tenantId }, async () => {
292
+ await handler()
293
+ })
294
+ ```
295
+
296
+ In a web framework, set the store once per request and run the route handler inside it.
297
+
298
+ ### 5. Use it
229
299
  ```ts
230
300
  await prisma.project
231
301
  .guard({
@@ -246,6 +316,71 @@ That's it. Input is validated, query shape is enforced, tenant scope is injected
246
316
 
247
317
  ---
248
318
 
319
+ ## Generator configuration
320
+
321
+ All generator config values are strings because Prisma generator config is string-based.
322
+
323
+ ```prisma
324
+ generator guard {
325
+ provider = "prisma-guard"
326
+ output = "generated/guard"
327
+
328
+ onInvalidZod = "error" // error | warn
329
+ onAmbiguousScope = "error" // error | warn | ignore
330
+ onMissingScopeContext = "error" // error | warn | ignore
331
+ findUniqueMode = "reject" // reject | verify
332
+ onScopeRelationWrite = "error" // error | warn | strip
333
+
334
+ strictDecimal = "false" // true | false
335
+ enforceProjection = "false" // true | false
336
+ typedGuardShapes = "true" // true | false
337
+ typedGuardRelationDepth = "1" // 0 | 1 | 2 | 3
338
+
339
+ importStyle = "auto" // auto | none | js | ts
340
+ runtimeImportPath = "prisma-guard"
341
+ }
342
+ ```
343
+
344
+ ### Typed shape generation depth
345
+
346
+ `typedGuardShapes` controls whether the generator emits `shapes.ts` helper types.
347
+
348
+ When `typedGuardShapes = "false"`, prisma-guard does not emit `shapes.ts`. If an older generated `shapes.ts` exists in the output directory, it is removed on the next generation.
349
+
350
+ `typedGuardRelationDepth` controls how deeply generated TypeScript helper types expand relation shapes:
351
+
352
+ | Value | Behavior |
353
+ | ----- | -------- |
354
+ | `"0"` | Do not expand relation shapes in generated helper types |
355
+ | `"1"` | Expand direct relations; default |
356
+ | `"2"` | Expand relations two levels deep |
357
+ | `"3"` | Expand relations three levels deep |
358
+
359
+ Higher values give richer editor assistance for nested projections and relation filters, but can make generated types heavier. Runtime validation is not limited by this setting.
360
+
361
+ ### Import style
362
+
363
+ `importStyle` controls imports inside generated `client.ts` and `shapes.ts`.
364
+
365
+ | Value | Behavior |
366
+ | ----- | -------- |
367
+ | `"auto"` | Read project `tsconfig.json` and nearest `package.json` to choose the import style |
368
+ | `"none"` | Use extensionless imports like `./index` |
369
+ | `"js"` | Use `.js` imports like `./index.js` |
370
+ | `"ts"` | Use `.ts` imports like `./index.ts` |
371
+
372
+ Auto mode chooses:
373
+
374
+ * `"ts"` when `allowImportingTsExtensions` is `true`
375
+ * `"js"` for `module` or `moduleResolution` values like `Node16`, `Node18`, or `NodeNext`
376
+ * `"none"` for classic CommonJS-style resolution like `node`, `node10`, `classic`, or `bundler`
377
+ * `"js"` when no decisive `tsconfig.json` setting is found but nearest `package.json` has `"type": "module"`
378
+ * `"none"` as final fallback
379
+
380
+ `runtimeImportPath` controls where generated files import runtime APIs from. The default is `prisma-guard`. This is mainly useful for monorepos, local package aliases, or development builds.
381
+
382
+ ---
383
+
249
384
  ## Before / After prisma-guard
250
385
 
251
386
  ### Without prisma-guard
@@ -312,6 +447,8 @@ Models with a single unambiguous foreign key to a scope root are auto-scoped. A
312
447
 
313
448
  If a model has multiple foreign keys to the same scope root, the ambiguous root is excluded from that model's scope entries when `onAmbiguousScope` is `"warn"` or `"ignore"`, and causes a generation error when `onAmbiguousScope` is `"error"` (the default). Other non-ambiguous roots on the same model are still auto-scoped.
314
449
 
450
+ Scope root models themselves are context roots. They are not automatically scoped by their own `@scope-root` marker. For example, if `Tenant` is marked `@scope-root`, child models with `tenantId` are scoped, but direct `tenant.findMany()` calls are not scoped by the tenant root marker. Do not expose root model delegates directly unless you add your own shape rules or application-level authorization.
451
+
315
452
  ### Add field-level validation with `@zod`
316
453
  ```prisma
317
454
  model User {
@@ -325,7 +462,7 @@ model User {
325
462
 
326
463
  `@zod` chains apply automatically when the field appears in a `data` shape with `true`.
327
464
 
328
- `@zod` directives are validated during `prisma generate`. The generator validates directive syntax, checks that each chained method is in the allowed list, and verifies method compatibility by advancing through the chain. Type-changing methods (such as `.nullable()`, `.optional()`, `.default()`) advance the schema type, so a chain like `.nullable().email()` is correctly rejected if `.email()` does not exist on the nullable wrapper. Note: some argument-level type mismatches may only be caught if Zod throws at schema construction time.
465
+ `@zod` directives are validated during `prisma generate`. The generator validates directive syntax, checks that each chained method is in the allowed list, checks argument count, and attempts to construct the final Zod schema from the generated base type. Invalid chains such as `.nullable().email()` are rejected because schema construction fails. Some argument-level type mismatches are only caught if Zod throws while the schema is being constructed.
329
466
 
330
467
  For list fields, `@zod` chains apply to the `z.array(...)` schema, not to individual elements. For example, `.min(1)` on a `String[]` field enforces a minimum array length of 1, not a minimum string length.
331
468
 
@@ -378,11 +515,12 @@ When a refine function returns a schema that handles undefined input (e.g. by in
378
515
 
379
516
  ### Data shape syntax
380
517
 
381
- Each field in a `data` shape accepts one of four value types:
518
+ Each field in a `data` shape accepts these value types:
382
519
 
383
520
  * `true` — the client may provide this value; `@zod` chains apply automatically
384
521
  * literal value — the server forces this value; the client cannot override it
385
522
  * `force(value)` — the server forces this value; required when the value is literally `true` (see [The `force()` helper](#the-force-helper))
523
+ * `unsupported()` — explicitly acknowledge and omit an `Unsupported(...)` Prisma field from client input
386
524
  * function `(base) => schema` — the client may provide this value; the function receives the base Zod type (without `@zod` chains) and returns a refined schema
387
525
  ```ts
388
526
  import { force } from 'prisma-guard'
@@ -427,6 +565,32 @@ where: {
427
565
 
428
566
  `force()` wraps the value in a marker object. It can wrap any value type, not just booleans. Using `force()` on non-`true` values is allowed but unnecessary — only the literal `true` collides with the client-controlled sentinel.
429
567
 
568
+ ### Unsupported Prisma field types
569
+
570
+ Prisma fields declared as `Unsupported(...)` are never client-controlled. A data shape like this is rejected:
571
+
572
+ ```ts
573
+ data: { rawVector: true }
574
+ ```
575
+
576
+ Use `unsupported()` when the field exists in the Prisma schema but should be intentionally omitted from guarded input:
577
+
578
+ ```ts
579
+ import { unsupported } from 'prisma-guard'
580
+
581
+ data: { rawVector: unsupported() }
582
+ ```
583
+
584
+ You may still force an unsupported field from trusted server code:
585
+
586
+ ```ts
587
+ data: { rawVector: serverComputedValue }
588
+ ```
589
+
590
+ Unsupported fields are not filterable in `where` shapes.
591
+
592
+ Unsupported fields are also not exposed through `guard.input()` or `guard.model()` by default. In data shapes, unsupported fields cannot be client-controlled; use `unsupported()` to acknowledge that the field is intentionally omitted from guarded input, or provide a forced server-side value.
593
+
430
594
  ### Query shape syntax
431
595
 
432
596
  For read operations, `true` means the client may provide this value and literal values are forced:
@@ -448,11 +612,35 @@ The client can only filter by `title`, sort by `title`, and take up to 100 rows.
448
612
 
449
613
  `take` accepts either an object or a number. When a number is provided, it serves as both the maximum and the default:
450
614
  ```ts
451
- take: 50 // equivalent to { max: 50, default: 50 }
452
- take: { max: 100, default: 25 } // explicit max and default
453
- take: { max: 100 } // max only, no default
615
+ take: 50 // equivalent to { max: 50, default: 50 }
616
+ take: { max: 100, default: 25 } // client may send 1..100; omitted value becomes 25
617
+ take: { max: 100 } // client may send 1..100; omitted value stays omitted
618
+ ```
619
+
620
+ This shorthand applies anywhere `take` is supported, including nested relation projections. Use `{ max }` without `default` only when an omitted `take` should remain omitted.
621
+
622
+ ### Order by shapes
623
+
624
+ `orderBy` shape config uses `true` per sortable field:
625
+
626
+ ```ts
627
+ orderBy: { createdAt: true, title: true }
628
+ ```
629
+
630
+ Client input then uses Prisma sort directions:
631
+
632
+ ```ts
633
+ { orderBy: { createdAt: 'desc' } }
454
634
  ```
455
635
 
636
+ Do not put filter operators under `orderBy`. This is rejected:
637
+
638
+ ```ts
639
+ orderBy: { title: { contains: true } }
640
+ ```
641
+
642
+ For `groupBy`, normal grouped fields and `_count` fields must also be configured with `true`.
643
+
456
644
  ### Unique where shapes
457
645
 
458
646
  `findUnique`, `findUniqueOrThrow`, `update`, `delete`, and `upsert` use Prisma's `WhereUniqueInput` syntax. For these methods, unique fields are configured directly in the shape:
@@ -469,7 +657,7 @@ await prisma.project
469
657
 
470
658
  Do not use filter operator objects such as `{ id: { equals: true } }` in unique where shapes. That syntax belongs to normal `WhereInput` filters used by methods such as `findMany`, `findFirst`, `count`, `updateMany`, and `deleteMany`.
471
659
 
472
- For compound unique constraints, use Prisma's generated compound selector name:
660
+ If a model has multiple single-field unique selectors and the shape lists more than one, the client may use any allowed selector. For example, `where: { id: true, slug: true }` allows a request with `where: { id: '...' }` or `where: { slug: '...' }`. For compound unique constraints, use Prisma's generated compound selector name:
473
661
  ```prisma
474
662
  model ProjectMember {
475
663
  tenantId String
@@ -574,7 +762,7 @@ await prisma.project
574
762
 
575
763
  `status = 'published'` and `isActive = true` are always enforced. The client can only control the `title` filter.
576
764
 
577
- Forced where conditions are conflict-checked during shape construction. If the same field and operator appear with different forced values in different parts of a shape (e.g. at the top level and inside a combinator), the shape is rejected with `ShapeError`. This prevents ambiguous security configurations where one forced value would silently overwrite another.
765
+ Forced where conditions are conflict-checked during shape construction. If the same field and operator appear with different forced values in different parts of a shape (e.g. at the top level and inside a combinator), the shape is rejected with `ShapeError`. This prevents ambiguous security configurations where one forced value would silently overwrite another. Forced `NOT` conditions are preserved as separate logical `NOT` branches when merged with client-provided `NOT`, rather than being merged like scalar fields.
578
766
 
579
767
  ### Deletes
580
768
  ```ts
@@ -756,7 +944,9 @@ Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `
756
944
 
757
945
  Data shapes support relation fields with a config object describing which nested write operations the client may use. Each operation (`connect`, `create`, `disconnect`, etc.) is configured individually.
758
946
 
759
- > **⚠️ Security warning:** The automatic tenant scope extension only intercepts **top-level** operations. Nested writes through relation configs bypass scope entirely — no FK injection on nested creates, no tenant filtering on nested updates/deletes. If you use relation writes on scoped models, you must handle tenant isolation manually in your application code or enforce it via database constraints (e.g. RLS, triggers).
947
+ > **⚠️ Security warning:** The automatic tenant scope extension only intercepts **top-level** operations. Nested writes through relation configs bypass scope entirely — no FK injection on nested creates, no tenant filtering on nested updates/deletes, and no tenant filtering on nested connects. If you use relation writes on scoped models, handle tenant isolation manually in application code or enforce it via database constraints such as RLS, triggers, and foreign keys.
948
+ >
949
+ > Be especially careful with destructive nested operations. An unconstrained nested delete such as `{ posts: { deleteMany: {} } }` can delete all related children under the parent. prisma-guard treats unconstrained destructive nested writes as unsafe boundary configuration; expose them only through explicit server-side code.
760
950
 
761
951
  ### Syntax
762
952
  ```ts
@@ -790,17 +980,29 @@ All 11 Prisma nested write operations are supported:
790
980
 
791
981
  | Operation | To-one | To-many | Config type |
792
982
  | ----------------- | ------ | ------- | ------------------------------------------------ |
793
- | `connect` | yes | yes | `{ fieldName: true, ... }` |
794
- | `connectOrCreate` | yes | yes | `{ where: { ... }, create: { ... } }` |
983
+ | `connect` | yes | yes | unique selector config, e.g. `{ id: true }` |
984
+ | `connectOrCreate` | yes | yes | `{ where: unique selector, create: { ... } }` |
795
985
  | `create` | yes | yes | `{ fieldName: true, ... }` |
796
986
  | `createMany` | no | yes | `{ data: { fieldName: true, ... } }` |
797
- | `disconnect` | yes | yes | `true` (to-one) or `{ fieldName: true }` (to-many) |
798
- | `delete` | yes | yes | `true` (to-one) or `{ fieldName: true }` (to-many) |
799
- | `set` | no | yes | `{ fieldName: true, ... }` |
800
- | `update` | yes | yes | `{ fieldName: true }` or `{ where: ..., data: ... }` |
987
+ | `disconnect` | yes | yes | `true` (to-one) or unique selector config (to-many) |
988
+ | `delete` | yes | yes | `true` (to-one) or unique selector config (to-many) |
989
+ | `set` | no | yes | unique selector config |
990
+ | `update` | yes | yes | `{ fieldName: true }` or `{ where: unique selector, data: ... }` |
801
991
  | `updateMany` | no | yes | `{ where: { ... }, data: { ... } }` |
802
- | `upsert` | yes | yes | `{ create: { ... }, update: { ... } }` |
803
- | `deleteMany` | no | yes | `{ fieldName: true, ... }` |
992
+ | `upsert` | yes | yes | `{ where?: unique selector, create: { ... }, update: { ... } }` |
993
+ | `deleteMany` | no | yes | filter config; unconstrained empty object is unsafe |
994
+
995
+ For to-many relation operations that use unique selectors, prisma-guard accepts Prisma-compatible single-object and array forms:
996
+
997
+ ```ts
998
+ tags: {
999
+ connect: { id: 'tag1' },
1000
+ disconnect: [{ id: 'tag2' }],
1001
+ set: { id: 'tag3' },
1002
+ }
1003
+ ```
1004
+
1005
+ Empty arrays are accepted where Prisma accepts them, for example `set: []` to clear a to-many relation.
804
1006
 
805
1007
  ### Example with multiple operations
806
1008
  ```ts
@@ -838,9 +1040,38 @@ Each operation's config is validated at shape construction time:
838
1040
 
839
1041
  * Unknown operations throw `ShapeError`
840
1042
  * Operations invalid for the relation cardinality throw `ShapeError` (e.g. `set` on to-one, `disconnect: true` on to-many)
841
- * Nested data fields are validated against the related model's type map — relation fields within nested data are not supported (no deep nesting)
1043
+ * Nested data fields are validated against the related model's type map
1044
+ * Relation fields inside nested data are not recursively expanded; nested write shapes support one relation-write level
1045
+ * Nested create paths validate configured fields with create semantics, but final required-field completeness may still be enforced by Prisma
1046
+ * Nested update paths use update semantics: all configured data fields are optional at runtime
1047
+ * Operations that use Prisma `WhereUniqueInput` semantics should be configured with unique fields or compound unique selector names
842
1048
  * `@zod` chains apply to nested data fields
843
1049
 
1050
+ ### Unique selectors in relation writes
1051
+
1052
+ Relation write operations such as `connect`, `disconnect`, `delete`, `set`, `connectOrCreate.where`, `update.where`, and `upsert.where` use Prisma unique selector semantics.
1053
+
1054
+ For single-field unique selectors, configure the field directly:
1055
+
1056
+ ```ts
1057
+ posts: {
1058
+ connect: { id: true },
1059
+ }
1060
+ ```
1061
+
1062
+ For compound unique constraints, use Prisma's compound selector name, same as top-level unique where shapes:
1063
+
1064
+ ```ts
1065
+ members: {
1066
+ connect: {
1067
+ tenantId_userId: {
1068
+ tenantId: true,
1069
+ userId: true,
1070
+ },
1071
+ },
1072
+ }
1073
+ ```
1074
+
844
1075
  ### Scope implications
845
1076
 
846
1077
  Nested writes bypass the scope extension because Prisma extension hooks only fire for top-level operations. This means:
@@ -1084,6 +1315,8 @@ where: {
1084
1315
 
1085
1316
  When a read shape defines `select` or `include`, the projection serves two roles: it whitelists what the client is allowed to request, and it provides the default projection when the client omits `select`/`include` from the body.
1086
1317
 
1318
+ A client-provided `select` or `include` is treated as a narrowing request inside the shape's whitelist. It should not widen back to the full default projection. If the client asks for fewer fields than the shape allows, only the requested allowed fields are returned.
1319
+
1087
1320
  If the client sends a body without `select` or `include`, the shape's projection is automatically synthesized and passed to Prisma. This eliminates the need for the client to duplicate the field list that the backend already defines.
1088
1321
 
1089
1322
  ```ts
@@ -1106,9 +1339,9 @@ await prisma.company
1106
1339
 
1107
1340
  The client sends only `{ where: { id: { equals: 'abc' } } }`. The shape's `select` is applied automatically, nested `take` defaults and forced `where` conditions are resolved through the normal pipeline.
1108
1341
 
1109
- If the client does send `select` or `include`, the shape acts as a whitelist — only the fields and relations defined in the shape are accepted. This behavior is unchanged from before.
1342
+ If the client does send `select` or `include`, the shape acts as a whitelist — only the fields and relations defined in the shape are accepted. When the shape defines a relation with an object config and the client sends `relation: true`, prisma-guard expands `true` to the relation's default projection skeleton before validation. This means nested defaults such as `take.default`, nested whitelists, and forced where rules still apply.
1110
1343
 
1111
- The synthesized projection includes the structural skeleton only: scalar fields as `true`, nested `select`/`include` trees. Client-controllable args like `orderBy`, `take`, `skip`, and `cursor` on nested relations are omitted from the synthesized body. Defaults (e.g. `take: { default: 5 }`) are filled by zod schema parsing, and forced `where` conditions are merged by the forced tree pipeline.
1344
+ The synthesized projection includes the structural skeleton only: scalar fields as `true`, nested `select`/`include` trees, and object skeletons for relation configs that need nested defaults. Client-controllable args like `orderBy`, `take`, `skip`, and `cursor` on nested relations are omitted from the synthesized body before parsing; defaults (e.g. `take: { default: 5 }`) are filled by zod schema parsing, and forced `where` conditions are merged by the forced tree pipeline. Empty object skeletons that remain empty after parsing collapse back to `true`.
1112
1345
 
1113
1346
  This applies to all read methods: `findMany`, `findFirst`, `findFirstOrThrow`, `findUnique`, `findUniqueOrThrow`, `count`, `aggregate`, and `groupBy`. Methods where `select`/`include` is not valid (`aggregate`, `groupBy`) already reject those shape keys upstream, so auto-apply never triggers for them.
1114
1347
 
@@ -1264,9 +1497,9 @@ Without enforced projection (default): Prisma returns all fields.
1264
1497
  When the client omits `select`/`include`, prisma-guard synthesizes a default projection body from the shape:
1265
1498
 
1266
1499
  * Scalar fields marked `true` in the shape produce `true` in the synthesized body
1267
- * Nested relation shapes produce their structural equivalent (nested `select`/`include`)
1500
+ * Nested relation shapes produce their structural equivalent (nested `select`/`include`) or an object skeleton when needed for nested defaults
1268
1501
  * `_count` configurations are preserved
1269
- * Client-controllable args like `where`, `orderBy`, `take`, `skip` on nested includes are omitted from the synthesized body only forced where conditions are applied through the existing forced-tree pipeline
1502
+ * Client-controllable args like `where`, `orderBy`, `take`, `skip` on nested includes are omitted from the synthesized body before parsing; defaults are then applied by the projection schema, and forced where conditions are applied through the forced-tree pipeline
1270
1503
 
1271
1504
  When the client does provide `select`/`include`, behavior is identical regardless of this setting: the client's projection is validated against the shape.
1272
1505
 
@@ -1559,6 +1792,8 @@ const prisma = new PrismaClient().$extends(
1559
1792
 
1560
1793
  The context function returns an object with arbitrary keys. Keys whose values are `string`, `number`, or `bigint` and that match a scope root model name are used as scope context for tenant isolation. The `caller` key (if a string) is used as the default caller for named shape routing. Other keys (like `role` in the example above) are passed through to shape functions but are not used for scoping or routing.
1561
1794
 
1795
+ The context function should be stable for the duration of a request. It may be read by caller routing, dynamic shape resolution, and scope injection. `AsyncLocalStorage` is the recommended source for request context.
1796
+
1562
1797
  The context function must return a plain object. If it returns `null`, `undefined`, an array, a primitive, or any non-plain-object value, a `PolicyError` is thrown. This is enforced consistently across all code paths that consume context — scope injection, caller resolution, and dynamic shape evaluation.
1563
1798
 
1564
1799
  If a context key matches a known scope root model name but has a non-primitive value (e.g. an object or array instead of a string, number, or bigint), a `PolicyError` is thrown immediately. This prevents bugs in the context function from silently weakening scope enforcement.
@@ -1647,6 +1882,7 @@ This applies to all top-level operations on scoped models, including reads, writ
1647
1882
 
1648
1883
  ### What is NOT scoped
1649
1884
 
1885
+ * Scope root model delegates themselves — `@scope-root` marks a context root; it does not self-scope direct calls to that model
1650
1886
  * Nested reads loaded via `include` or `select` — use forced where conditions in the shape to restrict these (to-many relations only; see [Limitations](#limitations))
1651
1887
  * Nested writes via relation write configs in data shapes — the scope extension hooks only fire for top-level operations (see [Relation writes in data shapes](#relation-writes-in-data-shapes))
1652
1888
  * `$queryRaw` and `$executeRaw` — raw SQL bypasses all guard protections
@@ -1843,12 +2079,18 @@ These limitations are real and should be treated as part of the security model.
1843
2079
 
1844
2080
  `$queryRaw` and `$executeRaw` are not intercepted.
1845
2081
 
2082
+ ### Scope root models are not self-scoped
2083
+
2084
+ `@scope-root` marks a model as a context root used to scope child models. It does not add a self-scope rule to that root model's own delegate. If you expose operations on the root model itself, protect those routes with explicit guard shapes, application authorization, or database policies.
2085
+
1846
2086
  ### Nested writes are not scope-intercepted
1847
2087
 
1848
2088
  Prisma extension hooks operate on top-level operations. Relation write configs in data shapes (see [Relation writes in data shapes](#relation-writes-in-data-shapes)) produce nested write operations that bypass the scope extension entirely. Nested creates do not receive scope FK injection. Nested updates and deletes do not receive tenant where conditions.
1849
2089
 
1850
2090
  For multi-tenant applications using relation writes, enforce tenant boundaries via database constraints (RLS, foreign key constraints, triggers) or application-level validation.
1851
2091
 
2092
+ Avoid exposing unconstrained destructive nested writes. For example, a nested `deleteMany: {}` can delete every child record related to the parent and is not tenant-filtered by the scope extension.
2093
+
1852
2094
  ### Nested reads via include are not scope-filtered
1853
2095
 
1854
2096
  The scope extension operates on the top-level operation only. If a query uses `include` or `select` to load a relation that is itself a scoped model, the nested results are not tenant-filtered by the extension. Use forced where conditions in the include/select shape to restrict nested reads. This applies to both read operations and mutation return projections.
@@ -1897,9 +2139,9 @@ cursor: {
1897
2139
 
1898
2140
  `createMany`, `updateMany`, and `deleteMany` return `BatchPayload` (a count). Passing `select` or `include` in the shape or body for these methods throws `ShapeError`.
1899
2141
 
1900
- ### Generated output is TypeScript with ESM imports
2142
+ ### Generated output is TypeScript with configurable imports
1901
2143
 
1902
- The generator writes `index.ts` and `client.ts` using `.js` extension imports. A TypeScript-capable build pipeline with ESM-aware module resolution is required (`"moduleResolution": "NodeNext"` or `"Bundler"` in `tsconfig.json`). The classic `"moduleResolution": "node"` setting is not compatible.
2144
+ The generator writes TypeScript files. Internal generated imports are controlled by `importStyle`. Auto mode reads the consuming project's `tsconfig.json` and package type, then emits extensionless, `.js`, or `.ts` imports as appropriate. CommonJS/classic projects normally use extensionless imports; NodeNext/Node16 ESM projects normally use `.js` imports. If auto-detection is wrong for your build pipeline, set `importStyle` explicitly.
1903
2145
 
1904
2146
  ### `having` supports logical combinators
1905
2147
 
@@ -2006,10 +2248,10 @@ Libraries like **prisma-sql** make this possible for advanced architectures.
2006
2248
 
2007
2249
  `.guard(shape).method(body)` may throw:
2008
2250
 
2009
- * `ZodError` — Zod validation failures on data or query args (unless `wrapZodErrors` is enabled)
2010
- * `ShapeError` — invalid shape config, unknown shape config keys, wrong method for shape, body format issues, unexpected body keys, incomplete create data shapes, invalid inline refine functions, dynamic shape functions returning invalid values, conflicting forced where values, empty combinator or relation filter definitions, empty projection shapes, vacuous combinator input, non-`true` config values in shape builders, or using `data` instead of `create`/`update` for upsert
2251
+ * `ShapeError` — invalid shape config, unknown shape config keys, wrong method for shape, body format issues, unexpected body keys, incomplete top-level create data shapes, invalid inline refine functions, dynamic shape functions returning invalid values, conflicting forced where values, empty combinator or relation filter definitions, empty projection shapes, vacuous combinator input, non-`true` config values in shape builders, Zod validation failures caught by guarded method parsing, or using `data` instead of `create`/`update` for upsert
2011
2252
  * `CallerError` — missing, unknown, or ambiguous caller in named shapes, or `caller` found in request body
2012
2253
  * `PolicyError` — denied scope, missing tenant context, invalid context function return value, invalid scope root value type, or rejected operations on scoped models (e.g. findUnique in reject mode)
2254
+ * `ZodError` — raw Zod validation failures from lower-level APIs such as `guard.input().parse()` and `guard.query().parse()` when `wrapZodErrors` is not enabled
2013
2255
 
2014
2256
  All guard errors include `status` and `code` properties for HTTP response mapping:
2015
2257
 
@@ -2023,7 +2265,7 @@ Note: Prisma errors (`PrismaClientKnownRequestError`, `PrismaClientValidationErr
2023
2265
 
2024
2266
  ### ZodError wrapping
2025
2267
 
2026
- By default, Zod validation failures throw a raw `ZodError`. This means error handling code must check for both `ZodError` and guard error types.
2268
+ By default, lower-level parsing APIs can expose raw `ZodError`. Most `.guard(shape).method(body)` validation failures are reported as `ShapeError`, but code that calls `guard.input().parse()` or `guard.query().parse()` directly should still handle raw Zod errors unless wrapping is enabled.
2027
2269
 
2028
2270
  To unify error handling, pass `wrapZodErrors: true` in the guard config:
2029
2271
  ```ts
@@ -2033,9 +2275,9 @@ const guard = createGuard({
2033
2275
  })
2034
2276
  ```
2035
2277
 
2036
- When enabled, all `ZodError` thrown during validation is caught and rethrown as `ShapeError` with `status: 400` and `code: 'SHAPE_INVALID'`. The original `ZodError` is preserved as the `cause` property. The error message includes a formatted summary of all Zod issues.
2278
+ When enabled, `ZodError` thrown during supported guard validation paths is caught and rethrown as `ShapeError` with `status: 400` and `code: 'SHAPE_INVALID'`. The original `ZodError` is preserved as the `cause` property. The error message includes a formatted summary of all Zod issues.
2037
2279
 
2038
- This applies to `guard.input().parse()`, `guard.query().parse()`, and all `.guard(shape).*` methods. `guard.model()` returns a raw `z.ZodObject` and is not affected.
2280
+ This applies to `guard.input().parse()`, `guard.query().parse()`, and guarded model methods. `guard.model()` returns a raw `z.ZodObject` and is not affected.
2039
2281
 
2040
2282
  ---
2041
2283
 
@@ -2052,7 +2294,7 @@ It reads the Prisma DMMF and emits:
2052
2294
  * `TYPE_MAP` — field metadata per model
2053
2295
  * `ENUM_MAP` — enum values
2054
2296
  * `SCOPE_MAP` — foreign key → scope root mappings
2055
- * `ZOD_CHAINS` — `@zod` directive chains (validated for syntax, method allowlist, argument arity, and basic type compatibility (method existence and type advancement for wrapper-changing methods like .nullable(), .optional(), .default()). Argument type mismatches for non-type-changing methods (e.g. .min('x') on a number field) are not caught at generation time and will fail at runtime when the schema is built.)
2297
+ * `ZOD_CHAINS` — `@zod` directive chains (validated for syntax, method allowlist, argument arity, and schema construction against the generated base type. Argument type mismatches are caught when Zod throws during schema construction; otherwise they may fail later when the schema is used.)
2056
2298
  * `ZOD_DEFAULTS` — per-model list of fields that have `@zod .default(...)` or `@zod .catch(...)`, used by the create completeness check and by runtime default injection for omitted fields
2057
2299
  * `GUARD_CONFIG` — generator config values (including `strictDecimal` and `enforceProjection`)
2058
2300
  * `UNIQUE_MAP` — unique constraint metadata per model
@@ -2084,7 +2326,7 @@ When a where shape includes forced conditions, prisma-guard merges them into the
2084
2326
  1. **Inline merge** — if a forced field's value is a plain operator object and the client also provided an operator object for the same field, the forced operator keys are merged into the client's operator object. This is required for modifiers like `mode` that must co-locate with the operator they modify. Conflicts on the same op key (different values) throw `ShapeError`.
2085
2327
  2. **AND-wrap** — forced fields not present in the client's where, or where the value types don't allow inline merging, are placed in a separate AND branch.
2086
2328
 
2087
- If all forced fields inline successfully, the result is a flat object with no synthetic `AND` wrapper. Forced conditions inside combinators are still lifted to top-level AND constraints, following the same merge logic.
2329
+ If all forced fields inline successfully, the result is a flat object with no synthetic `AND` wrapper. Forced conditions inside combinators are still lifted to top-level AND constraints, following the same merge logic. Forced `NOT` is special-cased: client `NOT` and forced `NOT` are kept as separate logical branches so arrays and objects preserve Prisma `NOT` semantics.
2088
2330
 
2089
2331
  ### The `force()` helper
2090
2332
 
@@ -2167,6 +2409,8 @@ For upsert, `create` and `update` data schemas are cached independently under na
2167
2409
  | invalid relation operator for type | error always (ShapeError) |
2168
2410
  | context function returns non-object | error always (PolicyError) |
2169
2411
  | conflicting forced where values | error always (ShapeError) |
2412
+ | client echoes forced-only where field | stripped before validation |
2413
+ | client `NOT` plus forced `NOT` | preserved as separate logical branches |
2170
2414
  | invalid context function return | error always (PolicyError) |
2171
2415
  | `mode` modifier without compatible op | error always (ShapeError) |
2172
2416
  | forced operator conflicts with client value | error always (ShapeError) |
@@ -2189,6 +2433,8 @@ generator guard {
2189
2433
  onScopeRelationWrite = "error"
2190
2434
  strictDecimal = "true"
2191
2435
  enforceProjection = "true"
2436
+ importStyle = "auto"
2437
+ runtimeImportPath = "prisma-guard"
2192
2438
  }
2193
2439
  ```
2194
2440