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 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. The shape defines the boundary; the method validates and executes.
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
+
@@ -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
- ` + emitGuardModelExtension(dmmf) + `
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(/[\s\n\r]+/);
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(serializeScopeEntry).join(", ");
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(Boolean).join("\n");
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", "by", "having", "_count", "_avg", "_sum", "_min", "_max", "orderBy", "take", "skip"],
360
- create: ["data", "select", "include"],
361
- createMany: ["data"],
362
- createManyAndReturn: ["data", "select", "include"],
363
- update: ["data", "where", "select", "include"],
364
- updateMany: ["data", "where"],
365
- updateManyAndReturn: ["data", "where", "select", "include"],
366
- upsert: ["where", "create", "update", "select", "include"],
367
- delete: ["where", "select", "include"],
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
- `export const GUARD_CONFIG = {
1199
- onMissingScopeContext: ${JSON.stringify(cfg.onMissingScopeContext)},
1200
- findUniqueMode: ${JSON.stringify(cfg.findUniqueMode)},
1201
- onScopeRelationWrite: ${JSON.stringify(cfg.onScopeRelationWrite)},
1202
- strictDecimal: ${JSON.stringify(cfg.strictDecimal)},
1203
- enforceProjection: ${JSON.stringify(cfg.enforceProjection)},
1204
- } as const
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);