prisma-guard 1.1.0 → 1.2.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 +365 -89
- package/dist/generator/index.js +98 -16
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.d.ts +7 -4
- package/dist/runtime/index.js +1165 -890
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,10 +38,13 @@ database
|
|
|
38
38
|
* [Before / After prisma-guard](#before--after-prisma-guard)
|
|
39
39
|
* [Schema annotations](#schema-annotations)
|
|
40
40
|
* [The guard API](#the-guard-api)
|
|
41
|
+
* [Logical combinators in where shapes](#logical-combinators-in-where-shapes)
|
|
42
|
+
* [Relation filters in where shapes](#relation-filters-in-where-shapes)
|
|
41
43
|
* [Mutation return projection](#mutation-return-projection)
|
|
42
44
|
* [Named shapes and caller routing](#named-shapes-and-caller-routing)
|
|
43
45
|
* [Context-dependent shapes](#context-dependent-shapes)
|
|
44
46
|
* [Automatic tenant isolation](#automatic-tenant-isolation)
|
|
47
|
+
* [Multi-root scope behavior](#multi-root-scope-behavior)
|
|
45
48
|
* [findUnique behavior](#findunique-behavior)
|
|
46
49
|
* [Output shaping](#output-shaping)
|
|
47
50
|
* [Security model](#security-model)
|
|
@@ -300,9 +303,9 @@ model Tenant {
|
|
|
300
303
|
}
|
|
301
304
|
```
|
|
302
305
|
|
|
303
|
-
Models with a single unambiguous foreign key to a scope root are auto-scoped.
|
|
306
|
+
Models with a single unambiguous foreign key to a scope root are auto-scoped. A model can be scoped by multiple roots if it has foreign keys to different scope root models — see [Multi-root scope behavior](#multi-root-scope-behavior).
|
|
304
307
|
|
|
305
|
-
If a model has multiple foreign keys to the same scope root,
|
|
308
|
+
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.
|
|
306
309
|
|
|
307
310
|
### Add field-level validation with `@zod`
|
|
308
311
|
```prisma
|
|
@@ -317,7 +320,7 @@ model User {
|
|
|
317
320
|
|
|
318
321
|
`@zod` chains apply automatically when the field appears in a `data` shape with `true`.
|
|
319
322
|
|
|
320
|
-
`@zod` directives are validated during `prisma generate`. The generator validates directive syntax, checks that each chained method
|
|
323
|
+
`@zod` directives are validated during `prisma generate`. The generator validates directive syntax, checks that each chained method is in the allowed list, and verifies method compatibility by advancing through the chain. Type-changing methods (such as `.nullable()`, `.optional()`, `.default()`) advance the schema type, so a chain like `.nullable().email()` is correctly rejected if `.email()` does not exist on the nullable wrapper. Note: some argument-level type mismatches may only be caught if Zod throws at schema construction time.
|
|
321
324
|
|
|
322
325
|
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.
|
|
323
326
|
|
|
@@ -335,7 +338,9 @@ The `@zod` DSL supports a restricted subset of Zod methods. These are the allowe
|
|
|
335
338
|
|
|
336
339
|
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.
|
|
337
340
|
|
|
338
|
-
Note on `default`: a `@zod .default(...)` chain
|
|
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 both Prisma's `@default` attribute and `@zod .default(...)` — a required field with either source of default is not flagged as missing from create data shapes. Prisma's `@default` remains the primary source of truth; `@zod .default(...)` is an additional signal.
|
|
342
|
+
|
|
343
|
+
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()`).
|
|
339
344
|
|
|
340
345
|
### Supported argument types in `@zod` directives
|
|
341
346
|
|
|
@@ -354,6 +359,8 @@ guard.input('User', {
|
|
|
354
359
|
|
|
355
360
|
In this example, any `@zod` directive on the `email` field in the Prisma schema is ignored. The `refine` callback is the sole source of validation for that field.
|
|
356
361
|
|
|
362
|
+
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
|
+
|
|
357
364
|
---
|
|
358
365
|
|
|
359
366
|
## The guard API
|
|
@@ -382,38 +389,7 @@ await prisma.project
|
|
|
382
389
|
|
|
383
390
|
In this example, `title` and `priority` use inline refines for custom validation and error messages, `status` uses `@zod` chains from the Prisma schema, and `createdBy` is forced to `currentUserId` regardless of client input.
|
|
384
391
|
|
|
385
|
-
|
|
386
|
-
```ts
|
|
387
|
-
await prisma.project
|
|
388
|
-
.guard({
|
|
389
|
-
'/admin/projects': {
|
|
390
|
-
data: {
|
|
391
|
-
title: (base) => base.min(1).max(500),
|
|
392
|
-
status: true,
|
|
393
|
-
priority: (base) => base.refine(v => v >= 1 && v <= 10, 'Priority 1-10'),
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
'/editor/projects': {
|
|
397
|
-
data: {
|
|
398
|
-
title: (base) => base.min(1).max(200),
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
})
|
|
402
|
-
.create({
|
|
403
|
-
caller: req.headers['x-caller'],
|
|
404
|
-
data: req.body,
|
|
405
|
-
})
|
|
406
|
-
```
|
|
407
|
-
```ts
|
|
408
|
-
await prisma.project
|
|
409
|
-
.guard((ctx) => ({
|
|
410
|
-
data: {
|
|
411
|
-
title: (base) => base.min(1).max(ctx.role === 'admin' ? 500 : 200),
|
|
412
|
-
status: ctx.role === 'admin' ? true : 'draft',
|
|
413
|
-
},
|
|
414
|
-
}))
|
|
415
|
-
.create({ data: req.body })
|
|
416
|
-
```
|
|
392
|
+
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).
|
|
417
393
|
|
|
418
394
|
### Query shape syntax
|
|
419
395
|
|
|
@@ -443,7 +419,7 @@ await prisma.project
|
|
|
443
419
|
|
|
444
420
|
Only `title` and `status` are accepted from the client. `@zod` chains apply automatically.
|
|
445
421
|
|
|
446
|
-
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),
|
|
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.
|
|
447
423
|
|
|
448
424
|
### Updates
|
|
449
425
|
```ts
|
|
@@ -487,6 +463,8 @@ await prisma.project
|
|
|
487
463
|
|
|
488
464
|
`status = 'published'` is always enforced. The client can only control the `title` filter.
|
|
489
465
|
|
|
466
|
+
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
|
+
|
|
490
468
|
### Deletes
|
|
491
469
|
```ts
|
|
492
470
|
await prisma.project
|
|
@@ -516,6 +494,8 @@ Each item in the array is validated against the same data shape.
|
|
|
516
494
|
|
|
517
495
|
In guarded mode, `createMany` and `createManyAndReturn` require `data` to be an array. Single-object data is not silently wrapped.
|
|
518
496
|
|
|
497
|
+
`createMany` and `createManyAndReturn` also accept `skipDuplicates: boolean` in the request body. This is passed through to Prisma without shape-level configuration.
|
|
498
|
+
|
|
519
499
|
### Bulk mutations
|
|
520
500
|
|
|
521
501
|
`updateMany`, `updateManyAndReturn`, and `deleteMany` require a `where` shape in the guard definition. This prevents accidental unconstrained bulk writes.
|
|
@@ -539,16 +519,20 @@ Mutation bodies are strictly validated. The accepted keys depend on whether the
|
|
|
539
519
|
|
|
540
520
|
Without projection in shape:
|
|
541
521
|
|
|
542
|
-
* `create
|
|
522
|
+
* `create`: `data`
|
|
523
|
+
* `createMany`, `createManyAndReturn`: `data`, `skipDuplicates`
|
|
543
524
|
* `update`, `updateMany`, `updateManyAndReturn`: `data`, `where`
|
|
544
525
|
* `delete`, `deleteMany`: `where`
|
|
545
526
|
|
|
546
527
|
With projection in shape (methods that support it):
|
|
547
528
|
|
|
548
|
-
* `create
|
|
529
|
+
* `create`: `data`, `select`, `include`
|
|
530
|
+
* `createManyAndReturn`: `data`, `select`, `include`, `skipDuplicates`
|
|
549
531
|
* `update`, `updateManyAndReturn`: `data`, `where`, `select`, `include`
|
|
550
532
|
* `delete`: `where`, `select`, `include`
|
|
551
533
|
|
|
534
|
+
For `createMany` and `createManyAndReturn`, `skipDuplicates` is also accepted as a body key. It must be a boolean if provided.
|
|
535
|
+
|
|
552
536
|
Unknown keys are rejected with `ShapeError`. If the body contains `select` or `include` but the shape does not define them, the request is rejected.
|
|
553
537
|
|
|
554
538
|
Guard shape keys are also validated per method:
|
|
@@ -565,6 +549,20 @@ For reads: `where`, `include`, `select`, `orderBy`, `cursor`, `take`, `skip`, `d
|
|
|
565
549
|
|
|
566
550
|
For writes: `data`, `where`, `select`, `include` (select/include only on methods that return records)
|
|
567
551
|
|
|
552
|
+
Where shapes accept scalar field filters, relation filters (`some`, `every`, `none`, `is`, `isNot`), and logical combinators (`AND`, `OR`, `NOT`).
|
|
553
|
+
|
|
554
|
+
### Where DSL is a constrained Prisma subset
|
|
555
|
+
|
|
556
|
+
The where shape syntax supports a subset of Prisma's where filter API. Notable differences from raw Prisma where clauses:
|
|
557
|
+
|
|
558
|
+
* 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.
|
|
560
|
+
* 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 when no forced values are present. `{ posts: {} }` is rejected.
|
|
562
|
+
* 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
|
+
|
|
564
|
+
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
|
+
|
|
568
566
|
### Supported methods
|
|
569
567
|
|
|
570
568
|
Reads: `findMany`, `findFirst`, `findFirstOrThrow`, `findUnique`, `findUniqueOrThrow`, `count`, `aggregate`, `groupBy`
|
|
@@ -573,6 +571,198 @@ Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `
|
|
|
573
571
|
|
|
574
572
|
---
|
|
575
573
|
|
|
574
|
+
## Logical combinators in where shapes
|
|
575
|
+
|
|
576
|
+
Where shapes support `AND`, `OR`, and `NOT` to compose filter conditions. The combinator value is a where config defining allowed fields inside the combinator:
|
|
577
|
+
```ts
|
|
578
|
+
await prisma.project
|
|
579
|
+
.guard({
|
|
580
|
+
where: {
|
|
581
|
+
OR: {
|
|
582
|
+
title: { contains: true },
|
|
583
|
+
description: { contains: true },
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
take: { max: 50 },
|
|
587
|
+
})
|
|
588
|
+
.findMany({
|
|
589
|
+
where: {
|
|
590
|
+
OR: [
|
|
591
|
+
{ title: { contains: 'demo' } },
|
|
592
|
+
{ description: { contains: 'demo' } },
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
})
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
The shape defines which fields are allowed inside each combinator. The client sends arrays for `AND`/`OR` and an object or array for `NOT`.
|
|
599
|
+
|
|
600
|
+
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
|
+
```ts
|
|
602
|
+
await prisma.project
|
|
603
|
+
.guard({
|
|
604
|
+
where: {
|
|
605
|
+
title: { contains: true },
|
|
606
|
+
NOT: {
|
|
607
|
+
status: { equals: 'archived' },
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
})
|
|
611
|
+
.findMany({
|
|
612
|
+
where: { title: { contains: 'demo' } },
|
|
613
|
+
})
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
`status = 'archived'` is always excluded regardless of client input.
|
|
617
|
+
|
|
618
|
+
Combinators can be nested and mixed with scalar fields freely. The same field can appear both at the top level and inside a combinator.
|
|
619
|
+
|
|
620
|
+
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
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Relation filters in where shapes
|
|
625
|
+
|
|
626
|
+
Where shapes support relation-level filters using Prisma's relation operators. To-many relations support `some`, `every`, and `none`. To-one relations support `is` and `isNot`.
|
|
627
|
+
```ts
|
|
628
|
+
await prisma.user
|
|
629
|
+
.guard({
|
|
630
|
+
where: {
|
|
631
|
+
posts: {
|
|
632
|
+
some: {
|
|
633
|
+
title: { contains: true },
|
|
634
|
+
published: { equals: true },
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
})
|
|
639
|
+
.findMany({
|
|
640
|
+
where: {
|
|
641
|
+
posts: {
|
|
642
|
+
some: {
|
|
643
|
+
title: { contains: 'guide' },
|
|
644
|
+
published: { equals: true },
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Each relation operator value is a nested where config for the related model. All where features — scalar operators, forced values, logical combinators, and nested relation filters — work recursively inside relation filters.
|
|
652
|
+
|
|
653
|
+
### Forced values in relation filters
|
|
654
|
+
```ts
|
|
655
|
+
await prisma.user
|
|
656
|
+
.guard({
|
|
657
|
+
where: {
|
|
658
|
+
posts: {
|
|
659
|
+
some: {
|
|
660
|
+
title: { contains: true },
|
|
661
|
+
status: { equals: 'published' },
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
})
|
|
666
|
+
.findMany({
|
|
667
|
+
where: {
|
|
668
|
+
posts: {
|
|
669
|
+
some: { title: { contains: 'guide' } },
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
})
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
`status = 'published'` is always enforced inside the `some` operator.
|
|
676
|
+
|
|
677
|
+
### To-one relations
|
|
678
|
+
```ts
|
|
679
|
+
await prisma.post
|
|
680
|
+
.guard({
|
|
681
|
+
where: {
|
|
682
|
+
author: {
|
|
683
|
+
is: {
|
|
684
|
+
role: { equals: true },
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
})
|
|
689
|
+
.findMany({
|
|
690
|
+
where: {
|
|
691
|
+
author: {
|
|
692
|
+
is: { role: { equals: 'ADMIN' } },
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
})
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Combined with logical combinators
|
|
699
|
+
```ts
|
|
700
|
+
await prisma.user
|
|
701
|
+
.guard({
|
|
702
|
+
where: {
|
|
703
|
+
OR: {
|
|
704
|
+
posts: {
|
|
705
|
+
some: { published: { equals: true } },
|
|
706
|
+
},
|
|
707
|
+
profile: {
|
|
708
|
+
is: { bio: { contains: true } },
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
})
|
|
713
|
+
.findMany({
|
|
714
|
+
where: {
|
|
715
|
+
OR: [
|
|
716
|
+
{ posts: { some: { published: { equals: true } } } },
|
|
717
|
+
{ profile: { is: { bio: { contains: 'engineer' } } } },
|
|
718
|
+
],
|
|
719
|
+
},
|
|
720
|
+
})
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
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
|
+
|
|
725
|
+
### Non-empty relation filter enforcement
|
|
726
|
+
|
|
727
|
+
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
|
+
```ts
|
|
729
|
+
// Shape allows filtering posts by title
|
|
730
|
+
where: {
|
|
731
|
+
posts: {
|
|
732
|
+
some: {
|
|
733
|
+
title: { contains: true },
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// This is rejected — at least one condition required
|
|
739
|
+
{ where: { posts: { some: {} } } }
|
|
740
|
+
|
|
741
|
+
// This is accepted
|
|
742
|
+
{ where: { posts: { some: { title: { contains: 'demo' } } } } }
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
When a relation operator contains forced values, the client may omit all client-controlled conditions. The forced values are still injected:
|
|
746
|
+
```ts
|
|
747
|
+
// Shape forces status filter, client controls title
|
|
748
|
+
where: {
|
|
749
|
+
posts: {
|
|
750
|
+
some: {
|
|
751
|
+
status: { equals: 'published' },
|
|
752
|
+
title: { contains: true },
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Accepted — forced status is still applied
|
|
758
|
+
{ where: { posts: { some: {} } } }
|
|
759
|
+
// Becomes: { posts: { some: { status: { equals: 'published' } } } }
|
|
760
|
+
```
|
|
761
|
+
|
|
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
|
+
---
|
|
765
|
+
|
|
576
766
|
## Mutation return projection
|
|
577
767
|
|
|
578
768
|
Mutations that return records can use `select` and `include` in the guard shape to control which fields and relations are returned. This uses the same shape syntax as reads — the shape whitelists what the client may request, and forced where conditions on nested includes work identically.
|
|
@@ -682,7 +872,9 @@ Same as reads: a shape (and a body) cannot define both `select` and `include` at
|
|
|
682
872
|
|
|
683
873
|
## Named shapes and caller routing
|
|
684
874
|
|
|
685
|
-
Different pages or API consumers often need different shapes for the same model. Named shapes route requests to the right shape based on a
|
|
875
|
+
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
|
+
|
|
877
|
+
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.
|
|
686
878
|
|
|
687
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`.
|
|
688
880
|
|
|
@@ -698,11 +890,8 @@ await prisma.project
|
|
|
698
890
|
where: { title: { contains: true } },
|
|
699
891
|
take: { max: 20, default: 10 },
|
|
700
892
|
},
|
|
701
|
-
})
|
|
702
|
-
.findMany(
|
|
703
|
-
caller: req.headers['x-caller'],
|
|
704
|
-
...req.body,
|
|
705
|
-
})
|
|
893
|
+
}, req.headers['x-caller'])
|
|
894
|
+
.findMany(req.body)
|
|
706
895
|
```
|
|
707
896
|
|
|
708
897
|
The frontend sends its current route as a header:
|
|
@@ -715,7 +904,7 @@ fetch('/api/projects', {
|
|
|
715
904
|
})
|
|
716
905
|
```
|
|
717
906
|
|
|
718
|
-
The backend
|
|
907
|
+
The backend passes `caller` as the second argument to `.guard()`. The request body contains only Prisma-compatible fields.
|
|
719
908
|
|
|
720
909
|
### Named mutation shapes
|
|
721
910
|
```ts
|
|
@@ -729,9 +918,8 @@ await prisma.project
|
|
|
729
918
|
data: { title: true },
|
|
730
919
|
where: { id: { equals: true } },
|
|
731
920
|
},
|
|
732
|
-
})
|
|
921
|
+
}, req.headers['x-caller'])
|
|
733
922
|
.update({
|
|
734
|
-
caller: req.headers['x-caller'],
|
|
735
923
|
data: req.body.data,
|
|
736
924
|
where: { id: { equals: req.params.id } },
|
|
737
925
|
})
|
|
@@ -752,15 +940,38 @@ await prisma.project
|
|
|
752
940
|
title: (base) => base.min(1).max(100),
|
|
753
941
|
},
|
|
754
942
|
},
|
|
755
|
-
})
|
|
756
|
-
.create({
|
|
757
|
-
caller: req.headers['x-caller'],
|
|
758
|
-
data: req.body,
|
|
759
|
-
})
|
|
943
|
+
}, req.headers['x-caller'])
|
|
944
|
+
.create({ data: req.body })
|
|
760
945
|
```
|
|
761
946
|
|
|
762
947
|
All data shape value types (`true`, literal, function) work in named shapes, context-dependent shapes, and single shapes.
|
|
763
948
|
|
|
949
|
+
### Caller resolution order
|
|
950
|
+
|
|
951
|
+
Caller is resolved in priority order:
|
|
952
|
+
|
|
953
|
+
1. **Explicit argument** — `.guard(shapes, '/admin/projects')` always wins
|
|
954
|
+
2. **Context function** — if the context object has a `caller` string property, it is used as the default
|
|
955
|
+
3. **None** — if neither source provides a caller and the shape is a named map, `CallerError` is thrown
|
|
956
|
+
|
|
957
|
+
This enables three usage patterns:
|
|
958
|
+
```ts
|
|
959
|
+
// 1. Per-request via context (set once, used everywhere)
|
|
960
|
+
guard.extension(() => ({
|
|
961
|
+
Tenant: store.getStore()?.tenantId,
|
|
962
|
+
caller: store.getStore()?.caller,
|
|
963
|
+
}))
|
|
964
|
+
await prisma.project.guard(shapes).findMany(req.body)
|
|
965
|
+
|
|
966
|
+
// 2. Explicit override per call
|
|
967
|
+
await prisma.project.guard(shapes, '/admin/projects').findMany(req.body)
|
|
968
|
+
|
|
969
|
+
// 3. Single shape (no caller needed)
|
|
970
|
+
await prisma.project.guard({ where: { ... } }).findMany(req.body)
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
The `caller` key in the context object is not used for scope injection — it is only used for shape routing. Scope roots are identified by matching context keys against `@scope-root` model names.
|
|
974
|
+
|
|
764
975
|
### Parameterized caller patterns
|
|
765
976
|
```text
|
|
766
977
|
/org/:orgId/users
|
|
@@ -773,21 +984,28 @@ Matching is case-sensitive. Exact matches are checked first. If no exact match i
|
|
|
773
984
|
|
|
774
985
|
If `caller` is missing or doesn't match any pattern, the request is rejected with a `CallerError`. If a caller matches multiple parameterized patterns, it is also rejected with a `CallerError`.
|
|
775
986
|
|
|
987
|
+
If a request body contains a `caller` field when using named shapes, it is rejected with a `CallerError` that directs the developer to use the second argument to `.guard()` or the context function instead.
|
|
988
|
+
|
|
776
989
|
---
|
|
777
990
|
|
|
778
991
|
## Context-dependent shapes
|
|
779
992
|
|
|
780
|
-
Shapes can be functions that receive the context provided to `guard.extension()`. This is the same context used for tenant scoping — no separate mechanism.
|
|
993
|
+
Shapes can be functions that receive the context provided to `guard.extension()`. This is the same context used for tenant scoping and caller routing — no separate mechanism.
|
|
781
994
|
```ts
|
|
782
995
|
const prisma = new PrismaClient().$extends(
|
|
783
996
|
guard.extension(() => ({
|
|
784
997
|
Tenant: store.getStore()?.tenantId,
|
|
785
998
|
role: store.getStore()?.role,
|
|
999
|
+
caller: store.getStore()?.caller,
|
|
786
1000
|
}))
|
|
787
1001
|
)
|
|
788
1002
|
```
|
|
789
1003
|
|
|
790
|
-
The context function returns an object with arbitrary keys. Keys whose values are `string`, `number`, or `bigint` are used as scope context for tenant isolation. Other keys (like `role` in the example above) are passed through to shape functions but are not used for scoping.
|
|
1004
|
+
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.
|
|
1005
|
+
|
|
1006
|
+
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
|
+
|
|
1008
|
+
Dynamic shape functions must return a plain guard shape object. If the function throws or returns a non-object value, a `ShapeError` is raised.
|
|
791
1009
|
|
|
792
1010
|
### Single context-dependent shape
|
|
793
1011
|
```ts
|
|
@@ -832,13 +1050,10 @@ await prisma.project
|
|
|
832
1050
|
take: { max: 20 },
|
|
833
1051
|
},
|
|
834
1052
|
})
|
|
835
|
-
.findMany(
|
|
836
|
-
caller: req.headers['x-caller'],
|
|
837
|
-
...req.body,
|
|
838
|
-
})
|
|
1053
|
+
.findMany(req.body)
|
|
839
1054
|
```
|
|
840
1055
|
|
|
841
|
-
Static shapes and function shapes can be mixed freely in the same shape map.
|
|
1056
|
+
Static shapes and function shapes can be mixed freely in the same shape map. In this example, the caller is resolved from `contextFn().caller` since no explicit caller is passed.
|
|
842
1057
|
|
|
843
1058
|
---
|
|
844
1059
|
|
|
@@ -870,8 +1085,8 @@ This applies to all top-level operations on scoped models, including reads, writ
|
|
|
870
1085
|
|
|
871
1086
|
### What is NOT scoped
|
|
872
1087
|
|
|
873
|
-
* Nested reads loaded via `include` or `select` — use forced where conditions in the shape to restrict these
|
|
874
|
-
* Nested writes — Prisma extension hooks operate on top-level operations only
|
|
1088
|
+
* 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
|
+
* 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.
|
|
875
1090
|
* `$queryRaw` and `$executeRaw` — raw SQL bypasses all guard protections
|
|
876
1091
|
* `upsert` on scoped models — rejected with `PolicyError`; handle explicitly in route logic
|
|
877
1092
|
|
|
@@ -896,6 +1111,20 @@ generator guard {
|
|
|
896
1111
|
|
|
897
1112
|
---
|
|
898
1113
|
|
|
1114
|
+
## Multi-root scope behavior
|
|
1115
|
+
|
|
1116
|
+
A model can be scoped by multiple scope roots simultaneously. If `Project` has a foreign key to both `Tenant` and `Organization` (both marked `@scope-root`), the scope extension enforces both.
|
|
1117
|
+
|
|
1118
|
+
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
|
+
|
|
1120
|
+
On writes, all scope roots must be present in the context. A missing root always throws `PolicyError`, regardless of `onMissingScopeContext`.
|
|
1121
|
+
|
|
1122
|
+
Scope foreign keys for all present roots are injected into create data and stripped from update/delete data.
|
|
1123
|
+
|
|
1124
|
+
If this behavior is not what you want, restructure your schema so the model references only one scope root, or handle scoping explicitly via shape rules.
|
|
1125
|
+
|
|
1126
|
+
---
|
|
1127
|
+
|
|
899
1128
|
## findUnique behavior
|
|
900
1129
|
|
|
901
1130
|
Prisma `findUnique` only accepts declared unique selectors.
|
|
@@ -942,6 +1171,8 @@ This is weaker because:
|
|
|
942
1171
|
|
|
943
1172
|
For tenant isolation, `"reject"` is the safer production default.
|
|
944
1173
|
|
|
1174
|
+
Guard shapes for `findUnique` and `findUniqueOrThrow` must define `where`. A shape without `where` for these methods throws `ShapeError`.
|
|
1175
|
+
|
|
945
1176
|
---
|
|
946
1177
|
|
|
947
1178
|
## Output shaping
|
|
@@ -996,33 +1227,33 @@ These limitations are real and should be treated as part of the security model.
|
|
|
996
1227
|
|
|
997
1228
|
Prisma extension hooks operate on top-level operations. Nested writes do not trigger separate scope interception.
|
|
998
1229
|
|
|
999
|
-
|
|
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.
|
|
1000
1231
|
|
|
1001
1232
|
### Nested reads via include are not scope-filtered
|
|
1002
1233
|
|
|
1003
1234
|
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.
|
|
1004
1235
|
|
|
1005
|
-
###
|
|
1236
|
+
### Forced where on nested reads is limited to to-many relations
|
|
1006
1237
|
|
|
1007
|
-
|
|
1238
|
+
Prisma does not support `where` on to-one relation includes. Because of this, forced `where` conditions in nested include/select shapes only work on to-many relations.
|
|
1008
1239
|
|
|
1009
|
-
|
|
1240
|
+
For to-one relations (e.g. `author` on a `Post`), the available mitigations are: omit the relation from the include/select shape entirely, restrict which scalar fields are returned using nested `select`, or rely on database-level constraints (e.g. RLS, foreign key guarantees).
|
|
1010
1241
|
|
|
1011
|
-
|
|
1242
|
+
This is a Prisma API constraint, not a prisma-guard limitation.
|
|
1012
1243
|
|
|
1013
|
-
|
|
1244
|
+
### `findUnique` cannot be safely scoped in Prisma extension mode
|
|
1014
1245
|
|
|
1015
|
-
|
|
1246
|
+
This is a Prisma API limitation, not a conceptual limitation of scoped unique lookups.
|
|
1016
1247
|
|
|
1017
|
-
|
|
1248
|
+
That is why `findUniqueMode = "reject"` is recommended.
|
|
1018
1249
|
|
|
1019
|
-
Guard
|
|
1250
|
+
Guard shapes for `findUnique` and `findUniqueOrThrow` must define `where`. A shape without `where` throws `ShapeError`.
|
|
1020
1251
|
|
|
1021
|
-
|
|
1252
|
+
### Composite foreign keys to scope roots
|
|
1022
1253
|
|
|
1023
|
-
|
|
1254
|
+
If a model references a scope root through composite foreign keys, that specific root is excluded from the 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.
|
|
1024
1255
|
|
|
1025
|
-
|
|
1256
|
+
Handle these models explicitly via shape rules.
|
|
1026
1257
|
|
|
1027
1258
|
### Cursor fields must cover a unique constraint
|
|
1028
1259
|
|
|
@@ -1046,7 +1277,7 @@ Guard `having` shapes support scalar field filters with their type-appropriate o
|
|
|
1046
1277
|
|
|
1047
1278
|
### Json fields accept any JSON-serializable value
|
|
1048
1279
|
|
|
1049
|
-
`Json` fields are recursively validated as JSON-serializable values (string, number, boolean, null, plain objects, arrays). Values that are not JSON-serializable — including `undefined`, functions, symbols, class instances (such as `Date`), `NaN`,
|
|
1280
|
+
`Json` fields are recursively validated as JSON-serializable values (string, number, boolean, null, plain objects, arrays). Values that are not JSON-serializable — including `undefined`, functions, symbols, class instances (such as `Date`), `NaN`, `Infinity`, and circular references — are rejected. This does not enforce any particular JSON structure. If you need structured JSON validation, use a context-dependent shape or validate before calling guard.
|
|
1050
1281
|
|
|
1051
1282
|
### `refine` and inline refine functions replace `@zod` chains
|
|
1052
1283
|
|
|
@@ -1060,14 +1291,38 @@ Both `guard.input()` and `guard.model()` reject configurations that specify both
|
|
|
1060
1291
|
|
|
1061
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.
|
|
1062
1293
|
|
|
1063
|
-
### `@zod .default()` does not affect create completeness checks
|
|
1064
|
-
|
|
1065
|
-
A `@zod .default(...)` directive adds a Zod-level default but does not set `hasDefault` in the type map. Create completeness validation still uses Prisma schema metadata (`@default`). A field with only `@zod .default(...)` and no Prisma `@default` will still be flagged as missing from create data shapes.
|
|
1066
|
-
|
|
1067
1294
|
### Inline refine functions are not cached
|
|
1068
1295
|
|
|
1069
1296
|
Data schemas containing inline refine functions are rebuilt on every request, since the function reference could be context-dependent (e.g. when used inside a dynamic shape that closes over context values). Static data shapes using only `true` and literal values are cached normally.
|
|
1070
1297
|
|
|
1298
|
+
### `take` does not support negative values
|
|
1299
|
+
|
|
1300
|
+
Prisma supports negative `take` for reverse cursor pagination. prisma-guard restricts `take` to positive integers (minimum 1). If you need reverse pagination, construct the query server-side using a context-dependent shape.
|
|
1301
|
+
|
|
1302
|
+
### `skip` in shape config is a permission flag
|
|
1303
|
+
|
|
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.
|
|
1305
|
+
|
|
1306
|
+
### `guard.input()` defaults to allowing null for nullable fields
|
|
1307
|
+
|
|
1308
|
+
`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
|
+
|
|
1310
|
+
### `Decimal` fields accept JavaScript numbers
|
|
1311
|
+
|
|
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.
|
|
1313
|
+
|
|
1314
|
+
### `skipDuplicates` is supported for batch create methods
|
|
1315
|
+
|
|
1316
|
+
`createMany` and `createManyAndReturn` accept `skipDuplicates: boolean` in the request body. This is passed through to Prisma without shape-level configuration. It is not available on `create`.
|
|
1317
|
+
|
|
1318
|
+
### Guarded data shapes do not permit relation fields
|
|
1319
|
+
|
|
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.
|
|
1321
|
+
|
|
1322
|
+
### Conflicting forced where values are rejected
|
|
1323
|
+
|
|
1324
|
+
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
|
+
|
|
1071
1326
|
---
|
|
1072
1327
|
|
|
1073
1328
|
## Advanced: SQL-backed runtimes
|
|
@@ -1094,9 +1349,9 @@ Libraries like **prisma-sql** make this possible for advanced architectures.
|
|
|
1094
1349
|
`.guard(shape).method(body)` may throw:
|
|
1095
1350
|
|
|
1096
1351
|
* `ZodError` — Zod validation failures on data or query args (unless `wrapZodErrors` is enabled)
|
|
1097
|
-
* `ShapeError` — invalid shape config, unknown shape config keys, wrong method for shape, body format issues, unexpected body keys, incomplete create data shapes,
|
|
1098
|
-
* `CallerError` — missing, unknown, or ambiguous caller in named shapes
|
|
1099
|
-
* `PolicyError` — denied scope, missing tenant context, or rejected operations on scoped models (e.g. upsert, findUnique in reject mode)
|
|
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, or conflicting forced where values
|
|
1353
|
+
* `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. upsert, findUnique in reject mode)
|
|
1100
1355
|
|
|
1101
1356
|
All guard errors include `status` and `code` properties for HTTP response mapping:
|
|
1102
1357
|
|
|
@@ -1137,7 +1392,8 @@ It reads the Prisma DMMF and emits:
|
|
|
1137
1392
|
* `TYPE_MAP` — field metadata per model
|
|
1138
1393
|
* `ENUM_MAP` — enum values
|
|
1139
1394
|
* `SCOPE_MAP` — foreign key → scope root mappings
|
|
1140
|
-
* `ZOD_CHAINS` — `@zod` directive chains (validated
|
|
1395
|
+
* `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
|
|
1141
1397
|
* `GUARD_CONFIG` — generator config values
|
|
1142
1398
|
* `UNIQUE_MAP` — unique constraint metadata per model
|
|
1143
1399
|
* `client.ts` — pre-wired guard instance with typed model extensions
|
|
@@ -1146,11 +1402,15 @@ It reads the Prisma DMMF and emits:
|
|
|
1146
1402
|
|
|
1147
1403
|
At runtime, `guard.extension()` creates a Prisma extension that provides:
|
|
1148
1404
|
|
|
1149
|
-
* `.guard(shape)` on every model delegate — validates input, enforces query shapes, returns typed Prisma methods
|
|
1405
|
+
* `.guard(shape, caller?)` on every model delegate — validates input, enforces query shapes, returns typed Prisma methods
|
|
1150
1406
|
* `$allOperations` query hook — injects tenant scope into every top-level database operation
|
|
1151
1407
|
|
|
1152
1408
|
The `.guard()` call validates against the shape, merges forced values, and delegates to the underlying Prisma method. The scope layer runs transparently underneath.
|
|
1153
1409
|
|
|
1410
|
+
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
|
+
|
|
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.
|
|
1413
|
+
|
|
1154
1414
|
---
|
|
1155
1415
|
|
|
1156
1416
|
## Why this approach
|
|
@@ -1174,9 +1434,11 @@ The runtime does lightweight argument rewriting and Zod validation.
|
|
|
1174
1434
|
|
|
1175
1435
|
In most real applications, overhead should be negligible relative to database round-trip time.
|
|
1176
1436
|
|
|
1177
|
-
Static shapes
|
|
1437
|
+
**`guard.query()` caching:** Static shapes passed to `guard.query()` are cached for the lifetime of the returned `QuerySchema` object. In a named shape map, each static entry is cached independently — a map with 9 static entries and 1 context-dependent entry will cache the 9 static entries. Context-dependent shapes (functions) resolve the function on each call because they depend on runtime context and are never cached.
|
|
1438
|
+
|
|
1439
|
+
**`.guard()` caching:** The `.guard(shape).method(body)` chain creates per-invocation caches. In typical usage, the cache is created, used for one method call, and discarded. If you store the result of `.guard(shape)` and call multiple methods on it, the cache is shared across those calls. For hot paths where the same static shape is used repeatedly, prefer `guard.query()` for persistent caching.
|
|
1178
1440
|
|
|
1179
|
-
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. Projection schemas (select/include on mutations) are cached independently for static shapes.
|
|
1441
|
+
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.
|
|
1180
1442
|
|
|
1181
1443
|
---
|
|
1182
1444
|
|
|
@@ -1192,6 +1454,7 @@ Data schemas containing inline refine functions are also not cached, since the f
|
|
|
1192
1454
|
| unsafe scoped `findUnique` | reject recommended |
|
|
1193
1455
|
| invalid `@zod` directive | error by default |
|
|
1194
1456
|
| missing `caller` in named shapes | error always |
|
|
1457
|
+
| `caller` in request body | error always |
|
|
1195
1458
|
| `data` in read shape | error always |
|
|
1196
1459
|
| missing `data` in write shape | error always |
|
|
1197
1460
|
| bulk mutation without `where` shape | error always |
|
|
@@ -1202,12 +1465,21 @@ Data schemas containing inline refine functions are also not cached, since the f
|
|
|
1202
1465
|
| cursor not covering unique constraint | error always |
|
|
1203
1466
|
| caller key collides with shape config | error always |
|
|
1204
1467
|
| empty operator objects in where | error always |
|
|
1468
|
+
| empty relation filter (no forced) | error always |
|
|
1469
|
+
| empty relation operator container | error always |
|
|
1205
1470
|
| `pick` and `omit` both specified | error always |
|
|
1206
1471
|
| scope relation in mutation data | controlled by `onScopeRelationWrite` (default: error) |
|
|
1207
1472
|
| incomplete create data shape | error always |
|
|
1208
1473
|
| invalid inline refine function | error always (ShapeError) |
|
|
1209
1474
|
| projection on batch method | error always |
|
|
1210
1475
|
| body projection without shape | error always |
|
|
1476
|
+
| dynamic shape returns non-object | error always (ShapeError) |
|
|
1477
|
+
| refine callback returns non-Zod schema | error always (ShapeError) |
|
|
1478
|
+
| `findUnique` shape without `where` | error always (ShapeError) |
|
|
1479
|
+
| invalid relation operator for type | error always (ShapeError) |
|
|
1480
|
+
| context function returns non-object | error always (PolicyError) |
|
|
1481
|
+
| conflicting forced where values | error always (ShapeError) |
|
|
1482
|
+
| invalid context function return | error always (PolicyError) |
|
|
1211
1483
|
|
|
1212
1484
|
---
|
|
1213
1485
|
|
|
@@ -1267,6 +1539,7 @@ Node 22
|
|
|
1267
1539
|
4. Avoid automatic relation traversal
|
|
1268
1540
|
5. Keep scope rules explicit and schema-driven
|
|
1269
1541
|
6. One chain — shape defines the boundary, method executes
|
|
1542
|
+
7. Method bodies stay Prisma-compatible — routing and context live in `.guard()`
|
|
1270
1543
|
|
|
1271
1544
|
---
|
|
1272
1545
|
|
|
@@ -1288,6 +1561,10 @@ Node 22
|
|
|
1288
1561
|
| Mutation return projection | yes | manual |
|
|
1289
1562
|
| Inline field refine in data shapes | yes | n/a |
|
|
1290
1563
|
| ZodError wrapping | opt-in | n/a |
|
|
1564
|
+
| Logical combinators in where | yes | manual |
|
|
1565
|
+
| Relation filters in where | yes | manual |
|
|
1566
|
+
| Empty relation filter rejection | yes | n/a |
|
|
1567
|
+
| Forced where conflict detection | yes | n/a |
|
|
1291
1568
|
|
|
1292
1569
|
---
|
|
1293
1570
|
|
|
@@ -1297,12 +1574,11 @@ Possible future improvements:
|
|
|
1297
1574
|
|
|
1298
1575
|
* optional nested-write enforcement helpers
|
|
1299
1576
|
* richer relation-level policies
|
|
1300
|
-
*
|
|
1301
|
-
* relation-level where filters
|
|
1302
|
-
* more query method coverage
|
|
1577
|
+
* more query method coverage (upsert)
|
|
1303
1578
|
* adapter integrations for SQL-backed runtimes
|
|
1304
1579
|
* model-specific generated types for stronger compile-time shape validation
|
|
1305
1580
|
* structured JSON field validation via schema annotations
|
|
1581
|
+
* optional strict Decimal mode (string-only, no JavaScript number)
|
|
1306
1582
|
|
|
1307
1583
|
---
|
|
1308
1584
|
|