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 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. For these methods, unique fields are configured directly in the shape:
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 `WhereInput` filters used by methods such as `findMany`, `findFirst`, `count`, `updateMany`, and `deleteMany`.
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
- If a model has multiple single-field unique selectors and the shape lists more than one, the client may use any allowed selector. For example, `where: { id: true, slug: true }` allows a request with `where: { id: '...' }` or `where: { slug: '...' }`. For compound unique constraints, use Prisma's generated compound selector name:
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 unique selector syntax for `update`.
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 unique selector syntax for `delete`.
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 named-shape key, such as `default`, `admin`, or `/user/me` |
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 `.guard({ where: ... })` form is still valid in the runtime API. Generated route configs from `prisma-generator-express`, however, type operation `shape` as a named shape map. For a single generated-route shape, put it under `default`:
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
- shape: {
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
- where: { title: { contains: true } },
1802
- take: { max: 50, default: 20 },
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
- Additional keys such as `admin`, `public`, or path patterns are caller-routed variants. Reserved shape keys such as `where`, `data`, `include`, and `select` cannot be used as caller keys.
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. Exact matches are checked first. If no exact match is found, parameterized patterns are evaluated. Parameters are routing-only and are not extracted into context.
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
- 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`.
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
@@ -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)}: {