prisma-guard 1.30.0 → 1.32.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 +190 -15
- package/dist/generator/index.js +6 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.cjs +157 -103
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +2 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +157 -103
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -644,10 +644,13 @@ For `groupBy`, normal grouped fields and `_count` fields must also be configured
|
|
|
644
644
|
|
|
645
645
|
### Unique where shapes
|
|
646
646
|
|
|
647
|
-
`findUnique`, `findUniqueOrThrow`, `update`, `delete`, and `upsert` use Prisma's `WhereUniqueInput` syntax.
|
|
647
|
+
`findUnique`, `findUniqueOrThrow`, `update`, `delete`, and `upsert` use Prisma's `WhereUniqueInput` syntax.
|
|
648
|
+
|
|
649
|
+
For these methods, configure selector fields directly in the shape:
|
|
648
650
|
```ts
|
|
649
651
|
await prisma.project
|
|
650
652
|
.guard({
|
|
653
|
+
data: { title: true },
|
|
651
654
|
where: { id: true },
|
|
652
655
|
})
|
|
653
656
|
.update({
|
|
@@ -656,9 +659,108 @@ await prisma.project
|
|
|
656
659
|
})
|
|
657
660
|
```
|
|
658
661
|
|
|
659
|
-
Do not use filter operator objects such as `{ id: { equals: true } }` in unique where shapes. That syntax belongs to normal
|
|
662
|
+
Do not use normal `WhereInput` filter operator objects such as `{ id: { equals: true } }` in unique where shapes. That syntax belongs to normal filter shapes used by methods such as `findMany`, `findFirst`, `count`, `updateMany`, and `deleteMany`.
|
|
663
|
+
|
|
664
|
+
#### Extra non-unique filters in `update` and `delete`
|
|
665
|
+
|
|
666
|
+
Prisma `WhereUniqueInput` can include additional non-unique scalar filters alongside a unique selector. This is useful for permission checks.
|
|
667
|
+
|
|
668
|
+
For example, this is valid Prisma-style update filtering:
|
|
669
|
+
```ts
|
|
670
|
+
await prisma.post.update({
|
|
671
|
+
where: {
|
|
672
|
+
id: postId,
|
|
673
|
+
authorId: userId,
|
|
674
|
+
},
|
|
675
|
+
data: {
|
|
676
|
+
title: 'Updated',
|
|
677
|
+
},
|
|
678
|
+
})
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
In prisma-guard shape syntax, model this with direct fields, not filter operators:
|
|
682
|
+
```ts
|
|
683
|
+
await prisma.post
|
|
684
|
+
.guard({
|
|
685
|
+
data: { title: true },
|
|
686
|
+
where: {
|
|
687
|
+
id: true,
|
|
688
|
+
authorId: force(userId),
|
|
689
|
+
},
|
|
690
|
+
})
|
|
691
|
+
.update({
|
|
692
|
+
data: { title: 'Updated' },
|
|
693
|
+
where: { id: postId },
|
|
694
|
+
})
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
The shape above means:
|
|
698
|
+
|
|
699
|
+
* `id: true` is client-controlled and must be provided by the caller.
|
|
700
|
+
* `authorId: force(userId)` is server-controlled and is merged into the final Prisma `where`.
|
|
701
|
+
* The final Prisma call is constrained by both `id` and `authorId`.
|
|
702
|
+
|
|
703
|
+
Do not write the same shape as:
|
|
704
|
+
```ts
|
|
705
|
+
where: {
|
|
706
|
+
id: true,
|
|
707
|
+
authorId: { equals: force(userId) },
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
`authorId: { equals: ... }` is normal `WhereInput` filter syntax, not direct `WhereUniqueInput` syntax.
|
|
712
|
+
|
|
713
|
+
At least one unique selector must still be present. Extra non-unique filters are additional constraints; they do not replace the unique selector requirement.
|
|
714
|
+
|
|
715
|
+
#### Top-level unique where vs relation write selectors
|
|
716
|
+
|
|
717
|
+
The direct forced-field syntax applies to **top-level** unique `where` shapes used by methods such as `update`, `delete`, `upsert`, `findUnique`, and `findUniqueOrThrow`.
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
where: {
|
|
721
|
+
id: true,
|
|
722
|
+
companyId: force(ctx.companyId),
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
This lets the client provide `id`, while the server forces `companyId` into the final Prisma `where`.
|
|
727
|
+
|
|
728
|
+
Do not assume the same forced-field syntax works inside relation write selectors such as `connect`, `disconnect`, `set`, nested `delete`, nested `update.where`, or `connectOrCreate.where`. Relation write selectors use their own validation path and should be configured as normal unique selector allowlists:
|
|
729
|
+
|
|
730
|
+
```ts
|
|
731
|
+
data: {
|
|
732
|
+
tags: {
|
|
733
|
+
connect: { id: true },
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
If you need tenant-safe relation writes, validate ownership in application code, use a top-level guarded operation, or rely on database-level constraints such as foreign keys, RLS, or triggers. Nested relation writes bypass automatic tenant scope injection.
|
|
660
739
|
|
|
661
|
-
|
|
740
|
+
#### Multiple unique selectors
|
|
741
|
+
|
|
742
|
+
If a model has multiple single-field unique selectors and the shape lists more than one, the client may use any allowed selector.
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
where: {
|
|
746
|
+
id: true,
|
|
747
|
+
slug: true,
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
This allows a request with:
|
|
752
|
+
|
|
753
|
+
```ts
|
|
754
|
+
where: { id: 'project_1' }
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
or:
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
where: { slug: 'my-project' }
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
For compound unique constraints, use Prisma's generated compound selector name:
|
|
662
764
|
```prisma
|
|
663
765
|
model ProjectMember {
|
|
664
766
|
tenantId String
|
|
@@ -731,7 +833,17 @@ await prisma.project
|
|
|
731
833
|
})
|
|
732
834
|
```
|
|
733
835
|
|
|
734
|
-
In update mode, all `data` fields are optional. The `where` shape must use Prisma
|
|
836
|
+
In update mode, all `data` fields are optional. The `where` shape must use Prisma `WhereUniqueInput` syntax for `update`.
|
|
837
|
+
|
|
838
|
+
For permission checks, you may include additional direct scalar filters alongside a unique selector:
|
|
839
|
+
```ts
|
|
840
|
+
where: {
|
|
841
|
+
id: true,
|
|
842
|
+
companyId: force(ctx.companyId),
|
|
843
|
+
}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Do not use normal filter operator syntax such as `{ companyId: { equals: force(ctx.companyId) } }` in an update unique where shape.
|
|
735
847
|
|
|
736
848
|
### Forced values
|
|
737
849
|
|
|
@@ -774,7 +886,17 @@ await prisma.project
|
|
|
774
886
|
.delete({ where: { id: 'abc123' } })
|
|
775
887
|
```
|
|
776
888
|
|
|
777
|
-
`data` is not valid for delete shapes. The `where` shape must use Prisma
|
|
889
|
+
`data` is not valid for delete shapes. The `where` shape must use Prisma `WhereUniqueInput` syntax for `delete`.
|
|
890
|
+
|
|
891
|
+
For permission checks, you may include additional direct scalar filters alongside a unique selector:
|
|
892
|
+
```ts
|
|
893
|
+
where: {
|
|
894
|
+
id: true,
|
|
895
|
+
companyId: force(ctx.companyId),
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Do not use normal filter operator syntax such as `{ companyId: { equals: force(ctx.companyId) } }` in a delete unique where shape.
|
|
778
900
|
|
|
779
901
|
### Batch creates
|
|
780
902
|
```ts
|
|
@@ -999,7 +1121,7 @@ The return value is:
|
|
|
999
1121
|
| `shape` | The concrete resolved guard shape after caller matching and dynamic shape execution |
|
|
1000
1122
|
| `body` | The normalized request body. Omitted input becomes `{}` |
|
|
1001
1123
|
| `effectiveReadBody` | The body used for read planning. If the body has no `select` or `include`, the shape's default read projection is applied |
|
|
1002
|
-
| `matchedKey` | The selected
|
|
1124
|
+
| `matchedKey` | The selected declared key, such as `default`, `admin`, or `/user/:id`. For parameterized callers this is the pattern key, not the concrete caller value |
|
|
1003
1125
|
| `wasDynamic` | `true` when the matched shape was a function and was executed with guard context |
|
|
1004
1126
|
|
|
1005
1127
|
`resolve()` uses the same guard extension context and caller selection path as `.findMany()`, `.findFirst()`, and the other guarded methods. If the shape is context-dependent, the shape function is called with the current guard context.
|
|
@@ -1143,6 +1265,8 @@ members: {
|
|
|
1143
1265
|
}
|
|
1144
1266
|
```
|
|
1145
1267
|
|
|
1268
|
+
Unlike top-level unique `where` shapes, relation write selector configs are allowlists for client-provided unique selectors. Do not rely on `force()` directly inside these selector configs unless your runtime version explicitly documents support for it.
|
|
1269
|
+
|
|
1146
1270
|
### Scope implications
|
|
1147
1271
|
|
|
1148
1272
|
Nested writes bypass the scope extension because Prisma extension hooks only fire for top-level operations. This means:
|
|
@@ -1791,22 +1915,40 @@ The `default` fallback works consistently across both `.guard()` and `guard.quer
|
|
|
1791
1915
|
|
|
1792
1916
|
### Note for generated route configs
|
|
1793
1917
|
|
|
1794
|
-
The direct
|
|
1918
|
+
The direct runtime API accepts all three guard input forms:
|
|
1919
|
+
|
|
1920
|
+
- one direct guard shape
|
|
1921
|
+
- one context-dependent shape function
|
|
1922
|
+
- a named shape map
|
|
1923
|
+
|
|
1924
|
+
`prisma-generator-express` mirrors those forms under an operation's `shape` property. It also provides a `variants` descriptor API when each named shape needs its own route hooks:
|
|
1795
1925
|
|
|
1796
1926
|
```ts
|
|
1797
1927
|
const projectConfig = {
|
|
1798
1928
|
findMany: {
|
|
1799
|
-
|
|
1929
|
+
before: [authenticate],
|
|
1930
|
+
variants: {
|
|
1931
|
+
admin: {
|
|
1932
|
+
shape: {
|
|
1933
|
+
where: { title: { contains: true }, status: { equals: true } },
|
|
1934
|
+
take: { max: 100 },
|
|
1935
|
+
},
|
|
1936
|
+
before: [requireAdmin],
|
|
1937
|
+
},
|
|
1800
1938
|
default: {
|
|
1801
|
-
|
|
1802
|
-
|
|
1939
|
+
shape: {
|
|
1940
|
+
where: { title: { contains: true } },
|
|
1941
|
+
take: { max: 20, default: 10 },
|
|
1942
|
+
},
|
|
1803
1943
|
},
|
|
1804
1944
|
},
|
|
1805
1945
|
},
|
|
1806
1946
|
}
|
|
1807
1947
|
```
|
|
1808
1948
|
|
|
1809
|
-
|
|
1949
|
+
Within one generated operation, `shape` and `variants` cannot both be defined. Both may be omitted when the operation only needs operation-wide hooks or pagination. Each `variants[key].shape` is one direct shape or one context-dependent shape function; it is not another nested named map.
|
|
1950
|
+
|
|
1951
|
+
Use `shape` when only guard routing is needed. Use `variants` when hooks need to differ by the matched named shape.
|
|
1810
1952
|
|
|
1811
1953
|
### Caller resolution order
|
|
1812
1954
|
|
|
@@ -1835,16 +1977,50 @@ await prisma.project.guard({ where: { ... } }).findMany(req.body)
|
|
|
1835
1977
|
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.
|
|
1836
1978
|
|
|
1837
1979
|
### Parameterized caller patterns
|
|
1980
|
+
|
|
1838
1981
|
```text
|
|
1839
1982
|
/org/:orgId/users
|
|
1840
1983
|
/org/:orgId/users/:userId
|
|
1841
1984
|
```
|
|
1842
1985
|
|
|
1843
|
-
Matching is case-sensitive.
|
|
1986
|
+
Matching is case-sensitive. A parameter segment starts with `:` and matches exactly one path segment. Segment counts must be equal. Parameters are routing-only and are not extracted into context.
|
|
1987
|
+
|
|
1988
|
+
Caller routing uses this precedence:
|
|
1989
|
+
|
|
1990
|
+
1. Reject any named key that collides with a reserved guard shape key.
|
|
1991
|
+
2. If caller is not a string, use `default` when present; otherwise throw the missing-caller `CallerError`.
|
|
1992
|
+
3. If caller is blank or whitespace-only, skip exact and parameterized matching. Use `default` when present; otherwise throw unknown-caller `CallerError`.
|
|
1993
|
+
4. A non-blank exact key match wins immediately.
|
|
1994
|
+
5. If there is no exact match, evaluate parameterized patterns.
|
|
1995
|
+
6. One matching pattern selects that declared pattern key.
|
|
1996
|
+
7. Multiple matching patterns throw an ambiguous-caller `CallerError` listing every match.
|
|
1997
|
+
8. With no match, use `default` when present; otherwise throw unknown-caller `CallerError`.
|
|
1998
|
+
|
|
1999
|
+
Exact matching therefore wins over a pattern that would also match:
|
|
2000
|
+
|
|
2001
|
+
```ts
|
|
2002
|
+
const shapes = {
|
|
2003
|
+
'customer/123': exactShape,
|
|
2004
|
+
'customer/:id': parameterizedShape,
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Selects "customer/123", not "customer/:id".
|
|
2008
|
+
await prisma.order.guard(shapes, 'customer/123').findMany()
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
For a parameterized caller, `matchedKey` is the declared pattern key. A caller of `customer/123` matched by `customer/:id` produces `matchedKey === 'customer/:id'`. This is the key used for shape memoization and returned by `resolve()`.
|
|
1844
2012
|
|
|
1845
2013
|
### Fail-closed behavior
|
|
1846
2014
|
|
|
1847
|
-
|
|
2015
|
+
Named routing fails closed:
|
|
2016
|
+
|
|
2017
|
+
- A non-string caller with no `default` throws the missing-caller `CallerError`.
|
|
2018
|
+
- A blank or whitespace-only caller never matches a blank key or a parameterized key. It uses `default` or throws unknown-caller `CallerError`.
|
|
2019
|
+
- An unmatched caller uses `default` or throws unknown-caller `CallerError`.
|
|
2020
|
+
- Multiple matching parameterized patterns throw ambiguous-caller `CallerError`.
|
|
2021
|
+
- A caller key that collides with a reserved shape key throws `ShapeError`.
|
|
2022
|
+
|
|
2023
|
+
The same precedence and error behavior apply to `.guard()` and `guard.query().parse()`.
|
|
1848
2024
|
|
|
1849
2025
|
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.
|
|
1850
2026
|
|
|
@@ -2614,5 +2790,4 @@ Possible future improvements:
|
|
|
2614
2790
|
|
|
2615
2791
|
## License
|
|
2616
2792
|
|
|
2617
|
-
MIT
|
|
2618
|
-
|
|
2793
|
+
MIT
|
package/dist/generator/index.js
CHANGED
|
@@ -302,6 +302,12 @@ function emitTypeMap(dmmf) {
|
|
|
302
302
|
metaPairs.push(["isUnsupported", true]);
|
|
303
303
|
if (field.isUnique)
|
|
304
304
|
metaPairs.push(["isUnique", true]);
|
|
305
|
+
if (isRelation && Array.isArray(field.relationFromFields) && field.relationFromFields.length > 0) {
|
|
306
|
+
metaPairs.push([
|
|
307
|
+
"relationFromFields",
|
|
308
|
+
[...field.relationFromFields]
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
305
311
|
return ` ${JSON.stringify(field.name)}: { ${serializeMeta(metaPairs)} },`;
|
|
306
312
|
}).join("\n");
|
|
307
313
|
return ` ${JSON.stringify(model.name)}: {
|