prisma-guard 1.27.1 → 1.28.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 +279 -33
- package/dist/generator/index.js +254 -218
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.cjs +1665 -1669
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +78 -44
- package/dist/runtime/index.d.ts +78 -44
- package/dist/runtime/index.js +1665 -1669
- package/dist/runtime/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
452
|
-
take: { max: 100, default: 25 }
|
|
453
|
-
take: { max: 100 }
|
|
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,
|
|
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 | `{
|
|
794
|
-
| `connectOrCreate` | yes | yes | `{ where:
|
|
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
|
|
798
|
-
| `delete` | yes | yes | `true` (to-one) or
|
|
799
|
-
| `set` | no | yes |
|
|
800
|
-
| `update` | yes | yes | `{ fieldName: true }` or `{ where:
|
|
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 |
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
2142
|
+
### Generated output is TypeScript with configurable imports
|
|
1901
2143
|
|
|
1902
|
-
The generator writes
|
|
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
|
-
* `
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|