prisma-guard 1.7.0 → 1.8.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
@@ -835,6 +835,40 @@ where: {
835
835
 
836
836
  ---
837
837
 
838
+ ## Read projection auto-apply
839
+
840
+ When a read shape defines `select` or `include`, the projection serves two roles: it whitelists what the client is allowed to request, and it provides the default projection when the client omits `select`/`include` from the body.
841
+
842
+ If the client sends a body without `select` or `include`, the shape's projection is automatically synthesized and passed to Prisma. This eliminates the need for the client to duplicate the field list that the backend already defines.
843
+
844
+ ```ts
845
+ await prisma.company
846
+ .guard({
847
+ where: { id: { equals: true } },
848
+ select: {
849
+ id: true,
850
+ name: true,
851
+ description: true,
852
+ posts: {
853
+ select: { id: true, title: true },
854
+ take: { max: 10, default: 5 },
855
+ where: { isDeleted: { equals: false } },
856
+ },
857
+ },
858
+ })
859
+ .findFirst({ where: { id: { equals: 'abc' } } })
860
+ ```
861
+
862
+ The client sends only `{ where: { id: { equals: 'abc' } } }`. The shape's `select` is applied automatically, nested `take` defaults and forced `where` conditions are resolved through the normal pipeline.
863
+
864
+ If the client does send `select` or `include`, the shape acts as a whitelist — only the fields and relations defined in the shape are accepted. This behavior is unchanged from before.
865
+
866
+ The synthesized projection includes the structural skeleton only: scalar fields as `true`, nested `select`/`include` trees. Client-controllable args like `orderBy`, `take`, `skip`, and `cursor` on nested relations are omitted from the synthesized body. Defaults (e.g. `take: { default: 5 }`) are filled by zod schema parsing, and forced `where` conditions are merged by the forced tree pipeline.
867
+
868
+ This applies to all read methods: `findMany`, `findFirst`, `findFirstOrThrow`, `findUnique`, `findUniqueOrThrow`, `count`, `aggregate`, and `groupBy`. Methods where `select`/`include` is not valid (`aggregate`, `groupBy`) already reject those shape keys upstream, so auto-apply never triggers for them.
869
+
870
+ ---
871
+
838
872
  ## Mutation return projection
839
873
 
840
874
  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.
@@ -931,9 +965,11 @@ await prisma.project
931
965
 
932
966
  The returned `members` will always be filtered to `isActive = true`, regardless of what the client sends.
933
967
 
934
- ### Projection is optional by default
968
+ ### Mutation projection is optional by default
969
+
970
+ For mutation methods, if the shape defines `select` or `include` but the client omits them from the body, the mutation returns the full record (default Prisma behavior). Mutation projection shapes only validate and constrain **client-requested** projections unless [enforced projection mode](#enforced-projection-mode) is enabled.
935
971
 
936
- If the shape does not define `select` or `include`, but the caller also omits them from the body, the mutation returns the full record (default Prisma behavior). Projection shapes only validate and constrain **client-requested** projections — they do not enforce a fixed output boundary unless [enforced projection mode](#enforced-projection-mode) is enabled.
972
+ This differs from read methods, where the shape's projection is [automatically applied as default](#read-projection-auto-apply) when the client omits it.
937
973
 
938
974
  ### select and include are mutually exclusive
939
975
 
@@ -947,9 +983,11 @@ Same as reads: a shape (and a body) cannot define both `select` and `include` at
947
983
 
948
984
  ## Enforced projection mode
949
985
 
950
- By default, projection shapes only constrain client-requested projections. If the client omits `select`/`include` from the body, Prisma returns its default full payload.
986
+ By default, mutation projection shapes only constrain client-requested projections. If the client omits `select`/`include` from the mutation body, Prisma returns its default full payload.
951
987
 
952
- When `enforceProjection` is enabled, the shape's projection is always applied — even when the client does not request one. If the shape defines `select` or `include` and the client omits them, prisma-guard synthesizes a projection from the shape and passes it to Prisma.
988
+ When `enforceProjection` is enabled, mutation shapes' projection is always applied — even when the client does not request one. If the shape defines `select` or `include` and the client omits them, prisma-guard synthesizes a projection from the shape and passes it to Prisma.
989
+
990
+ This setting applies to mutation methods only. Read methods always auto-apply the shape's projection as default when the client omits it — see [Read projection auto-apply](#read-projection-auto-apply).
953
991
 
954
992
  ### Configuration
955
993
  ```prisma
@@ -987,7 +1025,7 @@ When the client omits `select`/`include`, prisma-guard synthesizes a default pro
987
1025
 
988
1026
  When the client does provide `select`/`include`, behavior is identical regardless of this setting: the client's projection is validated against the shape.
989
1027
 
990
- This mode applies to all methods that support projection: `create`, `update`, `upsert`, `delete`, `createManyAndReturn`, and `updateManyAndReturn`.
1028
+ This mode applies to mutation methods that support projection: `create`, `update`, `upsert`, `delete`, `createManyAndReturn`, and `updateManyAndReturn`.
991
1029
 
992
1030
  ---
993
1031
 
@@ -1597,7 +1635,7 @@ If the same field and operator appear as forced values in different parts of a w
1597
1635
 
1598
1636
  ### Mutation projection shapes do not enforce a fixed output boundary by default
1599
1637
 
1600
- If a mutation shape defines `select` or `include` but the client omits them from the body, Prisma returns its default full payload. Projection shapes only validate and constrain client-requested projections. Enable [enforced projection mode](#enforced-projection-mode) to always apply the shape's projection.
1638
+ If a mutation shape defines `select` or `include` but the client omits them from the body, Prisma returns its default full payload. Mutation projection shapes only validate and constrain client-requested projections. Enable [enforced projection mode](#enforced-projection-mode) to always apply the shape's projection. This limitation applies to mutations only — read methods always auto-apply the shape's projection as default.
1601
1639
 
1602
1640
  ### `create` and `update` are reserved shape keys
1603
1641
 
@@ -1699,6 +1737,8 @@ At runtime, `guard.extension()` creates a Prisma extension that provides:
1699
1737
 
1700
1738
  The `.guard()` call validates against the shape, merges forced values, and delegates to the underlying Prisma method. The scope layer runs transparently underneath.
1701
1739
 
1740
+ For read methods, when the shape defines `select` or `include` and the client body omits them, the shape's projection is automatically synthesized and passed to Prisma. The synthesized body includes the structural skeleton only (scalar fields as `true`, nested `select`/`include` trees). Client-controllable args on nested relations are omitted — defaults are filled by zod schema parsing and forced where conditions are merged by the forced tree pipeline. This ensures the shape defines both the security boundary and the default response shape in a single declaration.
1741
+
1702
1742
  For create operations, fields tracked in `ZOD_DEFAULTS` that are omitted from the data shape are auto-injected as forced values. The runtime evaluates the field's Zod schema with `undefined` input and uses the resulting value. This ensures `@zod .default(...)` and `@zod .catch(...)` produce correct data even when the field is not listed in the shape.
1703
1743
 
1704
1744
  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.
@@ -1790,6 +1830,8 @@ For upsert, `create` and `update` data schemas are cached independently under na
1790
1830
  | conflicting forced where values | error always (ShapeError) |
1791
1831
  | invalid context function return | error always (PolicyError) |
1792
1832
  | `@zod .default()`/`.catch()` field omitted from shape | auto-injected as forced value |
1833
+ | read shape with select/include, client omits | auto-applied as default projection |
1834
+ | mutation shape with select/include, client omits | full payload unless enforceProjection enabled |
1793
1835
 
1794
1836
  ---
1795
1837
 
@@ -1855,6 +1897,7 @@ Node 22
1855
1897
  8. No overloaded sentinel values — `true` always means client-controlled, `force()` for forced booleans
1856
1898
  9. Upsert uses `create`/`update` keys, not `data` — matches Prisma's own API shape
1857
1899
  10. Shape config values are validated strictly — `true` means enabled, anything else is rejected
1900
+ 11. Read shapes with projection define both the security boundary and the default response — no client duplication needed
1858
1901
 
1859
1902
  ---
1860
1903
 
@@ -1876,6 +1919,7 @@ Node 22
1876
1919
  | Create completeness validation | yes | no |
1877
1920
  | Mutation return projection | yes | manual |
1878
1921
  | Enforced projection mode | opt-in | no |
1922
+ | Read projection auto-apply | yes | no |
1879
1923
  | Upsert support | yes | manual |
1880
1924
  | Inline field refine in data shapes | yes | n/a |
1881
1925
  | ZodError wrapping | opt-in | n/a |
@@ -3503,7 +3503,18 @@ function createModelGuardExtension(config) {
3503
3503
  resolved.wasDynamic
3504
3504
  );
3505
3505
  const isUnique = UNIQUE_READ_METHODS.has(method);
3506
- const args = applyBuiltShape(built, resolved.body, isUnique);
3506
+ let effectiveBody = resolved.body;
3507
+ const hasShapeProjection = !!resolved.shape.select || !!resolved.shape.include;
3508
+ if (hasShapeProjection) {
3509
+ const hasBodyProjection = "select" in resolved.body || "include" in resolved.body;
3510
+ if (!hasBodyProjection) {
3511
+ effectiveBody = {
3512
+ ...resolved.body,
3513
+ ...buildDefaultProjectionBody(resolved.shape)
3514
+ };
3515
+ }
3516
+ }
3517
+ const args = applyBuiltShape(built, effectiveBody, isUnique);
3507
3518
  if (isUnique && args.where) {
3508
3519
  validateResolvedUniqueWhere(
3509
3520
  modelName,