prisma-guard 1.2.0 → 1.3.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 +362 -42
- package/dist/generator/index.js +97 -38
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.cjs +3444 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.cts +188 -0
- package/dist/runtime/index.d.ts +10 -2
- package/dist/runtime/index.js +633 -172
- package/dist/runtime/index.js.map +1 -1
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Generate input validation, query shape enforcement, and tenant isolation directl
|
|
|
8
8
|
[](./package.json)
|
|
9
9
|
[](https://www.prisma.io/)
|
|
10
10
|
[](https://zod.dev/)
|
|
11
|
+
[](https://codecov.io/gh/multipliedtwice/prisma-guard)
|
|
11
12
|
|
|
12
13
|
`prisma-guard` helps prevent three common classes of backend mistakes:
|
|
13
14
|
|
|
@@ -41,12 +42,15 @@ database
|
|
|
41
42
|
* [Logical combinators in where shapes](#logical-combinators-in-where-shapes)
|
|
42
43
|
* [Relation filters in where shapes](#relation-filters-in-where-shapes)
|
|
43
44
|
* [Mutation return projection](#mutation-return-projection)
|
|
45
|
+
* [Enforced projection mode](#enforced-projection-mode)
|
|
46
|
+
* [Upsert](#upsert)
|
|
44
47
|
* [Named shapes and caller routing](#named-shapes-and-caller-routing)
|
|
45
48
|
* [Context-dependent shapes](#context-dependent-shapes)
|
|
46
49
|
* [Automatic tenant isolation](#automatic-tenant-isolation)
|
|
47
50
|
* [Multi-root scope behavior](#multi-root-scope-behavior)
|
|
48
51
|
* [findUnique behavior](#findunique-behavior)
|
|
49
52
|
* [Output shaping](#output-shaping)
|
|
53
|
+
* [Strict Decimal mode](#strict-decimal-mode)
|
|
50
54
|
* [Security model](#security-model)
|
|
51
55
|
* [Limitations](#limitations)
|
|
52
56
|
* [Advanced: SQL-backed runtimes](#advanced-sql-backed-runtimes)
|
|
@@ -338,7 +342,9 @@ The `@zod` DSL supports a restricted subset of Zod methods. These are the allowe
|
|
|
338
342
|
|
|
339
343
|
Note on field modifiers: prisma-guard already handles `optional` and `nullable` based on Prisma field metadata (`isRequired`, `hasDefault`). Adding `@zod .optional()` or `@zod .nullable()` explicitly will apply the Zod method on top of what prisma-guard already does, which may cause double-wrapping. Use these only when you need to override prisma-guard's default behavior.
|
|
340
344
|
|
|
341
|
-
Note on `default`: a `@zod .default(...)` chain adds a Zod-level default, which means Zod will fill in the value if it's undefined. The generator detects `.default()` in chains and emits a `ZOD_DEFAULTS` map. The create completeness check honors
|
|
345
|
+
Note on `default` and `catch`: a `@zod .default(...)` chain adds a Zod-level default, which means Zod will fill in the value if it's undefined. A `@zod .catch(...)` chain provides a fallback value when parsing fails. The generator detects both `.default()` and `.catch()` in chains and emits a `ZOD_DEFAULTS` map. The create completeness check honors Prisma's `@default` attribute, `@zod .default(...)`, and `@zod .catch(...)` — a required field with any of these sources of default is not flagged as missing from create data shapes. Prisma's `@default` remains the primary source of truth; `@zod .default(...)` and `@zod .catch(...)` are additional signals.
|
|
346
|
+
|
|
347
|
+
When a field has `@zod .default(...)` or `@zod .catch(...)` and appears in a data shape as `true`, prisma-guard preserves the Zod default/catch behavior in create mode by not wrapping the schema with `.optional()`. This ensures that omitting the field from client input triggers the Zod default or catch value rather than passing `undefined` through. If such a field is omitted from the data shape entirely (not listed as a key), the runtime auto-injects its default value as a forced field — the client cannot provide it, and the Zod default is always applied.
|
|
342
348
|
|
|
343
349
|
Note on chain ordering: type-changing methods like `.nullable()`, `.optional()`, `.default()`, and `.catch()` alter the wrapper type. Methods that follow a type-changing method must exist on the resulting wrapper, not on the original base type. For example, `.email().nullable()` is valid (`.email()` returns a string schema, `.nullable()` wraps it), but `.nullable().email()` is invalid (`.nullable()` returns a nullable wrapper that does not have `.email()`).
|
|
344
350
|
|
|
@@ -361,6 +367,8 @@ In this example, any `@zod` directive on the `email` field in the Prisma schema
|
|
|
361
367
|
|
|
362
368
|
Refine callbacks and inline refine functions must return a valid Zod schema. If the callback throws or returns a non-Zod value, a `ShapeError` is raised.
|
|
363
369
|
|
|
370
|
+
When a refine function returns a schema that handles undefined input (e.g. by including `.default(...)` or `.catch(...)`), prisma-guard detects this at runtime and preserves the default/catch behavior by not wrapping the schema with `.optional()` in create mode.
|
|
371
|
+
|
|
364
372
|
---
|
|
365
373
|
|
|
366
374
|
## The guard API
|
|
@@ -369,12 +377,15 @@ Refine callbacks and inline refine functions must return a valid Zod schema. If
|
|
|
369
377
|
|
|
370
378
|
### Data shape syntax
|
|
371
379
|
|
|
372
|
-
Each field in a `data` shape accepts one of
|
|
380
|
+
Each field in a `data` shape accepts one of four value types:
|
|
373
381
|
|
|
374
382
|
* `true` — the client may provide this value; `@zod` chains apply automatically
|
|
375
383
|
* literal value — the server forces this value; the client cannot override it
|
|
384
|
+
* `force(value)` — the server forces this value; required when the value is literally `true` (see [The `force()` helper](#the-force-helper))
|
|
376
385
|
* function `(base) => schema` — the client may provide this value; the function receives the base Zod type (without `@zod` chains) and returns a refined schema
|
|
377
386
|
```ts
|
|
387
|
+
import { force } from 'prisma-guard'
|
|
388
|
+
|
|
378
389
|
await prisma.project
|
|
379
390
|
.guard({
|
|
380
391
|
data: {
|
|
@@ -382,15 +393,39 @@ await prisma.project
|
|
|
382
393
|
status: true,
|
|
383
394
|
priority: (base) => base.refine(v => v >= 1 && v <= 5, 'Priority 1-5'),
|
|
384
395
|
createdBy: currentUserId,
|
|
396
|
+
isActive: force(true),
|
|
385
397
|
},
|
|
386
398
|
})
|
|
387
399
|
.create({ data: req.body })
|
|
388
400
|
```
|
|
389
401
|
|
|
390
|
-
In this example, `title` and `priority` use inline refines for custom validation and error messages, `status` uses `@zod` chains from the Prisma schema,
|
|
402
|
+
In this example, `title` and `priority` use inline refines for custom validation and error messages, `status` uses `@zod` chains from the Prisma schema, `createdBy` is forced to `currentUserId`, and `isActive` is forced to `true` using the `force()` helper.
|
|
391
403
|
|
|
392
404
|
Relation fields are not permitted in `data` shapes. Attempting to use a relation field in a data shape throws `ShapeError`. See [Limitations](#guarded-data-shapes-do-not-permit-relation-fields).
|
|
393
405
|
|
|
406
|
+
### The `force()` helper
|
|
407
|
+
|
|
408
|
+
The value `true` in a shape always means "client-controlled." This creates ambiguity when you need to force a boolean field to the literal value `true`. The `force()` helper resolves this:
|
|
409
|
+
```ts
|
|
410
|
+
import { force } from 'prisma-guard'
|
|
411
|
+
|
|
412
|
+
data: { isActive: true } // client-controlled — client sends any boolean
|
|
413
|
+
data: { isActive: false } // forced to false
|
|
414
|
+
data: { isActive: force(true) } // forced to true
|
|
415
|
+
data: { isActive: force(false) } // also valid — equivalent to just false
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`force()` works in both `data` shapes and `where` shapes:
|
|
419
|
+
```ts
|
|
420
|
+
where: {
|
|
421
|
+
published: { equals: true }, // client-controlled — client sends any boolean
|
|
422
|
+
isDeleted: { equals: false }, // forced to false
|
|
423
|
+
isActive: { equals: force(true) }, // forced to true
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`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.
|
|
428
|
+
|
|
394
429
|
### Query shape syntax
|
|
395
430
|
|
|
396
431
|
For read operations, `true` means the client may provide this value and literal values are forced:
|
|
@@ -419,7 +454,9 @@ await prisma.project
|
|
|
419
454
|
|
|
420
455
|
Only `title` and `status` are accepted from the client. `@zod` chains apply automatically.
|
|
421
456
|
|
|
422
|
-
For create operations, guard validates that all required fields without defaults are accounted for in the data shape — either as client-allowed (`true` or function), forced (literal value), as scope foreign keys that the scope extension will inject automatically, or as fields with a `@zod .default(...)` directive. If a required field is missing from the shape and has no Prisma `@default`, no `@zod .default(...)`, and is not a scope FK, guard throws `ShapeError` at shape evaluation time.
|
|
457
|
+
For create operations, guard validates that all required fields without defaults are accounted for in the data shape — either as client-allowed (`true` or function), forced (literal value or `force()`), as scope foreign keys that the scope extension will inject automatically, or as fields with a `@zod .default(...)` or `@zod .catch(...)` directive. If a required field is missing from the shape and has no Prisma `@default`, no `@zod .default(...)` or `@zod .catch(...)`, and is not a scope FK, guard throws `ShapeError` at shape evaluation time.
|
|
458
|
+
|
|
459
|
+
Fields with `@zod .default(...)` or `@zod .catch(...)` that are omitted from the data shape are automatically injected as forced values at runtime. The Zod schema is evaluated with `undefined` input and the resulting default or catch value is included in the create data. This ensures the field always has a value without requiring the client to provide one or the developer to list it in the shape.
|
|
423
460
|
|
|
424
461
|
### Updates
|
|
425
462
|
```ts
|
|
@@ -440,14 +477,16 @@ In update mode, all `data` fields are optional. The `where` shape enforces which
|
|
|
440
477
|
|
|
441
478
|
Literal values in the shape are forced by the server and cannot be overridden by the client:
|
|
442
479
|
```ts
|
|
480
|
+
import { force } from 'prisma-guard'
|
|
481
|
+
|
|
443
482
|
await prisma.project
|
|
444
483
|
.guard({
|
|
445
|
-
data: { title: true, status: 'draft' },
|
|
484
|
+
data: { title: true, status: 'draft', isActive: force(true) },
|
|
446
485
|
})
|
|
447
486
|
.create({ data: req.body })
|
|
448
487
|
```
|
|
449
488
|
|
|
450
|
-
`status` is always `'draft'` regardless of what the client sends.
|
|
489
|
+
`status` is always `'draft'` and `isActive` is always `true` regardless of what the client sends.
|
|
451
490
|
|
|
452
491
|
The same applies to `where`:
|
|
453
492
|
```ts
|
|
@@ -455,13 +494,14 @@ await prisma.project
|
|
|
455
494
|
.guard({
|
|
456
495
|
where: {
|
|
457
496
|
status: { equals: 'published' },
|
|
497
|
+
isActive: { equals: force(true) },
|
|
458
498
|
title: { contains: true },
|
|
459
499
|
},
|
|
460
500
|
})
|
|
461
501
|
.findMany(req.body)
|
|
462
502
|
```
|
|
463
503
|
|
|
464
|
-
`status = 'published'`
|
|
504
|
+
`status = 'published'` and `isActive = true` are always enforced. The client can only control the `title` filter.
|
|
465
505
|
|
|
466
506
|
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.
|
|
467
507
|
|
|
@@ -513,6 +553,8 @@ await prisma.project
|
|
|
513
553
|
|
|
514
554
|
A guard shape without `where` on a bulk mutation method throws `ShapeError`. Additionally, if the client provides no where conditions at runtime (or the resolved where is empty), the request is rejected. Each where operator object must contain at least one operator with a value — empty operator objects like `{ status: {} }` are rejected.
|
|
515
555
|
|
|
556
|
+
When combinators (`AND`, `OR`, `NOT`) are exposed in the where shape, the guard also prevents vacuous filters. Combinator arrays must contain at least one element, and each element must specify at least one condition with a defined value. Structures like `{ AND: [] }`, `{ AND: [{}] }`, and `{ NOT: [] }` are rejected. See [Logical combinators in where shapes](#logical-combinators-in-where-shapes) for details.
|
|
557
|
+
|
|
516
558
|
### Mutation body validation
|
|
517
559
|
|
|
518
560
|
Mutation bodies are strictly validated. The accepted keys depend on whether the shape defines a return projection (`select` or `include`):
|
|
@@ -522,6 +564,7 @@ Without projection in shape:
|
|
|
522
564
|
* `create`: `data`
|
|
523
565
|
* `createMany`, `createManyAndReturn`: `data`, `skipDuplicates`
|
|
524
566
|
* `update`, `updateMany`, `updateManyAndReturn`: `data`, `where`
|
|
567
|
+
* `upsert`: `where`, `create`, `update`, `select`, `include`
|
|
525
568
|
* `delete`, `deleteMany`: `where`
|
|
526
569
|
|
|
527
570
|
With projection in shape (methods that support it):
|
|
@@ -529,6 +572,7 @@ With projection in shape (methods that support it):
|
|
|
529
572
|
* `create`: `data`, `select`, `include`
|
|
530
573
|
* `createManyAndReturn`: `data`, `select`, `include`, `skipDuplicates`
|
|
531
574
|
* `update`, `updateManyAndReturn`: `data`, `where`, `select`, `include`
|
|
575
|
+
* `upsert`: `where`, `create`, `update`, `select`, `include`
|
|
532
576
|
* `delete`: `where`, `select`, `include`
|
|
533
577
|
|
|
534
578
|
For `createMany` and `createManyAndReturn`, `skipDuplicates` is also accepted as a body key. It must be a boolean if provided.
|
|
@@ -539,6 +583,7 @@ Guard shape keys are also validated per method:
|
|
|
539
583
|
|
|
540
584
|
* create methods accept `data`, and optionally `select`/`include` (if the method supports projection)
|
|
541
585
|
* update methods accept `data`, `where`, and optionally `select`/`include`
|
|
586
|
+
* upsert accepts `where`, `create`, `update`, and optionally `select`/`include`
|
|
542
587
|
* delete methods accept `where`, and optionally `select`/`include`
|
|
543
588
|
|
|
544
589
|
Shape keys not valid for the method throw `ShapeError`.
|
|
@@ -549,25 +594,38 @@ For reads: `where`, `include`, `select`, `orderBy`, `cursor`, `take`, `skip`, `d
|
|
|
549
594
|
|
|
550
595
|
For writes: `data`, `where`, `select`, `include` (select/include only on methods that return records)
|
|
551
596
|
|
|
597
|
+
For upsert: `where`, `create`, `update`, `select`, `include`
|
|
598
|
+
|
|
552
599
|
Where shapes accept scalar field filters, relation filters (`some`, `every`, `none`, `is`, `isNot`), and logical combinators (`AND`, `OR`, `NOT`).
|
|
553
600
|
|
|
601
|
+
### Shape config value validation
|
|
602
|
+
|
|
603
|
+
Shape config values are strictly validated at construction time. Fields in `orderBy`, `cursor`, `having`, `_count` (object form), `_avg`, `_sum`, `_min`, `_max` must have the value `true`. The `skip` config must be exactly `true`. Passing any other value (including `false`, numbers, or strings) throws `ShapeError`. This prevents accidental misconfiguration where a developer writes `{ orderBy: { title: false } }` expecting it to disable ordering — instead of silently enabling it, the shape is rejected.
|
|
604
|
+
|
|
554
605
|
### Where DSL is a constrained Prisma subset
|
|
555
606
|
|
|
556
607
|
The where shape syntax supports a subset of Prisma's where filter API. Notable differences from raw Prisma where clauses:
|
|
557
608
|
|
|
558
609
|
* The `not` operator accepts a scalar value only, not a nested filter object. Prisma's `{ not: { gt: 5 } }` form is not supported.
|
|
559
|
-
* `AND` and `OR` in client input must be arrays. Prisma accepts a single object for `AND`; prisma-guard requires an array.
|
|
610
|
+
* `AND` and `OR` in client input must be arrays with at least one element. Prisma accepts a single object for `AND`; prisma-guard requires an array. Empty arrays are rejected.
|
|
611
|
+
* `NOT` in client input accepts a single object or an array with at least one element. Empty arrays are rejected.
|
|
612
|
+
* Each combinator member must specify at least one condition with a defined value. Empty objects inside combinators (e.g. `{ AND: [{}] }`) are rejected when no forced values exist.
|
|
560
613
|
* Relation filter operators (`some`, `every`, `none`, `is`, `isNot`) require at least one nested condition when all conditions are client-controlled. Empty relation filters like `{ posts: { some: {} } }` are rejected.
|
|
561
|
-
* Relation operator containers require at least one operator
|
|
614
|
+
* Relation operator containers require at least one operator. `{ posts: {} }` is rejected.
|
|
615
|
+
* Empty combinator and relation filter definitions in shapes are rejected at shape construction time. A shape like `where: { AND: {} }` throws `ShapeError`.
|
|
562
616
|
* Forced where conditions are conflict-checked at shape construction time. The same field and operator with different forced values across shape branches (e.g. top-level vs inside a combinator) is rejected with `ShapeError`.
|
|
563
617
|
|
|
564
618
|
These restrictions are intentional. They prevent clients from sending structurally valid but semantically vacuous filters that could broaden query scope, particularly in bulk mutation where clauses.
|
|
565
619
|
|
|
620
|
+
### Body normalization
|
|
621
|
+
|
|
622
|
+
Read methods and mutation methods accept `undefined` or `null` as body input across all API surfaces. Missing bodies are consistently normalized to `{}` (empty object). This applies to single shapes, named shapes, and `guard.query().parse()`. An explicit body, when provided, must be a plain object.
|
|
623
|
+
|
|
566
624
|
### Supported methods
|
|
567
625
|
|
|
568
626
|
Reads: `findMany`, `findFirst`, `findFirstOrThrow`, `findUnique`, `findUniqueOrThrow`, `count`, `aggregate`, `groupBy`
|
|
569
627
|
|
|
570
|
-
Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `updateManyAndReturn`, `delete`, `deleteMany`
|
|
628
|
+
Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `updateManyAndReturn`, `upsert`, `delete`, `deleteMany`
|
|
571
629
|
|
|
572
630
|
---
|
|
573
631
|
|
|
@@ -599,6 +657,8 @@ The shape defines which fields are allowed inside each combinator. The client se
|
|
|
599
657
|
|
|
600
658
|
Forced values inside combinators are lifted to the top-level query as AND conditions, regardless of the combinator type. This means a forced value inside an `OR` shape does not become an OR branch — it becomes an additional AND constraint on the entire query. This is consistent with the fail-closed design: forced values always restrict, never broaden.
|
|
601
659
|
```ts
|
|
660
|
+
import { force } from 'prisma-guard'
|
|
661
|
+
|
|
602
662
|
await prisma.project
|
|
603
663
|
.guard({
|
|
604
664
|
where: {
|
|
@@ -619,6 +679,16 @@ Combinators can be nested and mixed with scalar fields freely. The same field ca
|
|
|
619
679
|
|
|
620
680
|
If the same field and operator appear as forced values in different parts of the shape (e.g. top-level and inside an `AND` combinator), the forced values are conflict-checked. Identical values are deduplicated. Different values throw `ShapeError` — this prevents ambiguous security configurations from silently degrading.
|
|
621
681
|
|
|
682
|
+
### Combinator validation rules
|
|
683
|
+
|
|
684
|
+
Combinator definitions in shapes must define at least one field. An empty combinator like `where: { AND: {} }` throws `ShapeError` at shape construction time. This prevents silent no-op branches that look restrictive but contribute nothing.
|
|
685
|
+
|
|
686
|
+
At runtime, combinator arrays from client input must contain at least one element. `{ AND: [] }`, `{ OR: [] }`, and `{ NOT: [] }` are all rejected.
|
|
687
|
+
|
|
688
|
+
When no forced values exist inside a combinator, each member object must specify at least one condition with a defined value. `{ AND: [{}] }` is rejected because the empty object carries no filtering constraint. This prevents clients from satisfying structural validation while bypassing semantic filtering, which is particularly important for bulk mutations where a vacuous where clause could affect all rows.
|
|
689
|
+
|
|
690
|
+
When a combinator branch contains forced values, client members may be empty because the forced values still provide meaningful filtering constraints.
|
|
691
|
+
|
|
622
692
|
---
|
|
623
693
|
|
|
624
694
|
## Relation filters in where shapes
|
|
@@ -652,6 +722,8 @@ Each relation operator value is a nested where config for the related model. All
|
|
|
652
722
|
|
|
653
723
|
### Forced values in relation filters
|
|
654
724
|
```ts
|
|
725
|
+
import { force } from 'prisma-guard'
|
|
726
|
+
|
|
655
727
|
await prisma.user
|
|
656
728
|
.guard({
|
|
657
729
|
where: {
|
|
@@ -722,11 +794,14 @@ await prisma.user
|
|
|
722
794
|
|
|
723
795
|
Using an unsupported operator for the relation type throws `ShapeError`. For example, `some` on a to-one relation or `is` on a to-many relation is rejected.
|
|
724
796
|
|
|
725
|
-
###
|
|
797
|
+
### Relation filter validation rules
|
|
798
|
+
|
|
799
|
+
Relation filter definitions in shapes must define at least one operator. An empty relation filter like `where: { posts: {} }` throws `ShapeError` at shape construction time.
|
|
800
|
+
|
|
801
|
+
Each operator's nested where config must define at least one field. An empty nested where like `where: { posts: { some: {} } }` throws `ShapeError` at shape construction time.
|
|
726
802
|
|
|
727
803
|
When all conditions inside a relation operator are client-controlled (no forced values), the client must provide at least one condition. Empty nested where objects are rejected:
|
|
728
804
|
```ts
|
|
729
|
-
// Shape allows filtering posts by title
|
|
730
805
|
where: {
|
|
731
806
|
posts: {
|
|
732
807
|
some: {
|
|
@@ -735,16 +810,15 @@ where: {
|
|
|
735
810
|
},
|
|
736
811
|
}
|
|
737
812
|
|
|
738
|
-
//
|
|
813
|
+
// Rejected — at least one condition required
|
|
739
814
|
{ where: { posts: { some: {} } } }
|
|
740
815
|
|
|
741
|
-
//
|
|
816
|
+
// Accepted
|
|
742
817
|
{ where: { posts: { some: { title: { contains: 'demo' } } } } }
|
|
743
818
|
```
|
|
744
819
|
|
|
745
820
|
When a relation operator contains forced values, the client may omit all client-controlled conditions. The forced values are still injected:
|
|
746
821
|
```ts
|
|
747
|
-
// Shape forces status filter, client controls title
|
|
748
822
|
where: {
|
|
749
823
|
posts: {
|
|
750
824
|
some: {
|
|
@@ -759,8 +833,6 @@ where: {
|
|
|
759
833
|
// Becomes: { posts: { some: { status: { equals: 'published' } } } }
|
|
760
834
|
```
|
|
761
835
|
|
|
762
|
-
Similarly, if all operators on a relation are forced or no operators have client-controlled conditions, the relation operator container itself may be empty or absent.
|
|
763
|
-
|
|
764
836
|
---
|
|
765
837
|
|
|
766
838
|
## Mutation return projection
|
|
@@ -777,6 +849,7 @@ Mutations that return records can use `select` and `include` in the guard shape
|
|
|
777
849
|
| `update` | record | yes |
|
|
778
850
|
| `updateMany` | BatchPayload | no |
|
|
779
851
|
| `updateManyAndReturn` | record[] | yes |
|
|
852
|
+
| `upsert` | record | yes |
|
|
780
853
|
| `delete` | record | yes |
|
|
781
854
|
| `deleteMany` | BatchPayload | no |
|
|
782
855
|
|
|
@@ -839,12 +912,14 @@ await prisma.project
|
|
|
839
912
|
|
|
840
913
|
Forced where conditions work the same as in reads. This is useful for ensuring tenant-scoped nested data in mutation responses:
|
|
841
914
|
```ts
|
|
915
|
+
import { force } from 'prisma-guard'
|
|
916
|
+
|
|
842
917
|
await prisma.project
|
|
843
918
|
.guard({
|
|
844
919
|
data: { title: true },
|
|
845
920
|
include: {
|
|
846
921
|
members: {
|
|
847
|
-
where: { isActive: { equals: true } },
|
|
922
|
+
where: { isActive: { equals: force(true) } },
|
|
848
923
|
},
|
|
849
924
|
},
|
|
850
925
|
})
|
|
@@ -856,9 +931,9 @@ await prisma.project
|
|
|
856
931
|
|
|
857
932
|
The returned `members` will always be filtered to `isActive = true`, regardless of what the client sends.
|
|
858
933
|
|
|
859
|
-
### Projection is optional
|
|
934
|
+
### Projection is optional by default
|
|
860
935
|
|
|
861
|
-
If the shape does not define `select` or `include`, the mutation returns the full record (default Prisma behavior).
|
|
936
|
+
If the shape does not define `select` or `include`, but the caller also omits them from the body, the mutation returns the full record (default Prisma behavior). Projection shapes only validate and constrain **client-requested** projections — they do not enforce a fixed output boundary unless [enforced projection mode](#enforced-projection-mode) is enabled.
|
|
862
937
|
|
|
863
938
|
### select and include are mutually exclusive
|
|
864
939
|
|
|
@@ -870,13 +945,165 @@ Same as reads: a shape (and a body) cannot define both `select` and `include` at
|
|
|
870
945
|
|
|
871
946
|
---
|
|
872
947
|
|
|
948
|
+
## Enforced projection mode
|
|
949
|
+
|
|
950
|
+
By default, projection shapes only constrain client-requested projections. If the client omits `select`/`include` from the body, Prisma returns its default full payload.
|
|
951
|
+
|
|
952
|
+
When `enforceProjection` is enabled, the shape's projection is always applied — even when the client does not request one. If the shape defines `select` or `include` and the client omits them, prisma-guard synthesizes a projection from the shape and passes it to Prisma.
|
|
953
|
+
|
|
954
|
+
### Configuration
|
|
955
|
+
```prisma
|
|
956
|
+
generator guard {
|
|
957
|
+
provider = "prisma-guard"
|
|
958
|
+
output = "generated/guard"
|
|
959
|
+
enforceProjection = "true"
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### Behavior
|
|
964
|
+
|
|
965
|
+
With enforced projection enabled:
|
|
966
|
+
```ts
|
|
967
|
+
await prisma.project
|
|
968
|
+
.guard({
|
|
969
|
+
data: { title: true },
|
|
970
|
+
select: { id: true, title: true },
|
|
971
|
+
})
|
|
972
|
+
.create({ data: { title: 'New' } })
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Even though the client omits `select` from the body, Prisma receives `{ select: { id: true, title: true } }` and returns only those fields.
|
|
976
|
+
|
|
977
|
+
Without enforced projection (default): Prisma returns all fields.
|
|
978
|
+
|
|
979
|
+
### Synthesized projection
|
|
980
|
+
|
|
981
|
+
When the client omits `select`/`include`, prisma-guard synthesizes a default projection body from the shape:
|
|
982
|
+
|
|
983
|
+
* Scalar fields marked `true` in the shape produce `true` in the synthesized body
|
|
984
|
+
* Nested relation shapes produce their structural equivalent (nested `select`/`include`)
|
|
985
|
+
* `_count` configurations are preserved
|
|
986
|
+
* 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
|
|
987
|
+
|
|
988
|
+
When the client does provide `select`/`include`, behavior is identical regardless of this setting: the client's projection is validated against the shape.
|
|
989
|
+
|
|
990
|
+
This mode applies to all methods that support projection: `create`, `update`, `upsert`, `delete`, `createManyAndReturn`, and `updateManyAndReturn`.
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Upsert
|
|
995
|
+
|
|
996
|
+
Upsert is supported with dedicated `create` and `update` shape keys that mirror Prisma's upsert API. The `data` key is not valid for upsert — use `create` and `update` instead.
|
|
997
|
+
```ts
|
|
998
|
+
await prisma.project
|
|
999
|
+
.guard({
|
|
1000
|
+
where: { id: { equals: true } },
|
|
1001
|
+
create: { title: true, status: true },
|
|
1002
|
+
update: { title: true },
|
|
1003
|
+
})
|
|
1004
|
+
.upsert({
|
|
1005
|
+
where: { id: { equals: 'abc123' } },
|
|
1006
|
+
create: { title: 'New Project', status: 'active' },
|
|
1007
|
+
update: { title: 'Updated Title' },
|
|
1008
|
+
})
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Shape requirements
|
|
1012
|
+
|
|
1013
|
+
Upsert shapes must define all three: `where`, `create`, and `update`. Missing any of them throws `ShapeError`. Using `data` instead of `create`/`update` throws `ShapeError`.
|
|
1014
|
+
|
|
1015
|
+
The `create` branch follows the same rules as regular create shapes: all required fields without defaults must be accounted for (as client-allowed, forced, scope FK, or `@zod .default(...)`/`@zod .catch(...)`). The `update` branch follows update rules: all fields are optional.
|
|
1016
|
+
|
|
1017
|
+
The `where` must satisfy a unique constraint with equality operators, same as `update` and `delete`.
|
|
1018
|
+
|
|
1019
|
+
### All data shape value types work
|
|
1020
|
+
```ts
|
|
1021
|
+
import { force } from 'prisma-guard'
|
|
1022
|
+
|
|
1023
|
+
await prisma.project
|
|
1024
|
+
.guard({
|
|
1025
|
+
where: { id: { equals: true } },
|
|
1026
|
+
create: {
|
|
1027
|
+
title: (base) => base.min(1).max(200),
|
|
1028
|
+
status: 'draft',
|
|
1029
|
+
isActive: force(true),
|
|
1030
|
+
},
|
|
1031
|
+
update: {
|
|
1032
|
+
title: (base) => base.min(1).max(200),
|
|
1033
|
+
},
|
|
1034
|
+
})
|
|
1035
|
+
.upsert({
|
|
1036
|
+
where: { id: { equals: 'abc123' } },
|
|
1037
|
+
create: { title: 'New Project' },
|
|
1038
|
+
update: { title: 'Updated' },
|
|
1039
|
+
})
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### Projection support
|
|
1043
|
+
|
|
1044
|
+
Upsert returns a record and supports `select` and `include`:
|
|
1045
|
+
```ts
|
|
1046
|
+
await prisma.project
|
|
1047
|
+
.guard({
|
|
1048
|
+
where: { id: { equals: true } },
|
|
1049
|
+
create: { title: true, status: true },
|
|
1050
|
+
update: { title: true },
|
|
1051
|
+
select: { id: true, title: true, status: true },
|
|
1052
|
+
})
|
|
1053
|
+
.upsert({
|
|
1054
|
+
where: { id: { equals: 'abc123' } },
|
|
1055
|
+
create: { title: 'New', status: 'active' },
|
|
1056
|
+
update: { title: 'Updated' },
|
|
1057
|
+
select: { id: true, title: true },
|
|
1058
|
+
})
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### Scope behavior
|
|
1062
|
+
|
|
1063
|
+
On scoped models, upsert is fully supported:
|
|
1064
|
+
|
|
1065
|
+
* Scope condition is merged into `where` using unique-preserving merge (same as `update` and `delete`)
|
|
1066
|
+
* Scope FK is injected into `create` data (same as regular creates)
|
|
1067
|
+
* Scope FK is stripped from `update` data (same as regular updates)
|
|
1068
|
+
* All scope roots must be present in context — missing roots throw `PolicyError`
|
|
1069
|
+
|
|
1070
|
+
### Named shapes and context-dependent shapes
|
|
1071
|
+
|
|
1072
|
+
Upsert works with named shapes and context-dependent shapes:
|
|
1073
|
+
```ts
|
|
1074
|
+
await prisma.project
|
|
1075
|
+
.guard({
|
|
1076
|
+
'/admin/projects/:id': {
|
|
1077
|
+
where: { id: { equals: true } },
|
|
1078
|
+
create: { title: true, status: true, priority: true },
|
|
1079
|
+
update: { title: true, status: true, priority: true },
|
|
1080
|
+
},
|
|
1081
|
+
'/editor/projects/:id': {
|
|
1082
|
+
where: { id: { equals: true } },
|
|
1083
|
+
create: { title: true, status: 'draft' },
|
|
1084
|
+
update: { title: true },
|
|
1085
|
+
},
|
|
1086
|
+
}, req.headers['x-caller'])
|
|
1087
|
+
.upsert({
|
|
1088
|
+
where: { id: { equals: req.params.id } },
|
|
1089
|
+
create: req.body.create,
|
|
1090
|
+
update: req.body.update,
|
|
1091
|
+
})
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
### Body keys
|
|
1095
|
+
|
|
1096
|
+
Upsert accepts: `where`, `create`, `update`, `select`, `include`. Unknown keys are rejected with `ShapeError`.
|
|
1097
|
+
|
|
1098
|
+
---
|
|
1099
|
+
|
|
873
1100
|
## Named shapes and caller routing
|
|
874
1101
|
|
|
875
1102
|
Different pages or API consumers often need different shapes for the same model. Named shapes route requests to the right shape based on a caller value.
|
|
876
1103
|
|
|
877
1104
|
Caller is provided as the second argument to `.guard()` or via the context function — never in the request body. This keeps the method body clean and Prisma-compatible.
|
|
878
1105
|
|
|
879
|
-
Caller keys must not collide with reserved shape config keys (`where`, `data`, `include`, `select`, `orderBy`, etc.). Using a reserved key as a caller path throws `ShapeError`.
|
|
1106
|
+
Caller keys must not collide with reserved shape config keys (`where`, `data`, `create`, `update`, `include`, `select`, `orderBy`, etc.). Using a reserved key as a caller path throws `ShapeError`.
|
|
880
1107
|
|
|
881
1108
|
### Define named shapes
|
|
882
1109
|
```ts
|
|
@@ -944,7 +1171,7 @@ await prisma.project
|
|
|
944
1171
|
.create({ data: req.body })
|
|
945
1172
|
```
|
|
946
1173
|
|
|
947
|
-
All data shape value types (`true`, literal, function) work in named shapes, context-dependent shapes, and single shapes.
|
|
1174
|
+
All data shape value types (`true`, literal, `force()`, function) work in named shapes, context-dependent shapes, and single shapes.
|
|
948
1175
|
|
|
949
1176
|
### Caller resolution order
|
|
950
1177
|
|
|
@@ -1005,6 +1232,8 @@ The context function returns an object with arbitrary keys. Keys whose values ar
|
|
|
1005
1232
|
|
|
1006
1233
|
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.
|
|
1007
1234
|
|
|
1235
|
+
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.
|
|
1236
|
+
|
|
1008
1237
|
Dynamic shape functions must return a plain guard shape object. If the function throws or returns a non-object value, a `ShapeError` is raised.
|
|
1009
1238
|
|
|
1010
1239
|
### Single context-dependent shape
|
|
@@ -1022,11 +1251,14 @@ await prisma.project
|
|
|
1022
1251
|
|
|
1023
1252
|
### Context-dependent data shapes with inline refines
|
|
1024
1253
|
```ts
|
|
1254
|
+
import { force } from 'prisma-guard'
|
|
1255
|
+
|
|
1025
1256
|
await prisma.project
|
|
1026
1257
|
.guard((ctx) => ({
|
|
1027
1258
|
data: {
|
|
1028
1259
|
title: (base) => base.min(1).max(ctx.role === 'admin' ? 500 : 200),
|
|
1029
1260
|
status: ctx.role === 'admin' ? true : 'draft',
|
|
1261
|
+
isActive: force(true),
|
|
1030
1262
|
},
|
|
1031
1263
|
}))
|
|
1032
1264
|
.create({ data: req.body })
|
|
@@ -1074,7 +1306,7 @@ WHERE id = ?
|
|
|
1074
1306
|
AND tenantId = ?
|
|
1075
1307
|
```
|
|
1076
1308
|
|
|
1077
|
-
This applies to all top-level operations on scoped models, including reads, writes, and deletes.
|
|
1309
|
+
This applies to all top-level operations on scoped models, including reads, writes, upserts, and deletes.
|
|
1078
1310
|
|
|
1079
1311
|
### What is scoped
|
|
1080
1312
|
|
|
@@ -1082,13 +1314,13 @@ This applies to all top-level operations on scoped models, including reads, writ
|
|
|
1082
1314
|
* All top-level creates (`create`, `createMany`, `createManyAndReturn`) — scope FK is injected into data
|
|
1083
1315
|
* All top-level unique mutations (`update`, `delete`) — scope condition is merged into where
|
|
1084
1316
|
* All top-level bulk mutations (`updateMany`, `updateManyAndReturn`, `deleteMany`) — scope condition is merged into where, scope FK is stripped from data
|
|
1317
|
+
* `upsert` — scope condition is merged into where, scope FK is injected into create data, scope FK is stripped from update data
|
|
1085
1318
|
|
|
1086
1319
|
### What is NOT scoped
|
|
1087
1320
|
|
|
1088
1321
|
* Nested reads loaded via `include` or `select` — use forced where conditions in the shape to restrict these (to-many relations only; see [Limitations](#limitations))
|
|
1089
1322
|
* Nested writes — Prisma extension hooks operate on top-level operations only. Guarded data shapes reject relation fields entirely, so nested writes are only possible through raw (unguarded) Prisma calls.
|
|
1090
1323
|
* `$queryRaw` and `$executeRaw` — raw SQL bypasses all guard protections
|
|
1091
|
-
* `upsert` on scoped models — rejected with `PolicyError`; handle explicitly in route logic
|
|
1092
1324
|
|
|
1093
1325
|
### Scope relation writes
|
|
1094
1326
|
|
|
@@ -1117,7 +1349,7 @@ A model can be scoped by multiple scope roots simultaneously. If `Project` has a
|
|
|
1117
1349
|
|
|
1118
1350
|
On reads, both scope conditions are combined with `AND`. If `onMissingScopeContext` is `"warn"` or `"ignore"`, only present roots are enforced — missing roots are skipped. If `"error"` (the default), all roots must be present.
|
|
1119
1351
|
|
|
1120
|
-
On writes, all scope roots must be present in the context. A missing root always throws `PolicyError`, regardless of `onMissingScopeContext`.
|
|
1352
|
+
On writes (including upsert), all scope roots must be present in the context. A missing root always throws `PolicyError`, regardless of `onMissingScopeContext`.
|
|
1121
1353
|
|
|
1122
1354
|
Scope foreign keys for all present roots are injected into create data and stripped from update/delete data.
|
|
1123
1355
|
|
|
@@ -1190,7 +1422,7 @@ const userOutput = guard.model('User', {
|
|
|
1190
1422
|
})
|
|
1191
1423
|
```
|
|
1192
1424
|
|
|
1193
|
-
`guard.model()` produces a non-strict schema by default, meaning
|
|
1425
|
+
`guard.model()` produces a non-strict schema by default, meaning unknown fields in the data are silently stripped from the output. For output validation where unknown fields should be rejected instead of stripped, pass `strict: true` as shown above.
|
|
1194
1426
|
|
|
1195
1427
|
Notes:
|
|
1196
1428
|
|
|
@@ -1201,6 +1433,44 @@ Notes:
|
|
|
1201
1433
|
|
|
1202
1434
|
---
|
|
1203
1435
|
|
|
1436
|
+
## Strict Decimal mode
|
|
1437
|
+
|
|
1438
|
+
By default, `Decimal` fields accept JavaScript `number`, decimal string, and Decimal-like objects. Accepting `number` is convenient but carries a precision risk: floating-point precision may already be lost by the time the validator sees the value (e.g. `0.1 + 0.2` arrives as `0.30000000000000004`).
|
|
1439
|
+
|
|
1440
|
+
Strict Decimal mode removes `number` from the accepted types, requiring decimal string or Prisma `Decimal` objects only.
|
|
1441
|
+
|
|
1442
|
+
### Configuration
|
|
1443
|
+
```prisma
|
|
1444
|
+
generator guard {
|
|
1445
|
+
provider = "prisma-guard"
|
|
1446
|
+
output = "generated/guard"
|
|
1447
|
+
strictDecimal = "true"
|
|
1448
|
+
}
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
### Behavior
|
|
1452
|
+
|
|
1453
|
+
| Mode | Accepted types |
|
|
1454
|
+
| -------- | -------------------------------------------------- |
|
|
1455
|
+
| default | `number`, decimal string, Decimal-like object |
|
|
1456
|
+
| strict | decimal string, Decimal-like object |
|
|
1457
|
+
|
|
1458
|
+
With strict mode enabled:
|
|
1459
|
+
```ts
|
|
1460
|
+
// Accepted
|
|
1461
|
+
{ price: "29.99" }
|
|
1462
|
+
{ price: new Prisma.Decimal("29.99") }
|
|
1463
|
+
|
|
1464
|
+
// Rejected
|
|
1465
|
+
{ price: 29.99 }
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
This applies globally to all `Decimal` fields across all models, in both `data` shapes and `where` filters.
|
|
1469
|
+
|
|
1470
|
+
For money or high-precision values, strict mode is recommended. Pass decimal strings (e.g. `"0.30"`) or Prisma `Decimal` objects instead of JavaScript numbers.
|
|
1471
|
+
|
|
1472
|
+
---
|
|
1473
|
+
|
|
1204
1474
|
## Security model
|
|
1205
1475
|
|
|
1206
1476
|
`prisma-guard` enforces three layers.
|
|
@@ -1227,7 +1497,7 @@ These limitations are real and should be treated as part of the security model.
|
|
|
1227
1497
|
|
|
1228
1498
|
Prisma extension hooks operate on top-level operations. Nested writes do not trigger separate scope interception.
|
|
1229
1499
|
|
|
1230
|
-
Guarded data shapes reject relation fields entirely — using a relation field in a `data` shape throws `ShapeError`. This means nested writes (e.g. `{ author: { connect: { id: '...' } } }`) are only possible through raw (unguarded) Prisma calls. The guard layer prevents them in guarded mutations, not by intercepting nested writes, but by refusing to include relation fields in the data shape.
|
|
1500
|
+
Guarded data shapes reject relation fields entirely — using a relation field in a `data`, `create`, or `update` shape throws `ShapeError`. This means nested writes (e.g. `{ author: { connect: { id: '...' } } }`) are only possible through raw (unguarded) Prisma calls. The guard layer prevents them in guarded mutations, not by intercepting nested writes, but by refusing to include relation fields in the data shape.
|
|
1231
1501
|
|
|
1232
1502
|
### Nested reads via include are not scope-filtered
|
|
1233
1503
|
|
|
@@ -1281,7 +1551,9 @@ Guard `having` shapes support scalar field filters with their type-appropriate o
|
|
|
1281
1551
|
|
|
1282
1552
|
### `refine` and inline refine functions replace `@zod` chains
|
|
1283
1553
|
|
|
1284
|
-
When a `refine` callback is provided for a field in `guard.input()`, or when a function is used instead of `true` in a `data` shape, the callback receives the base Zod type without `@zod` chains. The `@zod` directive for that field is bypassed entirely. See [Schema annotations](#refine-replaces-zod-chains).
|
|
1554
|
+
When a `refine` callback is provided for a field in `guard.input()`, or when a function is used instead of `true` in a `data`/`create`/`update` shape, the callback receives the base Zod type without `@zod` chains. The `@zod` directive for that field is bypassed entirely. See [Schema annotations](#refine-replaces-zod-chains).
|
|
1555
|
+
|
|
1556
|
+
If the refine function returns a schema that handles undefined input (produces a non-undefined value for undefined), prisma-guard detects this and preserves the behavior by not wrapping with `.optional()` in create mode.
|
|
1285
1557
|
|
|
1286
1558
|
### `pick` and `omit` are mutually exclusive
|
|
1287
1559
|
|
|
@@ -1289,7 +1561,7 @@ Both `guard.input()` and `guard.model()` reject configurations that specify both
|
|
|
1289
1561
|
|
|
1290
1562
|
### `@zod` field modifiers interact with prisma-guard nullability
|
|
1291
1563
|
|
|
1292
|
-
Using `@zod .optional()`, `.nullable()`, or `.nullish()` applies the Zod method on top of prisma-guard's own nullability/optionality handling. This can cause double-wrapping. These modifiers are available but should only be used when intentionally overriding default behavior.
|
|
1564
|
+
Using `@zod .optional()`, `.nullable()`, or `.nullish()` applies the Zod method on top of prisma-guard's own nullability/optionality handling. This can cause double-wrapping. These modifiers are available but should only be used when intentionally overriding default behavior. Exception: when a chain contains `.default()` or `.catch()`, prisma-guard skips adding `.optional()` in create mode to preserve the default/catch behavior.
|
|
1293
1565
|
|
|
1294
1566
|
### Inline refine functions are not cached
|
|
1295
1567
|
|
|
@@ -1301,15 +1573,15 @@ Prisma supports negative `take` for reverse cursor pagination. prisma-guard rest
|
|
|
1301
1573
|
|
|
1302
1574
|
### `skip` in shape config is a permission flag
|
|
1303
1575
|
|
|
1304
|
-
`skip: true` in a shape config means the client is allowed to provide a `skip` value. The actual `skip` value must be a non-negative integer. This is consistent with other shape flags but differs from `take`, which uses `{ max, default? }` syntax.
|
|
1576
|
+
`skip: true` in a shape config means the client is allowed to provide a `skip` value. The actual `skip` value must be a non-negative integer. The value must be exactly `true` — other truthy values are rejected with `ShapeError`. This is consistent with other shape flags but differs from `take`, which uses `{ max, default? }` syntax.
|
|
1305
1577
|
|
|
1306
1578
|
### `guard.input()` defaults to allowing null for nullable fields
|
|
1307
1579
|
|
|
1308
1580
|
`guard.input()` defaults `allowNull` to `true`, matching the behavior of `.guard({ data: ... })` and Prisma's own nullable field handling. Pass `allowNull: false` to reject null values for nullable fields.
|
|
1309
1581
|
|
|
1310
|
-
### `Decimal` fields accept JavaScript numbers
|
|
1582
|
+
### `Decimal` fields accept JavaScript numbers by default
|
|
1311
1583
|
|
|
1312
|
-
The `Decimal` base type accepts JavaScript `number`, decimal string, and Decimal-like objects. Accepting `number` is convenient but carries a precision risk: by the time the validator sees the value, floating-point precision may already be lost. For example, `0.1 + 0.2` arrives as `0.30000000000000004`. For money or high-precision values, pass decimal strings (e.g. `"0.30"`) or Prisma `Decimal` objects instead of JavaScript numbers.
|
|
1584
|
+
The `Decimal` base type accepts JavaScript `number`, decimal string, and Decimal-like objects by default. Accepting `number` is convenient but carries a precision risk: by the time the validator sees the value, floating-point precision may already be lost. For example, `0.1 + 0.2` arrives as `0.30000000000000004`. For money or high-precision values, enable [strict Decimal mode](#strict-decimal-mode) or pass decimal strings (e.g. `"0.30"`) or Prisma `Decimal` objects instead of JavaScript numbers.
|
|
1313
1585
|
|
|
1314
1586
|
### `skipDuplicates` is supported for batch create methods
|
|
1315
1587
|
|
|
@@ -1317,12 +1589,32 @@ The `Decimal` base type accepts JavaScript `number`, decimal string, and Decimal
|
|
|
1317
1589
|
|
|
1318
1590
|
### Guarded data shapes do not permit relation fields
|
|
1319
1591
|
|
|
1320
|
-
Relation fields in `data` shapes are rejected with `ShapeError`. Nested writes (e.g. `{ author: { connect: { id: '...' } } }`) are only possible through raw (unguarded) Prisma calls. The guard layer does not intercept or validate nested writes — it prevents them entirely in guarded mutations.
|
|
1592
|
+
Relation fields in `data`, `create`, and `update` shapes are rejected with `ShapeError`. Nested writes (e.g. `{ author: { connect: { id: '...' } } }`) are only possible through raw (unguarded) Prisma calls. The guard layer does not intercept or validate nested writes — it prevents them entirely in guarded mutations.
|
|
1321
1593
|
|
|
1322
1594
|
### Conflicting forced where values are rejected
|
|
1323
1595
|
|
|
1324
1596
|
If the same field and operator appear as forced values in different parts of a where shape (e.g. at the top level and inside an `AND` combinator) with different values, the shape is rejected with `ShapeError` at construction time. Identical duplicate forced values are deduplicated silently. This prevents ambiguous security configurations from silently degrading.
|
|
1325
1597
|
|
|
1598
|
+
### Mutation projection shapes do not enforce a fixed output boundary by default
|
|
1599
|
+
|
|
1600
|
+
If a mutation shape defines `select` or `include` but the client omits them from the body, Prisma returns its default full payload. Projection shapes only validate and constrain client-requested projections. Enable [enforced projection mode](#enforced-projection-mode) to always apply the shape's projection.
|
|
1601
|
+
|
|
1602
|
+
### `create` and `update` are reserved shape keys
|
|
1603
|
+
|
|
1604
|
+
The bare words `create` and `update` cannot be used as caller keys in named shape routing, as they are reserved for upsert shape configuration. Full paths like `'/admin/create'` or `'/api/users/update'` are unaffected — only the bare words collide.
|
|
1605
|
+
|
|
1606
|
+
### Empty `select` and `include` shapes are rejected
|
|
1607
|
+
|
|
1608
|
+
An empty `select: {}` or `include: {}` in a guard shape throws `ShapeError` at shape construction time. This is consistent with the fail-closed design applied to empty combinators, empty relation filters, and empty operator objects.
|
|
1609
|
+
|
|
1610
|
+
### Shape config values must be exactly `true`
|
|
1611
|
+
|
|
1612
|
+
Fields in `orderBy`, `cursor`, `having`, `_count` (object form), `_avg`, `_sum`, `_min`, `_max` config objects must have the value `true`. The `skip` config must be exactly `true`. Passing `false`, numbers, strings, or any other value throws `ShapeError`. This prevents misconfiguration where `false` is silently treated as enabled.
|
|
1613
|
+
|
|
1614
|
+
### `@zod .catch()` fields are tracked alongside `.default()` fields
|
|
1615
|
+
|
|
1616
|
+
Both `@zod .default(...)` and `@zod .catch(...)` are tracked in the generated `ZOD_DEFAULTS` map. Fields with either directive are exempted from create completeness checks and auto-injected as forced values when omitted from data shapes. The `.catch()` behavior (fallback on parse error) is preserved in create mode by not wrapping the schema with `.optional()`.
|
|
1617
|
+
|
|
1326
1618
|
---
|
|
1327
1619
|
|
|
1328
1620
|
## Advanced: SQL-backed runtimes
|
|
@@ -1349,9 +1641,9 @@ Libraries like **prisma-sql** make this possible for advanced architectures.
|
|
|
1349
1641
|
`.guard(shape).method(body)` may throw:
|
|
1350
1642
|
|
|
1351
1643
|
* `ZodError` — Zod validation failures on data or query args (unless `wrapZodErrors` is enabled)
|
|
1352
|
-
* `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,
|
|
1644
|
+
* `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
|
|
1353
1645
|
* `CallerError` — missing, unknown, or ambiguous caller in named shapes, or `caller` found in request body
|
|
1354
|
-
* `PolicyError` — denied scope, missing tenant context, invalid context function return value, or rejected operations on scoped models (e.g.
|
|
1646
|
+
* `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)
|
|
1355
1647
|
|
|
1356
1648
|
All guard errors include `status` and `code` properties for HTTP response mapping:
|
|
1357
1649
|
|
|
@@ -1393,8 +1685,8 @@ It reads the Prisma DMMF and emits:
|
|
|
1393
1685
|
* `ENUM_MAP` — enum values
|
|
1394
1686
|
* `SCOPE_MAP` — foreign key → scope root mappings
|
|
1395
1687
|
* `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.)
|
|
1396
|
-
* `ZOD_DEFAULTS` — per-model list of fields that have `@zod .default(...)`, used by the create completeness check
|
|
1397
|
-
* `GUARD_CONFIG` — generator config values
|
|
1688
|
+
* `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
|
|
1689
|
+
* `GUARD_CONFIG` — generator config values (including `strictDecimal` and `enforceProjection`)
|
|
1398
1690
|
* `UNIQUE_MAP` — unique constraint metadata per model
|
|
1399
1691
|
* `client.ts` — pre-wired guard instance with typed model extensions
|
|
1400
1692
|
|
|
@@ -1407,9 +1699,17 @@ At runtime, `guard.extension()` creates a Prisma extension that provides:
|
|
|
1407
1699
|
|
|
1408
1700
|
The `.guard()` call validates against the shape, merges forced values, and delegates to the underlying Prisma method. The scope layer runs transparently underneath.
|
|
1409
1701
|
|
|
1702
|
+
For create operations, fields tracked in `ZOD_DEFAULTS` that are omitted from the data shape are auto-injected as forced values. The runtime evaluates the field's Zod schema with `undefined` input and uses the resulting value. This ensures `@zod .default(...)` and `@zod .catch(...)` produce correct data even when the field is not listed in the shape.
|
|
1703
|
+
|
|
1704
|
+
For fields that ARE listed in the data shape with `true` and have `@zod .default(...)` or `@zod .catch(...)`, the runtime skips wrapping the schema with `.optional()` in create mode. This preserves the Zod default/catch behavior: omitting the field from client input triggers the default rather than passing `undefined` through.
|
|
1705
|
+
|
|
1410
1706
|
Caller routing is resolved before method execution: the explicit `caller` argument takes priority, then `contextFn().caller`, then absent (which is fine for single shapes but throws `CallerError` for named shape maps).
|
|
1411
1707
|
|
|
1412
|
-
The context function is validated on every code path that consumes it — scope injection, caller resolution, and dynamic shape evaluation all enforce the plain-object contract and throw `PolicyError` for invalid returns.
|
|
1708
|
+
The context function is validated on every code path that consumes it — scope injection, caller resolution, and dynamic shape evaluation all enforce the plain-object contract and throw `PolicyError` for invalid returns. Additionally, if a context key matches a known scope root but has a non-primitive value, `PolicyError` is thrown immediately rather than silently dropping the scope.
|
|
1709
|
+
|
|
1710
|
+
### The `force()` helper
|
|
1711
|
+
|
|
1712
|
+
The `force()` function is exported from `prisma-guard` and creates a wrapper object with an internal symbol marker. At runtime, shape processing checks for this marker to distinguish forced values from the `true` sentinel. The wrapper is unwrapped before Zod validation, so the forced value is validated against the field's schema like any other literal. The symbol is not enumerable and does not interfere with serialization or inspection of shape objects.
|
|
1413
1713
|
|
|
1414
1714
|
---
|
|
1415
1715
|
|
|
@@ -1440,6 +1740,8 @@ In most real applications, overhead should be negligible relative to database ro
|
|
|
1440
1740
|
|
|
1441
1741
|
Data schemas containing inline refine functions are also not cached, since the function could close over runtime context values. Data shapes using only `true` and literal values are cached normally within their invocation scope. Projection schemas (select/include on mutations) are cached independently for static shapes within their invocation scope.
|
|
1442
1742
|
|
|
1743
|
+
For upsert, `create` and `update` data schemas are cached independently under namespaced keys (`upsert:create`, `upsert:update`) to avoid cache collisions with regular create/update operations.
|
|
1744
|
+
|
|
1443
1745
|
---
|
|
1444
1746
|
|
|
1445
1747
|
## Security philosophy
|
|
@@ -1450,23 +1752,30 @@ Data schemas containing inline refine functions are also not cached, since the f
|
|
|
1450
1752
|
| -------------------------------------- | ----------------------------------------------------- |
|
|
1451
1753
|
| ambiguous scope mapping | error by default |
|
|
1452
1754
|
| missing scope context | error by default |
|
|
1755
|
+
| invalid scope root value type | error always |
|
|
1453
1756
|
| `onMissingScopeContext = "ignore"` | scope bypassed for missing roots; present roots still enforced |
|
|
1454
1757
|
| unsafe scoped `findUnique` | reject recommended |
|
|
1455
1758
|
| invalid `@zod` directive | error by default |
|
|
1456
1759
|
| missing `caller` in named shapes | error always |
|
|
1457
1760
|
| `caller` in request body | error always |
|
|
1458
1761
|
| `data` in read shape | error always |
|
|
1762
|
+
| `data` in upsert shape | error always (use `create`/`update`) |
|
|
1763
|
+
| missing `create` or `update` in upsert | error always |
|
|
1459
1764
|
| missing `data` in write shape | error always |
|
|
1460
1765
|
| bulk mutation without `where` shape | error always |
|
|
1461
1766
|
| bulk mutation with empty `where` | error always |
|
|
1462
|
-
|
|
|
1767
|
+
| vacuous combinator input | error always |
|
|
1768
|
+
| empty combinator/relation shape branch | error always |
|
|
1769
|
+
| empty `select`/`include` shape | error always |
|
|
1463
1770
|
| unexpected keys in mutation body | error always |
|
|
1464
1771
|
| unknown keys in shape config | error always |
|
|
1772
|
+
| non-`true` values in shape config | error always |
|
|
1465
1773
|
| cursor not covering unique constraint | error always |
|
|
1466
1774
|
| caller key collides with shape config | error always |
|
|
1467
1775
|
| empty operator objects in where | error always |
|
|
1468
1776
|
| empty relation filter (no forced) | error always |
|
|
1469
1777
|
| empty relation operator container | error always |
|
|
1778
|
+
| empty relation filter shape definition | error always |
|
|
1470
1779
|
| `pick` and `omit` both specified | error always |
|
|
1471
1780
|
| scope relation in mutation data | controlled by `onScopeRelationWrite` (default: error) |
|
|
1472
1781
|
| incomplete create data shape | error always |
|
|
@@ -1480,6 +1789,7 @@ Data schemas containing inline refine functions are also not cached, since the f
|
|
|
1480
1789
|
| context function returns non-object | error always (PolicyError) |
|
|
1481
1790
|
| conflicting forced where values | error always (ShapeError) |
|
|
1482
1791
|
| invalid context function return | error always (PolicyError) |
|
|
1792
|
+
| `@zod .default()`/`.catch()` field omitted from shape | auto-injected as forced value |
|
|
1483
1793
|
|
|
1484
1794
|
---
|
|
1485
1795
|
|
|
@@ -1493,6 +1803,8 @@ generator guard {
|
|
|
1493
1803
|
onMissingScopeContext = "error"
|
|
1494
1804
|
findUniqueMode = "reject"
|
|
1495
1805
|
onScopeRelationWrite = "error"
|
|
1806
|
+
strictDecimal = "true"
|
|
1807
|
+
enforceProjection = "true"
|
|
1496
1808
|
}
|
|
1497
1809
|
```
|
|
1498
1810
|
|
|
@@ -1540,6 +1852,9 @@ Node 22
|
|
|
1540
1852
|
5. Keep scope rules explicit and schema-driven
|
|
1541
1853
|
6. One chain — shape defines the boundary, method executes
|
|
1542
1854
|
7. Method bodies stay Prisma-compatible — routing and context live in `.guard()`
|
|
1855
|
+
8. No overloaded sentinel values — `true` always means client-controlled, `force()` for forced booleans
|
|
1856
|
+
9. Upsert uses `create`/`update` keys, not `data` — matches Prisma's own API shape
|
|
1857
|
+
10. Shape config values are validated strictly — `true` means enabled, anything else is rejected
|
|
1543
1858
|
|
|
1544
1859
|
---
|
|
1545
1860
|
|
|
@@ -1555,16 +1870,23 @@ Node 22
|
|
|
1555
1870
|
| Caller-based shape routing | yes | no |
|
|
1556
1871
|
| Typed method chaining | yes | n/a |
|
|
1557
1872
|
| Bulk mutation safety | required where | not handled |
|
|
1873
|
+
| Vacuous combinator rejection | yes | not handled |
|
|
1558
1874
|
| Mutation body validation | strict keys | no |
|
|
1559
|
-
| Shape config validation | strict
|
|
1875
|
+
| Shape config validation | strict values | n/a |
|
|
1560
1876
|
| Create completeness validation | yes | no |
|
|
1561
1877
|
| Mutation return projection | yes | manual |
|
|
1878
|
+
| Enforced projection mode | opt-in | no |
|
|
1879
|
+
| Upsert support | yes | manual |
|
|
1562
1880
|
| Inline field refine in data shapes | yes | n/a |
|
|
1563
1881
|
| ZodError wrapping | opt-in | n/a |
|
|
1564
1882
|
| Logical combinators in where | yes | manual |
|
|
1565
1883
|
| Relation filters in where | yes | manual |
|
|
1566
1884
|
| Empty relation filter rejection | yes | n/a |
|
|
1885
|
+
| Empty projection shape rejection | yes | n/a |
|
|
1567
1886
|
| Forced where conflict detection | yes | n/a |
|
|
1887
|
+
| Forced boolean values via `force()` | yes | n/a |
|
|
1888
|
+
| Strict Decimal mode | opt-in | n/a |
|
|
1889
|
+
| `@zod .default()`/`.catch()` auto-injection | yes | n/a |
|
|
1568
1890
|
|
|
1569
1891
|
---
|
|
1570
1892
|
|
|
@@ -1574,11 +1896,9 @@ Possible future improvements:
|
|
|
1574
1896
|
|
|
1575
1897
|
* optional nested-write enforcement helpers
|
|
1576
1898
|
* richer relation-level policies
|
|
1577
|
-
* more query method coverage (upsert)
|
|
1578
1899
|
* adapter integrations for SQL-backed runtimes
|
|
1579
1900
|
* model-specific generated types for stronger compile-time shape validation
|
|
1580
1901
|
* structured JSON field validation via schema annotations
|
|
1581
|
-
* optional strict Decimal mode (string-only, no JavaScript number)
|
|
1582
1902
|
|
|
1583
1903
|
---
|
|
1584
1904
|
|