prisma-guard 1.6.1 → 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 +50 -6
- package/dist/runtime/index.cjs +72 -15
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +72 -15
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
###
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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 |
|
package/dist/runtime/index.cjs
CHANGED
|
@@ -2224,21 +2224,67 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
|
|
|
2224
2224
|
}
|
|
2225
2225
|
}
|
|
2226
2226
|
if (shape.orderBy) {
|
|
2227
|
-
if (method === "groupBy" && shape.
|
|
2227
|
+
if (method === "groupBy" && shape.by) {
|
|
2228
2228
|
const sortEnum = import_zod7.z.enum(["asc", "desc"]);
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
groupByOrderFields
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2229
|
+
const bySet = new Set(shape.by);
|
|
2230
|
+
if (shape.orderBy === true) {
|
|
2231
|
+
const groupByOrderFields = {};
|
|
2232
|
+
for (const field of shape.by) {
|
|
2233
|
+
groupByOrderFields[field] = sortEnum.optional();
|
|
2234
|
+
}
|
|
2235
|
+
groupByOrderFields["_count"] = sortEnum.optional();
|
|
2236
|
+
const fieldKeys = Object.keys(groupByOrderFields);
|
|
2237
|
+
const singleSchema = import_zod7.z.object(groupByOrderFields).strict().refine(
|
|
2238
|
+
(v) => fieldKeys.some(
|
|
2239
|
+
(k) => v[k] !== void 0
|
|
2240
|
+
),
|
|
2241
|
+
{ message: "orderBy must specify at least one field" }
|
|
2242
|
+
);
|
|
2243
|
+
schemaFields["orderBy"] = import_zod7.z.union([singleSchema, import_zod7.z.array(singleSchema).min(1)]).optional();
|
|
2244
|
+
} else {
|
|
2245
|
+
const groupByOrderFields = {};
|
|
2246
|
+
for (const [fieldName, config] of Object.entries(shape.orderBy)) {
|
|
2247
|
+
if (fieldName === "_count") {
|
|
2248
|
+
if (config === true) {
|
|
2249
|
+
groupByOrderFields["_count"] = sortEnum.optional();
|
|
2250
|
+
} else if (typeof config === "object" && config !== null) {
|
|
2251
|
+
const countFields = {};
|
|
2252
|
+
for (const countField of Object.keys(config)) {
|
|
2253
|
+
if (!bySet.has(countField)) {
|
|
2254
|
+
throw new ShapeError(
|
|
2255
|
+
`orderBy _count field "${countField}" must be included in "by" for groupBy`
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
countFields[countField] = sortEnum.optional();
|
|
2259
|
+
}
|
|
2260
|
+
const countKeys = Object.keys(countFields);
|
|
2261
|
+
groupByOrderFields["_count"] = import_zod7.z.object(countFields).strict().refine(
|
|
2262
|
+
(v) => countKeys.some(
|
|
2263
|
+
(k) => v[k] !== void 0
|
|
2264
|
+
),
|
|
2265
|
+
{
|
|
2266
|
+
message: "orderBy._count must specify at least one field"
|
|
2267
|
+
}
|
|
2268
|
+
).optional();
|
|
2269
|
+
}
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
if (!bySet.has(fieldName)) {
|
|
2273
|
+
throw new ShapeError(
|
|
2274
|
+
`orderBy field "${fieldName}" must be included in "by" for groupBy`
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
groupByOrderFields[fieldName] = sortEnum.optional();
|
|
2278
|
+
}
|
|
2279
|
+
const fieldKeys = Object.keys(groupByOrderFields);
|
|
2280
|
+
const singleSchema = import_zod7.z.object(groupByOrderFields).strict().refine(
|
|
2281
|
+
(v) => fieldKeys.some(
|
|
2282
|
+
(k) => v[k] !== void 0
|
|
2283
|
+
),
|
|
2284
|
+
{ message: "orderBy must specify at least one field" }
|
|
2285
|
+
);
|
|
2286
|
+
schemaFields["orderBy"] = import_zod7.z.union([singleSchema, import_zod7.z.array(singleSchema).min(1)]).optional();
|
|
2287
|
+
}
|
|
2242
2288
|
} else {
|
|
2243
2289
|
schemaFields["orderBy"] = argsBuilder.buildOrderBySchema(
|
|
2244
2290
|
model,
|
|
@@ -3457,7 +3503,18 @@ function createModelGuardExtension(config) {
|
|
|
3457
3503
|
resolved.wasDynamic
|
|
3458
3504
|
);
|
|
3459
3505
|
const isUnique = UNIQUE_READ_METHODS.has(method);
|
|
3460
|
-
|
|
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);
|
|
3461
3518
|
if (isUnique && args.where) {
|
|
3462
3519
|
validateResolvedUniqueWhere(
|
|
3463
3520
|
modelName,
|