prisma-guard 1.28.0 → 1.29.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 +75 -1
- package/dist/generator/index.js +50 -40
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.cjs +265 -183
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +19 -9
- package/dist/runtime/index.d.ts +19 -9
- package/dist/runtime/index.js +265 -183
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ database
|
|
|
41
41
|
* [Before / After prisma-guard](#before--after-prisma-guard)
|
|
42
42
|
* [Schema annotations](#schema-annotations)
|
|
43
43
|
* [The guard API](#the-guard-api)
|
|
44
|
+
* [Read planning with resolve()](#read-planning-with-resolve)
|
|
44
45
|
* [Relation writes in data shapes](#relation-writes-in-data-shapes)
|
|
45
46
|
* [Logical combinators in where shapes](#logical-combinators-in-where-shapes)
|
|
46
47
|
* [Relation filters in where shapes](#relation-filters-in-where-shapes)
|
|
@@ -511,7 +512,7 @@ When a refine function returns a schema that handles undefined input (e.g. by in
|
|
|
511
512
|
|
|
512
513
|
## The guard API
|
|
513
514
|
|
|
514
|
-
`.guard(shape)` is available on every model delegate. It returns an object with all Prisma methods
|
|
515
|
+
`.guard(shape, caller?)` is available on every model delegate. It returns an object with all Prisma methods plus `resolve()`. The shape defines the boundary; the chained Prisma method validates and executes. `resolve()` is a read-planning helper that resolves the same shape boundary without executing Prisma.
|
|
515
516
|
|
|
516
517
|
### Data shape syntax
|
|
517
518
|
|
|
@@ -938,8 +939,78 @@ Reads: `findMany`, `findFirst`, `findFirstOrThrow`, `findUnique`, `findUniqueOrT
|
|
|
938
939
|
|
|
939
940
|
Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `updateManyAndReturn`, `upsert`, `delete`, `deleteMany`
|
|
940
941
|
|
|
942
|
+
### `findManyPaginated`
|
|
943
|
+
|
|
944
|
+
`findManyPaginated` is an intentional operation shape used by the integrated
|
|
945
|
+
`prisma-generator-express` stack.
|
|
946
|
+
|
|
947
|
+
It is not expected to map 1:1 to a native Prisma Client method. It exists so
|
|
948
|
+
guard shapes can be generated for the paginated route/helper layer.
|
|
949
|
+
|
|
950
|
+
|
|
941
951
|
---
|
|
942
952
|
|
|
953
|
+
|
|
954
|
+
## Read planning with resolve()
|
|
955
|
+
|
|
956
|
+
`.guard(shape, caller?).resolve(body?)` resolves a read shape without executing a Prisma query.
|
|
957
|
+
|
|
958
|
+
Use it when integration code needs to inspect the concrete guard shape and effective read body before deciding how to run a read. Typical use cases are generated routers, progressive response streaming, query planners, and adapters that need the same shape resolution as the real guarded method.
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
const resolved = prisma.user
|
|
962
|
+
.guard(
|
|
963
|
+
{
|
|
964
|
+
me: (ctx) => ({
|
|
965
|
+
where: {
|
|
966
|
+
id: { equals: force(ctx.userId) },
|
|
967
|
+
},
|
|
968
|
+
select: {
|
|
969
|
+
id: true,
|
|
970
|
+
email: true,
|
|
971
|
+
profile: {
|
|
972
|
+
select: {
|
|
973
|
+
id: true,
|
|
974
|
+
displayName: true,
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
}),
|
|
979
|
+
},
|
|
980
|
+
'me',
|
|
981
|
+
)
|
|
982
|
+
.resolve()
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
The return value is:
|
|
986
|
+
|
|
987
|
+
```ts
|
|
988
|
+
{
|
|
989
|
+
shape: GuardShape
|
|
990
|
+
body: Record<string, unknown>
|
|
991
|
+
effectiveReadBody: Record<string, unknown>
|
|
992
|
+
matchedKey: string
|
|
993
|
+
wasDynamic: boolean
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
| Field | Meaning |
|
|
998
|
+
| ----- | ------- |
|
|
999
|
+
| `shape` | The concrete resolved guard shape after caller matching and dynamic shape execution |
|
|
1000
|
+
| `body` | The normalized request body. Omitted input becomes `{}` |
|
|
1001
|
+
| `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` |
|
|
1003
|
+
| `wasDynamic` | `true` when the matched shape was a function and was executed with guard context |
|
|
1004
|
+
|
|
1005
|
+
`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.
|
|
1006
|
+
|
|
1007
|
+
`resolve()` is intentionally read-only. It rejects top-level `data`, `create`, and `update` keys on either the resolved shape or the request body. Use the corresponding guarded write method for writes.
|
|
1008
|
+
|
|
1009
|
+
`effectiveReadBody` is planning input, not final Prisma args. It does not replace the normal guarded read execution pipeline. The actual guarded read method still validates the body, applies forced values, applies default projection rules, injects scope filters, and calls Prisma.
|
|
1010
|
+
|
|
1011
|
+
No Prisma delegate method is called by `resolve()`.
|
|
1012
|
+
|
|
1013
|
+
|
|
943
1014
|
## Relation writes in data shapes
|
|
944
1015
|
|
|
945
1016
|
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.
|
|
@@ -1315,6 +1386,8 @@ where: {
|
|
|
1315
1386
|
|
|
1316
1387
|
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.
|
|
1317
1388
|
|
|
1389
|
+
The same default-projection behavior is exposed for planning through [`resolve()`](#read-planning-with-resolve): `effectiveReadBody` contains the request body plus the synthesized default projection when the client omitted projection.
|
|
1390
|
+
|
|
1318
1391
|
A client-provided `select` or `include` is treated as a narrowing request inside the shape's whitelist. It should not widen back to the full default projection. If the client asks for fewer fields than the shape allows, only the requested allowed fields are returned.
|
|
1319
1392
|
|
|
1320
1393
|
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.
|
|
@@ -2542,3 +2615,4 @@ Possible future improvements:
|
|
|
2542
2615
|
## License
|
|
2543
2616
|
|
|
2544
2617
|
MIT
|
|
2618
|
+
|
package/dist/generator/index.js
CHANGED
|
@@ -117,7 +117,7 @@ import { createGuard } from '${runtimeImportPath}'
|
|
|
117
117
|
import { SCOPE_MAP, TYPE_MAP, ENUM_MAP, ZOD_CHAINS, GUARD_CONFIG, UNIQUE_MAP, ZOD_DEFAULTS } from '${indexImport}'
|
|
118
118
|
import type { ScopeRoot } from '${indexImport}'
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
${emitGuardModelExtension(dmmf)}
|
|
121
121
|
export const guard = createGuard<typeof TYPE_MAP, ScopeRoot, GuardModelExtension>({
|
|
122
122
|
scopeMap: SCOPE_MAP,
|
|
123
123
|
typeMap: TYPE_MAP,
|
|
@@ -134,7 +134,7 @@ export const guard = createGuard<typeof TYPE_MAP, ScopeRoot, GuardModelExtension
|
|
|
134
134
|
function isScopeRoot(documentation) {
|
|
135
135
|
if (!documentation)
|
|
136
136
|
return false;
|
|
137
|
-
const tokens = documentation.split(
|
|
137
|
+
const tokens = documentation.split(/\s+/);
|
|
138
138
|
return tokens.some((t) => t === "@scope-root");
|
|
139
139
|
}
|
|
140
140
|
function reportScopeIssue(mode, errorMsg, warnMsg) {
|
|
@@ -143,9 +143,6 @@ function reportScopeIssue(mode, errorMsg, warnMsg) {
|
|
|
143
143
|
if (mode === "warn")
|
|
144
144
|
console.warn(`prisma-guard: ${warnMsg}`);
|
|
145
145
|
}
|
|
146
|
-
function serializeScopeEntry(e) {
|
|
147
|
-
return `{ fk: ${JSON.stringify(e.fk)}, root: ${JSON.stringify(e.root)}, relationName: ${JSON.stringify(e.relationName)} }`;
|
|
148
|
-
}
|
|
149
146
|
function emitScopeMap(dmmf, onAmbiguousScope) {
|
|
150
147
|
const rootModels = /* @__PURE__ */ new Set();
|
|
151
148
|
for (const model of dmmf.datamodel.models) {
|
|
@@ -205,8 +202,9 @@ function emitScopeMap(dmmf, onAmbiguousScope) {
|
|
|
205
202
|
}
|
|
206
203
|
}
|
|
207
204
|
const roots = Array.from(rootModels).sort();
|
|
205
|
+
const formatEntry = (e) => `{ fk: ${JSON.stringify(e.fk)}, root: ${JSON.stringify(e.root)}, relationName: ${JSON.stringify(e.relationName)} }`;
|
|
208
206
|
const mapEntries = Object.entries(scopeMap).map(([model, entries]) => {
|
|
209
|
-
const entriesStr = entries.map(
|
|
207
|
+
const entriesStr = entries.map(formatEntry).join(", ");
|
|
210
208
|
return ` ${model}: [${entriesStr}],`;
|
|
211
209
|
}).join("\n");
|
|
212
210
|
const scopeRootType = roots.length > 0 ? roots.map((r) => `'${r}'`).join(" | ") : "never";
|
|
@@ -323,7 +321,7 @@ ${fieldEntries}
|
|
|
323
321
|
return `{ selector: ${JSON.stringify(c.selector)}, fields: [${fields}] }`;
|
|
324
322
|
}).join(", ");
|
|
325
323
|
return ` ${JSON.stringify(model.name)}: [${constraintsStr}],`;
|
|
326
|
-
}).filter(
|
|
324
|
+
}).filter((entry) => entry !== null).join("\n");
|
|
327
325
|
const typeMapSource = `export const TYPE_MAP = {
|
|
328
326
|
${modelEntries}
|
|
329
327
|
} as const
|
|
@@ -350,21 +348,22 @@ ${typesSource}
|
|
|
350
348
|
// src/shared/operation-shape-keys.ts
|
|
351
349
|
var OPERATION_SHAPE_KEYS = {
|
|
352
350
|
findMany: ["where", "include", "select", "orderBy", "cursor", "take", "skip", "distinct"],
|
|
351
|
+
findManyPaginated: ["where", "include", "select", "orderBy", "cursor", "take", "skip", "distinct"],
|
|
353
352
|
findFirst: ["where", "include", "select", "orderBy", "cursor", "take", "skip", "distinct"],
|
|
354
353
|
findFirstOrThrow: ["where", "include", "select", "orderBy", "cursor", "take", "skip", "distinct"],
|
|
355
354
|
findUnique: ["where", "include", "select"],
|
|
356
355
|
findUniqueOrThrow: ["where", "include", "select"],
|
|
357
356
|
count: ["where", "select", "cursor", "orderBy", "skip", "take"],
|
|
358
357
|
aggregate: ["where", "orderBy", "cursor", "take", "skip", "_count", "_avg", "_sum", "_min", "_max"],
|
|
359
|
-
groupBy: ["where", "
|
|
360
|
-
create: ["data", "
|
|
361
|
-
createMany: ["data"],
|
|
362
|
-
createManyAndReturn: ["data", "select", "include"],
|
|
363
|
-
update: ["
|
|
364
|
-
updateMany: ["
|
|
365
|
-
updateManyAndReturn: ["
|
|
366
|
-
upsert: ["where", "create", "update", "
|
|
367
|
-
delete: ["where", "
|
|
358
|
+
groupBy: ["where", "orderBy", "by", "having", "take", "skip", "_count", "_avg", "_sum", "_min", "_max"],
|
|
359
|
+
create: ["data", "include", "select"],
|
|
360
|
+
createMany: ["data", "skipDuplicates"],
|
|
361
|
+
createManyAndReturn: ["data", "select", "include", "skipDuplicates"],
|
|
362
|
+
update: ["where", "data", "include", "select"],
|
|
363
|
+
updateMany: ["where", "data"],
|
|
364
|
+
updateManyAndReturn: ["where", "data", "select", "include"],
|
|
365
|
+
upsert: ["where", "create", "update", "include", "select"],
|
|
366
|
+
delete: ["where", "include", "select"],
|
|
368
367
|
deleteMany: ["where"]
|
|
369
368
|
};
|
|
370
369
|
var READ_METHOD_ALLOWED_ARGS = {
|
|
@@ -938,7 +937,7 @@ function createScalarBase(strictDecimal) {
|
|
|
938
937
|
return {
|
|
939
938
|
String: () => z.string(),
|
|
940
939
|
Int: () => z.number().int(),
|
|
941
|
-
Float: () => z.number(),
|
|
940
|
+
Float: () => z.number().finite(),
|
|
942
941
|
Decimal: createDecimalFactory(strictDecimal),
|
|
943
942
|
BigInt: () => z.union([
|
|
944
943
|
z.bigint(),
|
|
@@ -1085,6 +1084,33 @@ ${entries}
|
|
|
1085
1084
|
defaults
|
|
1086
1085
|
};
|
|
1087
1086
|
}
|
|
1087
|
+
function emitZodDefaults(defaults) {
|
|
1088
|
+
const entries = Object.entries(defaults);
|
|
1089
|
+
if (entries.length === 0) {
|
|
1090
|
+
return `export const ZOD_DEFAULTS: Record<string, readonly string[]> = {}
|
|
1091
|
+
`;
|
|
1092
|
+
}
|
|
1093
|
+
const mapEntries = entries.map(([model, fields]) => {
|
|
1094
|
+
const fieldsStr = fields.map((field) => JSON.stringify(field)).join(", ");
|
|
1095
|
+
return ` ${JSON.stringify(model)}: [${fieldsStr}],`;
|
|
1096
|
+
}).join("\n");
|
|
1097
|
+
return `export const ZOD_DEFAULTS: Record<string, readonly string[]> = {
|
|
1098
|
+
${mapEntries}
|
|
1099
|
+
}
|
|
1100
|
+
`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/generator/emit-guard-config.ts
|
|
1104
|
+
function emitGuardConfig(cfg) {
|
|
1105
|
+
return `export const GUARD_CONFIG = {
|
|
1106
|
+
onMissingScopeContext: ${JSON.stringify(cfg.onMissingScopeContext)},
|
|
1107
|
+
findUniqueMode: ${JSON.stringify(cfg.findUniqueMode)},
|
|
1108
|
+
onScopeRelationWrite: ${JSON.stringify(cfg.onScopeRelationWrite)},
|
|
1109
|
+
strictDecimal: ${JSON.stringify(cfg.strictDecimal)},
|
|
1110
|
+
enforceProjection: ${JSON.stringify(cfg.enforceProjection)},
|
|
1111
|
+
} as const
|
|
1112
|
+
`;
|
|
1113
|
+
}
|
|
1088
1114
|
|
|
1089
1115
|
// src/generator/index.ts
|
|
1090
1116
|
var { generatorHandler } = pkg;
|
|
@@ -1112,21 +1138,6 @@ function parseGeneratorConfig(raw) {
|
|
|
1112
1138
|
}).join("; ");
|
|
1113
1139
|
throw new Error(`prisma-guard: Invalid generator config: ${issues}`);
|
|
1114
1140
|
}
|
|
1115
|
-
function emitZodDefaults(defaults) {
|
|
1116
|
-
const entries = Object.entries(defaults);
|
|
1117
|
-
if (entries.length === 0) {
|
|
1118
|
-
return `export const ZOD_DEFAULTS: Record<string, readonly string[]> = {}
|
|
1119
|
-
`;
|
|
1120
|
-
}
|
|
1121
|
-
const mapEntries = entries.map(([model, fields]) => {
|
|
1122
|
-
const fieldsStr = fields.map((field) => JSON.stringify(field)).join(", ");
|
|
1123
|
-
return ` ${JSON.stringify(model)}: [${fieldsStr}],`;
|
|
1124
|
-
}).join("\n");
|
|
1125
|
-
return `export const ZOD_DEFAULTS: Record<string, readonly string[]> = {
|
|
1126
|
-
${mapEntries}
|
|
1127
|
-
}
|
|
1128
|
-
`;
|
|
1129
|
-
}
|
|
1130
1141
|
function getProviderValue(provider) {
|
|
1131
1142
|
if (typeof provider === "string")
|
|
1132
1143
|
return provider;
|
|
@@ -1195,14 +1206,13 @@ generatorHandler({
|
|
|
1195
1206
|
const dmmf = options.dmmf;
|
|
1196
1207
|
const parts = [];
|
|
1197
1208
|
parts.push(
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
`
|
|
1209
|
+
emitGuardConfig({
|
|
1210
|
+
onMissingScopeContext: cfg.onMissingScopeContext,
|
|
1211
|
+
findUniqueMode: cfg.findUniqueMode,
|
|
1212
|
+
onScopeRelationWrite: cfg.onScopeRelationWrite,
|
|
1213
|
+
strictDecimal: cfg.strictDecimal,
|
|
1214
|
+
enforceProjection: cfg.enforceProjection
|
|
1215
|
+
})
|
|
1206
1216
|
);
|
|
1207
1217
|
const { source: scopeSource } = emitScopeMap(dmmf, cfg.onAmbiguousScope);
|
|
1208
1218
|
parts.push(scopeSource);
|