prisma-generator-express 1.64.2 → 1.64.4

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.
@@ -107,6 +107,91 @@ export type RouteConfig<
107
107
  >,
108
108
  HonoOpKeys
109
109
  > & {
110
+ /**
111
+ * SEVEN INDEPENDENT GUARD CONTROLS. Every default reproduces the public
112
+ * package's pre-1.64.2 behaviour exactly.
113
+ *
114
+ * 1.64.2 shipped these behaviours ON by default, which was a breaking change
115
+ * to a published package. The first correction put all seven behind one
116
+ * `requireGuard` switch, which was also wrong: they are unrelated decisions,
117
+ * and bundling them means a consumer who wants a missing-guard refusal is also
118
+ * forced into a hook-ordering change and the removal of a route they may use.
119
+ *
120
+ * So each is its own option. An unset option means the upstream default — never
121
+ * an opinionated value chosen on the consumer's behalf. `HARDENED_GUARD_PROFILE`
122
+ * selects all seven together for those who want them, but it is a set of values
123
+ * to spread, not a mode: what gets stored is the individual settings.
124
+ *
125
+ * `GUARD_OPTION_METADATA` describes these machine-readably, so a UI can render
126
+ * them without a hand-maintained second copy of the defaults.
127
+ */
128
+
129
+ /**
130
+ * Refuse an operation that configures no guard at all. **Default `false`.**
131
+ *
132
+ * With it off — the upstream default — an unconfigured operation is emitted and
133
+ * passes the caller's own where/select/include straight to Prisma. On, router
134
+ * construction throws, which on a Worker is boot.
135
+ */
136
+ requireGuardShape?: boolean
137
+
138
+ /**
139
+ * Validate that a shape constrains something. **Default `false`.**
140
+ *
141
+ * On, `shape: {}` and a shape mixing guard keys with non-guard keys (usually a
142
+ * typo: `wheer`, `iclude`) are refused, at the top level and inside variants.
143
+ */
144
+ validateGuardShapes?: boolean
145
+
146
+ /**
147
+ * Require `allowDefaultVariant: true` on an operation carrying a variant named
148
+ * `default`. **Default `false`.**
149
+ *
150
+ * A `default` variant answers every unrecognised, blank and missing caller.
151
+ * That is safe when it is the most restrictive variant and dangerous when it is
152
+ * the most permissive, and nothing can tell which from the shape.
153
+ */
154
+ requireDefaultVariantOptIn?: boolean
155
+
156
+ /**
157
+ * Register the `updateEach` batch route. **Default `true`.**
158
+ *
159
+ * It bypasses guard shapes by design — a batch of `{ where, data }` applied
160
+ * directly — and is protected only by a development-only warning. Set `false`
161
+ * to refuse it at router construction.
162
+ */
163
+ enableUpdateEach?: boolean
164
+
165
+ /**
166
+ * When guard failures are raised relative to operation `before` hooks.
167
+ * **Default `'after-hooks'`.**
168
+ *
169
+ * `'after-hooks'` is upstream behaviour: a hook may answer the request before
170
+ * variant resolution is settled. `'before-hooks'` settles the guard first, so a
171
+ * hook cannot answer a request whose guard was never established.
172
+ */
173
+ guardResolutionOrder?: 'after-hooks' | 'before-hooks'
174
+
175
+ /**
176
+ * Honour `E2E=true` in the environment as a guard bypass. **Default `true`.**
177
+ *
178
+ * Upstream behaviour, and a real hazard on an edge runtime where that variable
179
+ * is an ordinary config var — set on staging, copied forward, flagged as
180
+ * security-relevant nowhere. `false` makes guard behaviour a property of the
181
+ * artifact alone.
182
+ */
183
+ allowE2EGuardBypass?: boolean
184
+
185
+ /**
186
+ * Check what a shape FUNCTION returned before using it. **Default `false`.**
187
+ *
188
+ * A function shape is opaque at construction time. On, a resolved value that is
189
+ * empty, not an object, or carrying non-guard keys fails the request with 500 —
190
+ * a deployment fault, not a caller fault — instead of running unguarded. It
191
+ * also resolves the function exactly once and passes the validated value on.
192
+ */
193
+ validateResolvedShapes?: boolean
194
+
110
195
  findUnique?: OperationConfig<TShape, TEnv> | false
111
196
  findUniqueOrThrow?: OperationConfig<TShape, TEnv> | false
112
197
  findFirst?: OperationConfig<TShape, TEnv> | false
@@ -119,6 +119,14 @@ export type BaseOperationConfig<
119
119
  before?: TBefore[]
120
120
  after?: TAfter[]
121
121
  pagination?: Partial<PaginationConfig>
122
+ /**
123
+ * Confirms that a variant literally named `default` is meant to answer every
124
+ * unrecognised, blank and missing caller.
125
+ *
126
+ * Only consulted when `requireDefaultVariantOptIn` is on. Otherwise the key
127
+ * behaves as it always has, so this is inert for existing configurations.
128
+ */
129
+ allowDefaultVariant?: boolean
122
130
  }
123
131
 
124
132
  export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
@@ -216,6 +224,77 @@ type OperationConfigInput<TBefore, TAfter> = {
216
224
  pagination?: Partial<PaginationConfig>
217
225
  }
218
226
 
227
+ /**
228
+ * The seven guard controls, resolved — the RUNTIME half.
229
+ *
230
+ * This lives here, not in the compiled `src/guardOptions.ts`, because a generated
231
+ * project gets these files copied in and must run with no dependency on this
232
+ * package. The two cannot import each other across that boundary, so the values
233
+ * appear twice and `guardOptionIndependence.test.ts` asserts they agree — against
234
+ * this resolver, which is the one the router actually runs.
235
+ *
236
+ * The machine-readable METADATA is compiled and public: see src/guardOptions.ts.
237
+ * Defining it here would put it in a file that never reaches `dist`.
238
+ */
239
+ export type GuardPolicy = {
240
+ requireGuardShape: boolean
241
+ validateGuardShapes: boolean
242
+ requireDefaultVariantOptIn: boolean
243
+ enableUpdateEach: boolean
244
+ guardResolutionOrder: 'after-hooks' | 'before-hooks'
245
+ allowE2EGuardBypass: boolean
246
+ validateResolvedShapes: boolean
247
+ }
248
+
249
+ /**
250
+ * PRE-1.64.2 BEHAVIOUR, exactly. Changing any value here changes what an
251
+ * unconfigured consumer gets on upgrade, which is the thing that must not happen.
252
+ */
253
+ export const UPSTREAM_GUARD_DEFAULTS: Readonly<GuardPolicy> = Object.freeze({
254
+ requireGuardShape: false,
255
+ validateGuardShapes: false,
256
+ requireDefaultVariantOptIn: false,
257
+ enableUpdateEach: true,
258
+ guardResolutionOrder: 'after-hooks',
259
+ allowE2EGuardBypass: true,
260
+ validateResolvedShapes: false,
261
+ })
262
+
263
+ /**
264
+ * Every control at its most restrictive — a SHORTCUT, not a mode.
265
+ *
266
+ * Spread it, edit what you disagree with, and store the result. There is
267
+ * deliberately no `preset: 'hardened'` option: a stored preset name would mean a
268
+ * future version of this package could silently change what a consumer's
269
+ * configuration does, which is the same class of mistake as 1.64.2 itself.
270
+ */
271
+ export const HARDENED_GUARD_PROFILE: Readonly<GuardPolicy> = Object.freeze({
272
+ requireGuardShape: true,
273
+ validateGuardShapes: true,
274
+ requireDefaultVariantOptIn: true,
275
+ enableUpdateEach: false,
276
+ guardResolutionOrder: 'before-hooks',
277
+ allowE2EGuardBypass: false,
278
+ validateResolvedShapes: true,
279
+ })
280
+
281
+ /** Fill in the defaults. Absent means upstream, never an opinion. */
282
+ export function resolveGuardPolicy(config: Partial<GuardPolicy> | undefined): GuardPolicy {
283
+ return {
284
+ requireGuardShape: config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
285
+ validateGuardShapes: config?.validateGuardShapes ?? UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
286
+ requireDefaultVariantOptIn:
287
+ config?.requireDefaultVariantOptIn ?? UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
288
+ enableUpdateEach: config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
289
+ guardResolutionOrder:
290
+ config?.guardResolutionOrder ?? UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
291
+ allowE2EGuardBypass:
292
+ config?.allowE2EGuardBypass ?? UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
293
+ validateResolvedShapes:
294
+ config?.validateResolvedShapes ?? UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
295
+ }
296
+ }
297
+
219
298
  function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
220
299
  if (shape === undefined) return { kind: 'none' }
221
300
  if (typeof shape === 'function') return { kind: 'single' }
@@ -295,34 +374,79 @@ function validateShapeConfig(shape: unknown, location: string): void {
295
374
  export function validateOperationConfig(
296
375
  config: { shape?: unknown; variants?: unknown; allowDefaultVariant?: unknown } | undefined,
297
376
  location: string,
377
+ policy: Partial<GuardPolicy> = {},
298
378
  ): void {
379
+ const { requireGuardShape, validateGuardShapes, requireDefaultVariantOptIn } =
380
+ resolveGuardPolicy(policy)
381
+
382
+ /**
383
+ * THE CHECKS THAT PREDATE 1.64.2 RUN UNCONDITIONALLY.
384
+ *
385
+ * Everything below them is gated per option. Note each gate stands alone: a
386
+ * consumer who enables `validateGuardShapes` gets shape validation and NOT a
387
+ * missing-guard refusal, and vice versa. That independence is the point, and
388
+ * `guardOptionIndependence.test.ts` asserts it option by option.
389
+ */
299
390
  if (!config) {
300
- throw new Error(
301
- location + ': no guard configured. Every generated operation must define ' +
302
- '"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
303
- 'own where/select/include straight to Prisma.',
304
- )
391
+ if (requireGuardShape) {
392
+ throw new Error(
393
+ location + ': no guard configured. Every generated operation must define ' +
394
+ '"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
395
+ 'own where/select/include straight to Prisma.',
396
+ )
397
+ }
398
+ return
305
399
  }
306
400
 
307
401
  const hasShape = config.shape !== undefined
308
402
  const hasVariants = config.variants !== undefined
309
403
 
404
+ // Pre-1.64.2. Not gated.
310
405
  if (hasShape && hasVariants) {
311
406
  throw new Error(location + ': shape and variants cannot both be defined')
312
407
  }
313
408
 
314
409
  if (!hasShape && !hasVariants) {
315
- throw new Error(
316
- location + ': no guard configured. Define "shape" or "variants".',
317
- )
410
+ if (requireGuardShape) {
411
+ throw new Error(
412
+ location + ': no guard configured. Define "shape" or "variants".',
413
+ )
414
+ }
415
+ return
318
416
  }
319
417
 
320
418
  if (hasShape) {
321
- validateShapeConfig(config.shape, location)
419
+ if (validateGuardShapes) validateShapeConfig(config.shape, location)
322
420
  return
323
421
  }
324
422
 
325
- const variants = config.variants
423
+ validateVariantMap(
424
+ config.variants,
425
+ location,
426
+ { validateGuardShapes, requireDefaultVariantOptIn },
427
+ config.allowDefaultVariant,
428
+ )
429
+ }
430
+
431
+ /**
432
+ * The variant-map checks.
433
+ *
434
+ * The four that predate 1.64.2 — non-array object, at least one entry, no
435
+ * reserved-key collision, every entry an object carrying a `shape` — run
436
+ * ALWAYS, because they are not new and removing them would be its own breaking
437
+ * change.
438
+ *
439
+ * The two the hardening added are gated on their OWN options, independently:
440
+ * `requireDefaultVariantOptIn` confirms a `default` variant,
441
+ * `validateGuardShapes` validates each variant's shape the way a top-level
442
+ * `shape` is validated. Enabling either does not enable the other.
443
+ */
444
+ function validateVariantMap(
445
+ variants: unknown,
446
+ location: string,
447
+ flags: { validateGuardShapes: boolean; requireDefaultVariantOptIn: boolean },
448
+ allowDefaultVariant?: unknown,
449
+ ): void {
326
450
  if (!isPlainObject(variants)) {
327
451
  throw new Error(location + ': variants must be a non-array object')
328
452
  }
@@ -343,18 +467,20 @@ export function validateOperationConfig(
343
467
  * writes on purpose; a key called `default` is one they may have typed without
344
468
  * noticing what it catches.
345
469
  */
346
- if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
347
- if (config.allowDefaultVariant !== true) {
470
+ if (flags.requireDefaultVariantOptIn) {
471
+ if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
472
+ if (allowDefaultVariant !== true) {
473
+ throw new Error(
474
+ location + ': a variant named "default" answers every unrecognised, ' +
475
+ 'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
476
+ 'that is intended, and make sure it is the most restrictive variant.',
477
+ )
478
+ }
479
+ } else if (allowDefaultVariant !== undefined) {
348
480
  throw new Error(
349
- location + ': a variant named "default" answers every unrecognised, ' +
350
- 'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
351
- 'that is intended, and make sure it is the most restrictive variant.',
481
+ location + ': allowDefaultVariant is set but no "default" variant exists.',
352
482
  )
353
483
  }
354
- } else if (config.allowDefaultVariant !== undefined) {
355
- throw new Error(
356
- location + ': allowDefaultVariant is set but no "default" variant exists.',
357
- )
358
484
  }
359
485
 
360
486
  for (const [key, rawEntry] of entries) {
@@ -384,7 +510,9 @@ export function validateOperationConfig(
384
510
  * different key. A guard is not more trustworthy for being written inside a
385
511
  * variant.
386
512
  */
387
- validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
513
+ if (flags.validateGuardShapes) {
514
+ validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
515
+ }
388
516
  }
389
517
  }
390
518
 
@@ -1,6 +1,8 @@
1
1
  import { GeneratorOptions } from '@prisma/generator-helper'
2
2
 
3
- export function generateQueryBuilderHelper(options: GeneratorOptions): string {
3
+ export function generateQueryBuilderHelper(
4
+ options: Pick<GeneratorOptions, 'schemaPath'>,
5
+ ): string {
4
6
  const schemaPath = options.schemaPath
5
7
  ? JSON.stringify(options.schemaPath)
6
8
  : "resolve(process.cwd(), 'prisma/schema.prisma')"
@@ -52,7 +54,7 @@ export function startQueryBuilder(options: QueryBuilderOptions = {}): void {
52
54
 
53
55
  function doStart(options: QueryBuilderOptions): Promise<void> {
54
56
  return new Promise<void>((resolvePromise) => {
55
- const env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
57
+ const env = process.env
56
58
 
57
59
  if (env.NODE_ENV === 'production') {
58
60
  resolvePromise()
@@ -97,7 +99,7 @@ function doStart(options: QueryBuilderOptions): Promise<void> {
97
99
 
98
100
  const schemaCwd = dirname(resolve(schemaPath))
99
101
 
100
- _process = spawn(process.execPath, [cliPath], {
102
+ const child = spawn(process.execPath, [cliPath], {
101
103
  stdio: 'inherit',
102
104
  env: {
103
105
  ...env,
@@ -110,14 +112,15 @@ function doStart(options: QueryBuilderOptions): Promise<void> {
110
112
  DATABASE_URL: databaseUrl,
111
113
  },
112
114
  })
115
+ _process = child
113
116
 
114
- _process.on('error', (err) => {
117
+ child.on('error', (err) => {
115
118
  console.error('[query-builder] Failed to start:', err.message)
116
119
  _process = null
117
120
  resolvePromise()
118
121
  })
119
122
 
120
- _process.on('exit', (code) => {
123
+ child.on('exit', (code) => {
121
124
  const wasStopping = _stopping
122
125
  _stopping = false
123
126
  _process = null
@@ -160,4 +163,4 @@ export function stopQueryBuilder(): void {
160
163
  _process.kill()
161
164
  }
162
165
  `
163
- }
166
+ }
@@ -117,10 +117,15 @@ export function generateRouteConfigType(
117
117
  ` after?: ${afterRef}[]\n` +
118
118
  ` }>`
119
119
 
120
+ // Only consulted when `requireDefaultVariantOptIn` is on; inert otherwise.
121
+ // Emitted so a TypeScript consumer who opts in can confirm a `default`
122
+ // variant without a cast — 1.64.2 demanded it and never added it to the type.
123
+ const allowDefault = ` allowDefaultVariant?: boolean`
124
+
120
125
  return (
121
126
  ` ${routerOp}?: (${commonConfig} & (\n` +
122
127
  ` | { shape?: ${m}${c}ShapeInput<TCtx>; variants?: never }\n` +
123
- ` | { shape?: never; variants: ${variantsConfig} }\n` +
128
+ ` | { shape?: never; variants: ${variantsConfig}\n${allowDefault} }\n` +
124
129
  ` )) | false`
125
130
  )
126
131
  }).join('\n')