prisma-guard 1.20.0 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -20
- package/dist/generator/index.js +9 -6
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.cjs +462 -110
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +4 -4
- package/dist/runtime/index.d.ts +4 -4
- package/dist/runtime/index.js +462 -110
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ database
|
|
|
39
39
|
* [Before / After prisma-guard](#before--after-prisma-guard)
|
|
40
40
|
* [Schema annotations](#schema-annotations)
|
|
41
41
|
* [The guard API](#the-guard-api)
|
|
42
|
+
* [Relation writes in data shapes](#relation-writes-in-data-shapes)
|
|
42
43
|
* [Logical combinators in where shapes](#logical-combinators-in-where-shapes)
|
|
43
44
|
* [Relation filters in where shapes](#relation-filters-in-where-shapes)
|
|
44
45
|
* [Mutation return projection](#mutation-return-projection)
|
|
@@ -401,7 +402,7 @@ await prisma.project
|
|
|
401
402
|
|
|
402
403
|
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.
|
|
403
404
|
|
|
404
|
-
Relation fields are
|
|
405
|
+
Relation fields are supported in `data` shapes with a config object describing allowed nested write operations. See [Relation writes in data shapes](#relation-writes-in-data-shapes) for syntax and security implications.
|
|
405
406
|
|
|
406
407
|
### The `force()` helper
|
|
407
408
|
|
|
@@ -443,6 +444,15 @@ await prisma.project
|
|
|
443
444
|
|
|
444
445
|
The client can only filter by `title`, sort by `title`, and take up to 100 rows. Everything else is rejected.
|
|
445
446
|
|
|
447
|
+
### Take shorthand
|
|
448
|
+
|
|
449
|
+
`take` accepts either an object or a number. When a number is provided, it serves as both the maximum and the default:
|
|
450
|
+
```ts
|
|
451
|
+
take: 50 // equivalent to { max: 50, default: 50 }
|
|
452
|
+
take: { max: 100, default: 25 } // explicit max and default
|
|
453
|
+
take: { max: 100 } // max only, no default
|
|
454
|
+
```
|
|
455
|
+
|
|
446
456
|
### Creates
|
|
447
457
|
```ts
|
|
448
458
|
await prisma.project
|
|
@@ -602,11 +612,43 @@ Where shapes accept scalar field filters, relation filters (`some`, `every`, `no
|
|
|
602
612
|
|
|
603
613
|
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
614
|
|
|
605
|
-
### Where DSL
|
|
615
|
+
### Where DSL: Prisma-compatible subset
|
|
616
|
+
|
|
617
|
+
The where shape syntax supports a subset of Prisma's where filter API. This section documents both supported features and known differences.
|
|
618
|
+
|
|
619
|
+
**Supported operators:**
|
|
620
|
+
|
|
621
|
+
All standard scalar operators are supported: `equals`, `not`, `contains`, `startsWith`, `endsWith`, `in`, `notIn`, `gt`, `gte`, `lt`, `lte`, `search`.
|
|
622
|
+
|
|
623
|
+
The `not` operator accepts either a scalar value or a nested filter object:
|
|
624
|
+
```ts
|
|
625
|
+
where: {
|
|
626
|
+
age: { not: true }, // client can send { age: { not: 5 } } or { age: { not: { gt: 5 } } }
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The `search` operator is supported for String fields with `@@fulltext` indexes:
|
|
631
|
+
```ts
|
|
632
|
+
where: {
|
|
633
|
+
title: { search: true },
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Relation existence checks with `is: null` / `isNot: null`:**
|
|
638
|
+
|
|
639
|
+
To-one relation filters support null checks for testing relation existence. In the shape config, use `null` as the operator value to force a null check:
|
|
640
|
+
```ts
|
|
641
|
+
where: {
|
|
642
|
+
author: {
|
|
643
|
+
is: null, // forced: always filters for records where author IS null
|
|
644
|
+
},
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
This produces `{ author: { is: null } }` in the Prisma query. Since `null` in a shape is always a forced value (the client cannot control it), this is equivalent to a forced where condition.
|
|
606
649
|
|
|
607
|
-
|
|
650
|
+
**Notable differences from raw Prisma where clauses:**
|
|
608
651
|
|
|
609
|
-
* The `not` operator accepts a scalar value only, not a nested filter object. Prisma's `{ not: { gt: 5 } }` form is not supported.
|
|
610
652
|
* `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
653
|
* `NOT` in client input accepts a single object or an array with at least one element. Empty arrays are rejected.
|
|
612
654
|
* 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.
|
|
@@ -629,6 +671,109 @@ Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `
|
|
|
629
671
|
|
|
630
672
|
---
|
|
631
673
|
|
|
674
|
+
## Relation writes in data shapes
|
|
675
|
+
|
|
676
|
+
Data shapes support relation fields with a config object describing which nested write operations the client may use. Each operation (`connect`, `create`, `disconnect`, etc.) is configured individually.
|
|
677
|
+
|
|
678
|
+
> **⚠️ Security warning:** The automatic tenant scope extension only intercepts **top-level** operations. Nested writes through relation configs bypass scope entirely — no FK injection on nested creates, no tenant filtering on nested updates/deletes. If you use relation writes on scoped models, you must handle tenant isolation manually in your application code or enforce it via database constraints (e.g. RLS, triggers).
|
|
679
|
+
|
|
680
|
+
### Syntax
|
|
681
|
+
```ts
|
|
682
|
+
await prisma.post
|
|
683
|
+
.guard({
|
|
684
|
+
data: {
|
|
685
|
+
title: true,
|
|
686
|
+
content: true,
|
|
687
|
+
tags: {
|
|
688
|
+
connect: { id: true },
|
|
689
|
+
disconnect: { id: true },
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
})
|
|
693
|
+
.update({
|
|
694
|
+
data: {
|
|
695
|
+
title: 'Updated',
|
|
696
|
+
tags: {
|
|
697
|
+
connect: [{ id: 'tag1' }, { id: 'tag2' }],
|
|
698
|
+
disconnect: [{ id: 'tag3' }],
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
where: { id: { equals: 'post1' } },
|
|
702
|
+
})
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Supported operations
|
|
706
|
+
|
|
707
|
+
All 11 Prisma nested write operations are supported:
|
|
708
|
+
|
|
709
|
+
| Operation | To-one | To-many | Config type |
|
|
710
|
+
| ----------------- | ------ | ------- | ------------------------------------------------ |
|
|
711
|
+
| `connect` | yes | yes | `{ fieldName: true, ... }` |
|
|
712
|
+
| `connectOrCreate` | yes | yes | `{ where: { ... }, create: { ... } }` |
|
|
713
|
+
| `create` | yes | yes | `{ fieldName: true, ... }` |
|
|
714
|
+
| `createMany` | no | yes | `{ data: { fieldName: true, ... } }` |
|
|
715
|
+
| `disconnect` | yes | yes | `true` (to-one) or `{ fieldName: true }` (to-many) |
|
|
716
|
+
| `delete` | yes | yes | `true` (to-one) or `{ fieldName: true }` (to-many) |
|
|
717
|
+
| `set` | no | yes | `{ fieldName: true, ... }` |
|
|
718
|
+
| `update` | yes | yes | `{ fieldName: true }` or `{ where: ..., data: ... }` |
|
|
719
|
+
| `updateMany` | no | yes | `{ where: { ... }, data: { ... } }` |
|
|
720
|
+
| `upsert` | yes | yes | `{ create: { ... }, update: { ... } }` |
|
|
721
|
+
| `deleteMany` | no | yes | `{ fieldName: true, ... }` |
|
|
722
|
+
|
|
723
|
+
### Example with multiple operations
|
|
724
|
+
```ts
|
|
725
|
+
await prisma.user
|
|
726
|
+
.guard({
|
|
727
|
+
data: {
|
|
728
|
+
name: true,
|
|
729
|
+
posts: {
|
|
730
|
+
create: { title: true, content: true },
|
|
731
|
+
connect: { id: true },
|
|
732
|
+
disconnect: { id: true },
|
|
733
|
+
update: {
|
|
734
|
+
where: { id: true },
|
|
735
|
+
data: { title: true },
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
})
|
|
740
|
+
.update({
|
|
741
|
+
data: {
|
|
742
|
+
name: 'Updated Name',
|
|
743
|
+
posts: {
|
|
744
|
+
create: { title: 'New Post', content: 'Content' },
|
|
745
|
+
connect: [{ id: 'existing-post-id' }],
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
where: { id: { equals: userId } },
|
|
749
|
+
})
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Validation
|
|
753
|
+
|
|
754
|
+
Each operation's config is validated at shape construction time:
|
|
755
|
+
|
|
756
|
+
* Unknown operations throw `ShapeError`
|
|
757
|
+
* Operations invalid for the relation cardinality throw `ShapeError` (e.g. `set` on to-one, `disconnect: true` on to-many)
|
|
758
|
+
* Nested data fields are validated against the related model's type map — relation fields within nested data are not supported (no deep nesting)
|
|
759
|
+
* `@zod` chains apply to nested data fields
|
|
760
|
+
|
|
761
|
+
### Scope implications
|
|
762
|
+
|
|
763
|
+
Nested writes bypass the scope extension because Prisma extension hooks only fire for top-level operations. This means:
|
|
764
|
+
|
|
765
|
+
* **Nested creates** do not get scope FK injection — the related record will not have the tenant FK set automatically
|
|
766
|
+
* **Nested updates/deletes** do not get tenant where conditions — they can affect records across tenants
|
|
767
|
+
* **Nested connects** reference records by unique fields without tenant filtering
|
|
768
|
+
|
|
769
|
+
For multi-tenant applications, consider:
|
|
770
|
+
|
|
771
|
+
* Using database-level constraints (foreign keys, RLS policies) to enforce tenant boundaries on related models
|
|
772
|
+
* Restricting relation write operations to `connect` and `disconnect` only (which reference existing records by ID)
|
|
773
|
+
* Using forced values for tenant FK fields in nested create configs where possible
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
632
777
|
## Logical combinators in where shapes
|
|
633
778
|
|
|
634
779
|
Where shapes support `AND`, `OR`, and `NOT` to compose filter conditions. The combinator value is a where config defining allowed fields inside the combinator:
|
|
@@ -720,6 +865,23 @@ await prisma.user
|
|
|
720
865
|
|
|
721
866
|
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.
|
|
722
867
|
|
|
868
|
+
### Null existence checks for to-one relations
|
|
869
|
+
|
|
870
|
+
To-one relation operators support `null` for testing whether a relation exists:
|
|
871
|
+
```ts
|
|
872
|
+
await prisma.post
|
|
873
|
+
.guard({
|
|
874
|
+
where: {
|
|
875
|
+
author: {
|
|
876
|
+
is: null, // forced: filter for posts where author IS null
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
})
|
|
880
|
+
.findMany(req.body)
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
In the shape config, `null` as an operator value is always forced — the client cannot control it. This is the standard Prisma pattern for checking to-one relation existence.
|
|
884
|
+
|
|
723
885
|
### Forced values in relation filters
|
|
724
886
|
```ts
|
|
725
887
|
import { force } from 'prisma-guard'
|
|
@@ -1211,13 +1373,40 @@ await prisma.project
|
|
|
1211
1373
|
|
|
1212
1374
|
All data shape value types (`true`, literal, `force()`, function) work in named shapes, context-dependent shapes, and single shapes.
|
|
1213
1375
|
|
|
1376
|
+
### Default fallback
|
|
1377
|
+
|
|
1378
|
+
Named shape maps support a `default` key that acts as a fallback when no caller is provided or no pattern matches:
|
|
1379
|
+
```ts
|
|
1380
|
+
await prisma.project
|
|
1381
|
+
.guard({
|
|
1382
|
+
'/admin/projects': {
|
|
1383
|
+
where: { title: { contains: true }, status: { equals: true } },
|
|
1384
|
+
take: { max: 100 },
|
|
1385
|
+
},
|
|
1386
|
+
default: {
|
|
1387
|
+
where: { title: { contains: true } },
|
|
1388
|
+
take: { max: 20 },
|
|
1389
|
+
},
|
|
1390
|
+
}, req.headers['x-caller'])
|
|
1391
|
+
.findMany(req.body)
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
The `default` fallback is used when:
|
|
1395
|
+
|
|
1396
|
+
* No caller is provided (missing from both the second argument and context function)
|
|
1397
|
+
* The provided caller doesn't match any pattern
|
|
1398
|
+
|
|
1399
|
+
Without a `default` key, missing or unmatched callers throw `CallerError`.
|
|
1400
|
+
|
|
1401
|
+
The `default` fallback works consistently across both `.guard()` and `guard.query().parse()` API surfaces.
|
|
1402
|
+
|
|
1214
1403
|
### Caller resolution order
|
|
1215
1404
|
|
|
1216
1405
|
Caller is resolved in priority order:
|
|
1217
1406
|
|
|
1218
1407
|
1. **Explicit argument** — `.guard(shapes, '/admin/projects')` always wins
|
|
1219
1408
|
2. **Context function** — if the context object has a `caller` string property, it is used as the default
|
|
1220
|
-
3. **None** — if neither source provides a caller and the shape is a named map, `CallerError` is thrown
|
|
1409
|
+
3. **None** — if neither source provides a caller and the shape is a named map without a `default` key, `CallerError` is thrown
|
|
1221
1410
|
|
|
1222
1411
|
This enables three usage patterns:
|
|
1223
1412
|
```ts
|
|
@@ -1247,7 +1436,7 @@ Matching is case-sensitive. Exact matches are checked first. If no exact match i
|
|
|
1247
1436
|
|
|
1248
1437
|
### Fail-closed behavior
|
|
1249
1438
|
|
|
1250
|
-
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`.
|
|
1439
|
+
If `caller` is missing or doesn't match any pattern and no `default` key exists, the request is rejected with a `CallerError`. If a caller matches multiple parameterized patterns, it is also rejected with a `CallerError`.
|
|
1251
1440
|
|
|
1252
1441
|
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.
|
|
1253
1442
|
|
|
@@ -1357,7 +1546,7 @@ This applies to all top-level operations on scoped models, including reads, writ
|
|
|
1357
1546
|
### What is NOT scoped
|
|
1358
1547
|
|
|
1359
1548
|
* Nested reads loaded via `include` or `select` — use forced where conditions in the shape to restrict these (to-many relations only; see [Limitations](#limitations))
|
|
1360
|
-
* Nested writes
|
|
1549
|
+
* Nested writes via relation write configs in data shapes — the scope extension hooks only fire for top-level operations (see [Relation writes in data shapes](#relation-writes-in-data-shapes))
|
|
1361
1550
|
* `$queryRaw` and `$executeRaw` — raw SQL bypasses all guard protections
|
|
1362
1551
|
|
|
1363
1552
|
### Scope relation writes
|
|
@@ -1531,11 +1720,11 @@ These limitations are real and should be treated as part of the security model.
|
|
|
1531
1720
|
|
|
1532
1721
|
`$queryRaw` and `$executeRaw` are not intercepted.
|
|
1533
1722
|
|
|
1534
|
-
### Nested writes are not intercepted
|
|
1723
|
+
### Nested writes are not scope-intercepted
|
|
1535
1724
|
|
|
1536
|
-
Prisma extension hooks operate on top-level operations.
|
|
1725
|
+
Prisma extension hooks operate on top-level operations. Relation write configs in data shapes (see [Relation writes in data shapes](#relation-writes-in-data-shapes)) produce nested write operations that bypass the scope extension entirely. Nested creates do not receive scope FK injection. Nested updates and deletes do not receive tenant where conditions.
|
|
1537
1726
|
|
|
1538
|
-
|
|
1727
|
+
For multi-tenant applications using relation writes, enforce tenant boundaries via database constraints (RLS, foreign key constraints, triggers) or application-level validation.
|
|
1539
1728
|
|
|
1540
1729
|
### Nested reads via include are not scope-filtered
|
|
1541
1730
|
|
|
@@ -1563,6 +1752,12 @@ If a model references a scope root through composite foreign keys, that specific
|
|
|
1563
1752
|
|
|
1564
1753
|
Handle these models explicitly via shape rules.
|
|
1565
1754
|
|
|
1755
|
+
### Compound unique selectors
|
|
1756
|
+
|
|
1757
|
+
Guard currently records unique constraints as arrays of field names but does not generate the named compound selector schemas that Prisma uses for `@@unique` constraints. For example, `@@unique([firstName, lastName])` requires the selector `{ firstName_lastName: { firstName: "A", lastName: "B" } }`, but guard produces flat `{ firstName: "A", lastName: "B" }` output. This affects `findUnique`, `update`, `delete`, `upsert`, `connect`, and `connectOrCreate` with compound unique constraints.
|
|
1758
|
+
|
|
1759
|
+
Single-field unique constraints work correctly. Compound unique support is planned.
|
|
1760
|
+
|
|
1566
1761
|
### Cursor fields must cover a unique constraint
|
|
1567
1762
|
|
|
1568
1763
|
Prisma requires cursor-based pagination to use uniquely-identifiable fields. Guard enforces this at shape construction time: cursor fields must cover at least one unique constraint from the model. Non-unique cursor shapes are rejected with `ShapeError`.
|
|
@@ -1579,9 +1774,9 @@ Prisma requires cursor-based pagination to use uniquely-identifiable fields. Gua
|
|
|
1579
1774
|
|
|
1580
1775
|
The generator writes `index.ts` and `client.ts` using `.js` extension imports. A TypeScript-capable build pipeline with ESM-aware module resolution is required (`"moduleResolution": "NodeNext"` or `"Bundler"` in `tsconfig.json`). The classic `"moduleResolution": "node"` setting is not compatible.
|
|
1581
1776
|
|
|
1582
|
-
### `having`
|
|
1777
|
+
### `having` supports logical combinators
|
|
1583
1778
|
|
|
1584
|
-
Guard `having` shapes support
|
|
1779
|
+
Guard `having` shapes support `AND`, `OR`, and `NOT` combinators for composing complex grouped aggregation filters. The fields available inside combinators are the same fields defined in the having shape config.
|
|
1585
1780
|
|
|
1586
1781
|
### Json fields accept any JSON-serializable value
|
|
1587
1782
|
|
|
@@ -1611,7 +1806,7 @@ Prisma supports negative `take` for reverse cursor pagination. prisma-guard rest
|
|
|
1611
1806
|
|
|
1612
1807
|
### `skip` in shape config is a permission flag
|
|
1613
1808
|
|
|
1614
|
-
`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.
|
|
1809
|
+
`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 or a number shorthand.
|
|
1615
1810
|
|
|
1616
1811
|
### `guard.input()` defaults to allowing null for nullable fields
|
|
1617
1812
|
|
|
@@ -1625,10 +1820,6 @@ The `Decimal` base type accepts JavaScript `number`, decimal string, and Decimal
|
|
|
1625
1820
|
|
|
1626
1821
|
`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`.
|
|
1627
1822
|
|
|
1628
|
-
### Guarded data shapes do not permit relation fields
|
|
1629
|
-
|
|
1630
|
-
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.
|
|
1631
|
-
|
|
1632
1823
|
### Conflicting forced where values are rejected
|
|
1633
1824
|
|
|
1634
1825
|
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.
|
|
@@ -1691,6 +1882,8 @@ All guard errors include `status` and `code` properties for HTTP response mappin
|
|
|
1691
1882
|
| `CallerError` | 400 | `CALLER_UNKNOWN` |
|
|
1692
1883
|
| `PolicyError` | 403 | `POLICY_DENIED` |
|
|
1693
1884
|
|
|
1885
|
+
Note: Prisma errors (`PrismaClientKnownRequestError`, `PrismaClientValidationError`, etc.) propagate through the guard layer unmodified. Error handlers should be prepared to handle both guard errors (with `status`/`code` properties) and Prisma errors.
|
|
1886
|
+
|
|
1694
1887
|
### ZodError wrapping
|
|
1695
1888
|
|
|
1696
1889
|
By default, Zod validation failures throw a raw `ZodError`. This means error handling code must check for both `ZodError` and guard error types.
|
|
@@ -1743,7 +1936,7 @@ For create operations, fields tracked in `ZOD_DEFAULTS` that are omitted from th
|
|
|
1743
1936
|
|
|
1744
1937
|
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.
|
|
1745
1938
|
|
|
1746
|
-
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).
|
|
1939
|
+
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 without a `default` key).
|
|
1747
1940
|
|
|
1748
1941
|
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.
|
|
1749
1942
|
|
|
@@ -1796,7 +1989,7 @@ For upsert, `create` and `update` data schemas are cached independently under na
|
|
|
1796
1989
|
| `onMissingScopeContext = "ignore"` | scope bypassed for missing roots; present roots still enforced |
|
|
1797
1990
|
| unsafe scoped `findUnique` | reject recommended |
|
|
1798
1991
|
| invalid `@zod` directive | error by default |
|
|
1799
|
-
| missing `caller` in named shapes | error
|
|
1992
|
+
| missing `caller` in named shapes | error unless `default` key exists |
|
|
1800
1993
|
| `caller` in request body | error always |
|
|
1801
1994
|
| `data` in read shape | error always |
|
|
1802
1995
|
| `data` in upsert shape | error always (use `create`/`update`) |
|
|
@@ -1832,6 +2025,7 @@ For upsert, `create` and `update` data schemas are cached independently under na
|
|
|
1832
2025
|
| `@zod .default()`/`.catch()` field omitted from shape | auto-injected as forced value |
|
|
1833
2026
|
| read shape with select/include, client omits | auto-applied as default projection |
|
|
1834
2027
|
| mutation shape with select/include, client omits | full payload unless enforceProjection enabled |
|
|
2028
|
+
| nested writes via relation configs | bypass scope extension (top-level only) |
|
|
1835
2029
|
|
|
1836
2030
|
---
|
|
1837
2031
|
|
|
@@ -1898,6 +2092,7 @@ Node 22
|
|
|
1898
2092
|
9. Upsert uses `create`/`update` keys, not `data` — matches Prisma's own API shape
|
|
1899
2093
|
10. Shape config values are validated strictly — `true` means enabled, anything else is rejected
|
|
1900
2094
|
11. Read shapes with projection define both the security boundary and the default response — no client duplication needed
|
|
2095
|
+
12. Nested writes are validated but not scope-intercepted — document clearly and rely on database constraints for tenant boundaries
|
|
1901
2096
|
|
|
1902
2097
|
---
|
|
1903
2098
|
|
|
@@ -1925,12 +2120,14 @@ Node 22
|
|
|
1925
2120
|
| ZodError wrapping | opt-in | n/a |
|
|
1926
2121
|
| Logical combinators in where | yes | manual |
|
|
1927
2122
|
| Relation filters in where | yes | manual |
|
|
2123
|
+
| Relation writes in data shapes | yes | manual |
|
|
1928
2124
|
| Empty relation filter rejection | yes | n/a |
|
|
1929
2125
|
| Empty projection shape rejection | yes | n/a |
|
|
1930
2126
|
| Forced where conflict detection | yes | n/a |
|
|
1931
2127
|
| Forced boolean values via `force()` | yes | n/a |
|
|
1932
2128
|
| Strict Decimal mode | opt-in | n/a |
|
|
1933
2129
|
| `@zod .default()`/`.catch()` auto-injection | yes | n/a |
|
|
2130
|
+
| Nested write scope enforcement | no (top-level only) | no |
|
|
1934
2131
|
|
|
1935
2132
|
---
|
|
1936
2133
|
|
|
@@ -1938,8 +2135,9 @@ Node 22
|
|
|
1938
2135
|
|
|
1939
2136
|
Possible future improvements:
|
|
1940
2137
|
|
|
1941
|
-
*
|
|
2138
|
+
* compound unique selector support
|
|
1942
2139
|
* richer relation-level policies
|
|
2140
|
+
* nested write scope enforcement helpers
|
|
1943
2141
|
* adapter integrations for SQL-backed runtimes
|
|
1944
2142
|
* model-specific generated types for stronger compile-time shape validation
|
|
1945
2143
|
* structured JSON field validation via schema annotations
|
package/dist/generator/index.js
CHANGED
|
@@ -268,11 +268,8 @@ function validateDirective(raw) {
|
|
|
268
268
|
}
|
|
269
269
|
if (peek() === "e" || peek() === "E") {
|
|
270
270
|
advance();
|
|
271
|
-
if (peek() === "-")
|
|
271
|
+
if (peek() === "-" || peek() === "+")
|
|
272
272
|
advance();
|
|
273
|
-
if (peek() === "+") {
|
|
274
|
-
return { valid: false, reason: 'Invalid number: "+" not allowed in exponent' };
|
|
275
|
-
}
|
|
276
273
|
if (!/[0-9]/.test(peek())) {
|
|
277
274
|
return { valid: false, reason: "Invalid number: expected digit in exponent" };
|
|
278
275
|
}
|
|
@@ -602,7 +599,13 @@ function createScalarBase(strictDecimal) {
|
|
|
602
599
|
z.string().regex(/^-?\d+$/).transform((v) => BigInt(v))
|
|
603
600
|
]),
|
|
604
601
|
Boolean: () => z.boolean(),
|
|
605
|
-
DateTime: () => z.union([
|
|
602
|
+
DateTime: () => z.union([
|
|
603
|
+
z.date(),
|
|
604
|
+
z.string().refine(
|
|
605
|
+
(s) => !isNaN(Date.parse(s)),
|
|
606
|
+
"Invalid date string"
|
|
607
|
+
)
|
|
608
|
+
]).pipe(z.coerce.date()),
|
|
606
609
|
Json: () => z.unknown().refine(
|
|
607
610
|
isJsonSafe,
|
|
608
611
|
"Value must be JSON-serializable (no undefined, functions, symbols, class instances, NaN, Infinity, or circular references)"
|
|
@@ -926,7 +929,7 @@ function emitClient(dmmf) {
|
|
|
926
929
|
import type { GuardInput, GuardedModel } from 'prisma-guard'
|
|
927
930
|
import { createGuard } from 'prisma-guard'
|
|
928
931
|
import { SCOPE_MAP, TYPE_MAP, ENUM_MAP, ZOD_CHAINS, GUARD_CONFIG, UNIQUE_MAP, ZOD_DEFAULTS } from './index'
|
|
929
|
-
import type { ScopeRoot } from './index
|
|
932
|
+
import type { ScopeRoot } from './index'
|
|
930
933
|
|
|
931
934
|
interface GuardModelExtension {
|
|
932
935
|
${modelEntries}
|