prisma-generator-express 1.59.0 → 1.61.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.
Files changed (67) hide show
  1. package/README.md +150 -14
  2. package/dist/client/encodeQueryParams.js +19 -23
  3. package/dist/client/encodeQueryParams.js.map +1 -1
  4. package/dist/copy/operationDefinitions.d.ts +37 -0
  5. package/dist/copy/operationDefinitions.js +412 -0
  6. package/dist/copy/operationDefinitions.js.map +1 -0
  7. package/dist/generators/generateFastifyHandler.js +10 -41
  8. package/dist/generators/generateFastifyHandler.js.map +1 -1
  9. package/dist/generators/generateHonoHandler.js +10 -41
  10. package/dist/generators/generateHonoHandler.js.map +1 -1
  11. package/dist/generators/generateModelMetadata.d.ts +8 -0
  12. package/dist/generators/generateModelMetadata.js +77 -0
  13. package/dist/generators/generateModelMetadata.js.map +1 -0
  14. package/dist/generators/generateOperationCore.js +20 -30
  15. package/dist/generators/generateOperationCore.js.map +1 -1
  16. package/dist/generators/generateQueryBuilderHelper.js +1 -1
  17. package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
  18. package/dist/generators/generateRouteConfigType.js +11 -57
  19. package/dist/generators/generateRouteConfigType.js.map +1 -1
  20. package/dist/generators/generateRouter.js +64 -177
  21. package/dist/generators/generateRouter.js.map +1 -1
  22. package/dist/generators/generateRouterFastify.js +59 -169
  23. package/dist/generators/generateRouterFastify.js.map +1 -1
  24. package/dist/generators/generateRouterHono.js +52 -152
  25. package/dist/generators/generateRouterHono.js.map +1 -1
  26. package/dist/generators/generateUnifiedHandler.js +7 -30
  27. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  28. package/dist/generators/generateUnifiedScalarUI.js +9 -74
  29. package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
  30. package/dist/index.js +11 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/utils/copyFiles.js +7 -0
  33. package/dist/utils/copyFiles.js.map +1 -1
  34. package/dist/utils/writeFileSafely.js +5 -12
  35. package/dist/utils/writeFileSafely.js.map +1 -1
  36. package/package.json +12 -2
  37. package/src/client/encodeQueryParams.ts +23 -36
  38. package/src/copy/autoIncludePlanner.ts +14 -23
  39. package/src/copy/autoIncludePlannerGuarded.ts +477 -0
  40. package/src/copy/autoIncludeRuntime.ts +269 -261
  41. package/src/copy/autoIncludeRuntimeGuarded.ts +379 -0
  42. package/src/copy/buildModelOpenApi.ts +248 -628
  43. package/src/copy/concurrency.ts +20 -0
  44. package/src/copy/docsRenderer.ts +63 -333
  45. package/src/copy/errorMapper.ts +126 -0
  46. package/src/copy/guardHelpers.ts +56 -0
  47. package/src/copy/materializedCount.ts +68 -0
  48. package/src/copy/materializedRouter.ts +33 -29
  49. package/src/copy/operationDefinitions.ts +359 -35
  50. package/src/copy/operationRuntime.ts +11 -605
  51. package/src/copy/pagination.ts +151 -0
  52. package/src/copy/scalarTypes.ts +2 -0
  53. package/src/copy/sse.ts +296 -0
  54. package/src/generators/generateFastifyHandler.ts +13 -47
  55. package/src/generators/generateHonoHandler.ts +13 -47
  56. package/src/generators/generateModelMetadata.ts +92 -0
  57. package/src/generators/generateOperationCore.ts +19 -32
  58. package/src/generators/generateQueryBuilderHelper.ts +1 -1
  59. package/src/generators/generateRouteConfigType.ts +9 -60
  60. package/src/generators/generateRouter.ts +88 -180
  61. package/src/generators/generateRouterFastify.ts +65 -172
  62. package/src/generators/generateRouterHono.ts +58 -155
  63. package/src/generators/generateUnifiedHandler.ts +8 -33
  64. package/src/generators/generateUnifiedScalarUI.ts +9 -91
  65. package/src/index.ts +13 -1
  66. package/src/utils/copyFiles.ts +7 -0
  67. package/src/utils/writeFileSafely.ts +5 -11
@@ -0,0 +1,477 @@
1
+ import { isPlainObject } from './misc'
2
+ import type {
3
+ ModelRelationField,
4
+ ModelRelationMap,
5
+ } from './autoIncludePlanner'
6
+
7
+ export type GuardedAutoIncludeStage = {
8
+ relationPath: string
9
+ parentPath: string
10
+ relationName: string
11
+ relationField: ModelRelationField
12
+ stageArgs: Record<string, unknown>
13
+ stageShape: Record<string, unknown>
14
+ depth: number
15
+ }
16
+
17
+ export type GuardedAutoIncludePlan = {
18
+ rootArgs: Record<string, unknown>
19
+ rootShape: Record<string, unknown>
20
+ stages: GuardedAutoIncludeStage[]
21
+ internalFieldPaths: string[]
22
+ unsupportedReason?: string
23
+ }
24
+
25
+ export type GuardedAutoIncludePlannerInput = {
26
+ rootModelName: string
27
+ models: Record<string, ModelRelationMap>
28
+ effectiveReadBody: Record<string, unknown>
29
+ shape: Record<string, unknown>
30
+ maxDepth?: number
31
+ maxStages?: number
32
+ }
33
+
34
+ export const DEFAULT_GUARDED_MAX_DEPTH = 3
35
+ export const DEFAULT_GUARDED_MAX_STAGES = 20
36
+
37
+ const ALLOWED_TO_ONE_STAGE_ARGS = new Set(['select', 'include', 'omit'])
38
+ const ALLOWED_TO_MANY_STAGE_ARGS = new Set([
39
+ 'select', 'include', 'omit', 'where', 'orderBy', 'take', 'skip', 'cursor', 'distinct',
40
+ ])
41
+
42
+ function containsCountProjection(value: unknown): boolean {
43
+ if (!isPlainObject(value)) return false
44
+ if ('_count' in value) return true
45
+ for (const child of Object.values(value)) {
46
+ if (containsCountProjection(child)) return true
47
+ }
48
+ return false
49
+ }
50
+
51
+ function projectionHasCount(node: Record<string, unknown>): boolean {
52
+ return containsCountProjection(node.select) || containsCountProjection(node.include)
53
+ }
54
+
55
+ function rootHasRelationRefInFilter(
56
+ args: Record<string, unknown>,
57
+ model: ModelRelationMap,
58
+ ): boolean {
59
+ const walkWhere = (obj: unknown): boolean => {
60
+ if (!isPlainObject(obj)) return false
61
+ for (const key of Object.keys(obj)) {
62
+ if (model.relations[key]) return true
63
+ if (key === 'AND' || key === 'OR' || key === 'NOT') {
64
+ const sub = obj[key]
65
+ if (Array.isArray(sub)) {
66
+ for (const item of sub) if (walkWhere(item)) return true
67
+ } else if (walkWhere(sub)) {
68
+ return true
69
+ }
70
+ }
71
+ }
72
+ return false
73
+ }
74
+
75
+ const orderByHas = (v: unknown): boolean => {
76
+ const list = Array.isArray(v) ? v : [v]
77
+ for (const ob of list) {
78
+ if (!isPlainObject(ob)) continue
79
+ for (const key of Object.keys(ob)) if (model.relations[key]) return true
80
+ }
81
+ return false
82
+ }
83
+
84
+ if (walkWhere(args.where)) return true
85
+ if (args.orderBy && orderByHas(args.orderBy)) return true
86
+ if (isPlainObject(args.cursor)) {
87
+ for (const key of Object.keys(args.cursor)) {
88
+ if (model.relations[key]) return true
89
+ }
90
+ }
91
+ return false
92
+ }
93
+
94
+ function stageHasBlockedRelationRef(
95
+ args: Record<string, unknown>,
96
+ model: ModelRelationMap,
97
+ ): boolean {
98
+ const orderByHas = (v: unknown): boolean => {
99
+ const list = Array.isArray(v) ? v : [v]
100
+ for (const ob of list) {
101
+ if (!isPlainObject(ob)) continue
102
+ for (const key of Object.keys(ob)) if (model.relations[key]) return true
103
+ }
104
+ return false
105
+ }
106
+
107
+ if (args.orderBy && orderByHas(args.orderBy)) return true
108
+ if (isPlainObject(args.cursor)) {
109
+ for (const key of Object.keys(args.cursor)) {
110
+ if (model.relations[key]) return true
111
+ }
112
+ }
113
+ return false
114
+ }
115
+
116
+ function whereMentionsField(where: unknown, fieldName: string): boolean {
117
+ if (!isPlainObject(where)) return false
118
+ for (const key of Object.keys(where)) {
119
+ if (key === fieldName) return true
120
+ if (key === 'AND' || key === 'OR' || key === 'NOT') {
121
+ const sub = where[key]
122
+ if (Array.isArray(sub)) {
123
+ for (const item of sub) if (whereMentionsField(item, fieldName)) return true
124
+ } else if (whereMentionsField(sub, fieldName)) {
125
+ return true
126
+ }
127
+ }
128
+ }
129
+ return false
130
+ }
131
+
132
+ function rejectIncludeOrOmit(node: Record<string, unknown>, label: string): string | null {
133
+ if (isPlainObject(node.include)) {
134
+ return label + ' uses include (guarded MVP supports select only)'
135
+ }
136
+ if (isPlainObject(node.omit)) {
137
+ return label + ' uses omit (guarded MVP supports select only)'
138
+ }
139
+ return null
140
+ }
141
+
142
+ type WalkContext = {
143
+ models: Record<string, ModelRelationMap>
144
+ stages: GuardedAutoIncludeStage[]
145
+ internalFieldPaths: string[]
146
+ maxDepth: number
147
+ maxStages: number
148
+ }
149
+
150
+ type WalkResult = {
151
+ unsupportedReason?: string
152
+ bodyProjectionAfterStrip?: Record<string, unknown>
153
+ shapeProjectionAfterStrip?: Record<string, unknown>
154
+ }
155
+
156
+ function isRejectableWhere(value: unknown): boolean {
157
+ return value !== undefined && !isPlainObject(value)
158
+ }
159
+
160
+ function walkRoot(
161
+ ctx: WalkContext,
162
+ modelName: string,
163
+ parentBody: Record<string, unknown>,
164
+ parentShape: Record<string, unknown>,
165
+ ): WalkResult {
166
+ if (ctx.stages.length >= ctx.maxStages) {
167
+ return { unsupportedReason: 'stages reached maxStages=' + ctx.maxStages }
168
+ }
169
+
170
+ const model = ctx.models[modelName]
171
+ if (!model) {
172
+ return { unsupportedReason: 'model ' + modelName + ' not in relation metadata' }
173
+ }
174
+
175
+ const bodyIncludeOmit = rejectIncludeOrOmit(parentBody, 'root body')
176
+ if (bodyIncludeOmit) return { unsupportedReason: bodyIncludeOmit }
177
+ const shapeIncludeOmit = rejectIncludeOrOmit(parentShape, 'root shape')
178
+ if (shapeIncludeOmit) return { unsupportedReason: shapeIncludeOmit }
179
+
180
+ if (projectionHasCount(parentBody)) {
181
+ return { unsupportedReason: '_count in root body not supported in guarded MVP' }
182
+ }
183
+ if (projectionHasCount(parentShape)) {
184
+ return { unsupportedReason: '_count in root shape not supported in guarded MVP' }
185
+ }
186
+
187
+ if (isRejectableWhere(parentBody.where)) {
188
+ return { unsupportedReason: 'root body where must be a plain object' }
189
+ }
190
+ if (isRejectableWhere(parentShape.where)) {
191
+ return { unsupportedReason: 'root shape where must be a plain object' }
192
+ }
193
+
194
+ if (rootHasRelationRefInFilter(parentBody, model)) {
195
+ return { unsupportedReason: 'root body where/orderBy/cursor relation ref not supported' }
196
+ }
197
+ if (rootHasRelationRefInFilter(parentShape, model)) {
198
+ return { unsupportedReason: 'root shape where/orderBy/cursor relation ref not supported' }
199
+ }
200
+
201
+ const bodySelect = parentBody.select
202
+ const shapeSelect = parentShape.select
203
+
204
+ const bodyProjection = isPlainObject(bodySelect) ? bodySelect : null
205
+ const shapeProjection = isPlainObject(shapeSelect) ? shapeSelect : null
206
+
207
+ if (!bodyProjection || !shapeProjection) {
208
+ return {}
209
+ }
210
+
211
+ const updatedBodyProjection: Record<string, unknown> = {}
212
+ const updatedShapeProjection: Record<string, unknown> = {}
213
+
214
+ const relationBranches: Array<{
215
+ name: string
216
+ bodyValue: unknown
217
+ shapeValue: unknown
218
+ }> = []
219
+
220
+ for (const [key, value] of Object.entries(bodyProjection)) {
221
+ if (model.relations[key]) {
222
+ if (value === false || value === null || value === undefined) {
223
+ updatedBodyProjection[key] = value
224
+ if (key in shapeProjection) {
225
+ updatedShapeProjection[key] = shapeProjection[key]
226
+ }
227
+ continue
228
+ }
229
+ const shapeBranch = shapeProjection[key]
230
+ if (shapeBranch === undefined) {
231
+ return {
232
+ unsupportedReason: 'body projects relation "' + key + '" not present in guard shape at root',
233
+ }
234
+ }
235
+ relationBranches.push({ name: key, bodyValue: value, shapeValue: shapeBranch })
236
+ } else {
237
+ updatedBodyProjection[key] = value
238
+ if (key in shapeProjection) {
239
+ updatedShapeProjection[key] = shapeProjection[key]
240
+ }
241
+ }
242
+ }
243
+
244
+ for (const branch of relationBranches) {
245
+ if (ctx.stages.length >= ctx.maxStages) {
246
+ return { unsupportedReason: 'stages reached maxStages=' + ctx.maxStages }
247
+ }
248
+
249
+ const relation = model.relations[branch.name]
250
+
251
+ if (relation.direction === 'implicitM2M') {
252
+ return { unsupportedReason: 'implicit many-to-many not supported in guarded MVP' }
253
+ }
254
+ if (relation.parentLinkFields.length === 0 || relation.childLinkFields.length === 0) {
255
+ return { unsupportedReason: 'ambiguous relation metadata for ' + relation.name }
256
+ }
257
+ if (relation.parentLinkFields.length !== relation.childLinkFields.length) {
258
+ return { unsupportedReason: 'mismatched link field counts for ' + relation.name }
259
+ }
260
+ if (relation.parentLinkFields.length !== 1) {
261
+ return {
262
+ unsupportedReason: 'composite link fields not supported for guarded stage ' + relation.name,
263
+ }
264
+ }
265
+ if (!ctx.models[relation.type]) {
266
+ return {
267
+ unsupportedReason: 'target model ' + relation.type + ' not in relation metadata for ' + branch.name,
268
+ }
269
+ }
270
+
271
+ if (branch.bodyValue !== true && !isPlainObject(branch.bodyValue)) {
272
+ return { unsupportedReason: 'invalid relation projection body for ' + branch.name }
273
+ }
274
+ if (branch.shapeValue !== true && !isPlainObject(branch.shapeValue)) {
275
+ return { unsupportedReason: 'invalid relation projection shape for ' + branch.name }
276
+ }
277
+
278
+ const parentKey = relation.parentLinkFields[0]
279
+ const childKey = relation.childLinkFields[0]
280
+
281
+ if (updatedBodyProjection[parentKey] !== true) {
282
+ updatedBodyProjection[parentKey] = true
283
+ ctx.internalFieldPaths.push(parentKey)
284
+ }
285
+ if (updatedShapeProjection[parentKey] !== true) {
286
+ updatedShapeProjection[parentKey] = true
287
+ }
288
+
289
+ const relationBodyArgs: Record<string, unknown> = branch.bodyValue === true ? {} : branch.bodyValue
290
+ const relationShapeArgs: Record<string, unknown> = branch.shapeValue === true ? {} : branch.shapeValue
291
+
292
+ const allowedArgs = relation.isList ? ALLOWED_TO_MANY_STAGE_ARGS : ALLOWED_TO_ONE_STAGE_ARGS
293
+ for (const key of Object.keys(relationBodyArgs)) {
294
+ if (!allowedArgs.has(key)) {
295
+ return {
296
+ unsupportedReason: 'unsupported body arg "' + key + '" for ' +
297
+ (relation.isList ? 'to-many' : 'to-one') + ' relation ' + relation.name,
298
+ }
299
+ }
300
+ }
301
+ for (const key of Object.keys(relationShapeArgs)) {
302
+ if (!allowedArgs.has(key)) {
303
+ return {
304
+ unsupportedReason: 'unsupported shape arg "' + key + '" for ' +
305
+ (relation.isList ? 'to-many' : 'to-one') + ' relation ' + relation.name,
306
+ }
307
+ }
308
+ }
309
+
310
+ if (isPlainObject(relationBodyArgs.include)) {
311
+ return { unsupportedReason: 'stage body uses include for ' + relation.name + ' (guarded MVP supports select only)' }
312
+ }
313
+ if (isPlainObject(relationBodyArgs.omit)) {
314
+ return { unsupportedReason: 'stage body uses omit for ' + relation.name + ' (guarded MVP supports select only)' }
315
+ }
316
+ if (isPlainObject(relationShapeArgs.include)) {
317
+ return { unsupportedReason: 'stage shape uses include for ' + relation.name + ' (guarded MVP supports select only)' }
318
+ }
319
+ if (isPlainObject(relationShapeArgs.omit)) {
320
+ return { unsupportedReason: 'stage shape uses omit for ' + relation.name + ' (guarded MVP supports select only)' }
321
+ }
322
+
323
+ if (projectionHasCount(relationBodyArgs)) {
324
+ return { unsupportedReason: '_count in stage body for ' + relation.name + ' not supported in guarded MVP' }
325
+ }
326
+ if (projectionHasCount(relationShapeArgs)) {
327
+ return { unsupportedReason: '_count in stage shape for ' + relation.name + ' not supported in guarded MVP' }
328
+ }
329
+
330
+ if (isRejectableWhere(relationBodyArgs.where)) {
331
+ return { unsupportedReason: 'stage body where must be a plain object for ' + relation.name }
332
+ }
333
+ if (isRejectableWhere(relationShapeArgs.where)) {
334
+ return { unsupportedReason: 'stage shape where must be a plain object for ' + relation.name }
335
+ }
336
+
337
+ if (stageHasBlockedRelationRef(relationBodyArgs, ctx.models[relation.type])) {
338
+ return { unsupportedReason: 'stage body orderBy/cursor relation ref not supported for ' + relation.name }
339
+ }
340
+ if (stageHasBlockedRelationRef(relationShapeArgs, ctx.models[relation.type])) {
341
+ return { unsupportedReason: 'stage shape orderBy/cursor relation ref not supported for ' + relation.name }
342
+ }
343
+
344
+ if (whereMentionsField(relationBodyArgs.where, childKey)) {
345
+ return {
346
+ unsupportedReason: 'FK collision: stage body where for ' + relation.name +
347
+ ' already mentions child link field "' + childKey + '"',
348
+ }
349
+ }
350
+ if (whereMentionsField(relationShapeArgs.where, childKey)) {
351
+ return {
352
+ unsupportedReason: 'FK collision: stage shape where for ' + relation.name +
353
+ ' already mentions child link field "' + childKey + '"',
354
+ }
355
+ }
356
+
357
+ const stageArgs: Record<string, unknown> = { ...relationBodyArgs }
358
+ const stageShape: Record<string, unknown> = { ...relationShapeArgs }
359
+
360
+ injectChildFkIntoStageProjection(
361
+ stageArgs,
362
+ stageShape,
363
+ childKey,
364
+ branch.name,
365
+ ctx.internalFieldPaths,
366
+ )
367
+
368
+ injectChildFkPermissionIntoStageShape(stageShape, childKey)
369
+
370
+ ctx.stages.push({
371
+ relationPath: branch.name,
372
+ parentPath: '',
373
+ relationName: branch.name,
374
+ relationField: relation,
375
+ stageArgs,
376
+ stageShape,
377
+ depth: 1,
378
+ })
379
+ }
380
+
381
+ return {
382
+ bodyProjectionAfterStrip: updatedBodyProjection,
383
+ shapeProjectionAfterStrip: updatedShapeProjection,
384
+ }
385
+ }
386
+
387
+ function injectChildFkIntoStageProjection(
388
+ stageArgs: Record<string, unknown>,
389
+ stageShape: Record<string, unknown>,
390
+ childKey: string,
391
+ relationPath: string,
392
+ internalFieldPaths: string[],
393
+ ): void {
394
+ const bodySelect = stageArgs.select
395
+ const shapeSelect = stageShape.select
396
+
397
+ if (isPlainObject(bodySelect)) {
398
+ if ((bodySelect as Record<string, unknown>)[childKey] !== true) {
399
+ const next: Record<string, unknown> = { ...bodySelect, [childKey]: true }
400
+ stageArgs.select = next
401
+ internalFieldPaths.push(relationPath + '.' + childKey)
402
+ }
403
+ }
404
+
405
+ if (isPlainObject(shapeSelect)) {
406
+ if ((shapeSelect as Record<string, unknown>)[childKey] !== true) {
407
+ const next: Record<string, unknown> = { ...shapeSelect, [childKey]: true }
408
+ stageShape.select = next
409
+ }
410
+ }
411
+ }
412
+
413
+ function injectChildFkPermissionIntoStageShape(
414
+ stageShape: Record<string, unknown>,
415
+ childKey: string,
416
+ ): void {
417
+ const existingWhere = stageShape.where
418
+ const nextWhere: Record<string, unknown> = isPlainObject(existingWhere)
419
+ ? { ...existingWhere }
420
+ : {}
421
+ nextWhere[childKey] = { in: true }
422
+ stageShape.where = nextWhere
423
+ }
424
+
425
+ export function planGuardedAutoInclude(
426
+ input: GuardedAutoIncludePlannerInput,
427
+ ): GuardedAutoIncludePlan {
428
+ const maxDepth = input.maxDepth ?? DEFAULT_GUARDED_MAX_DEPTH
429
+ const maxStages = input.maxStages ?? DEFAULT_GUARDED_MAX_STAGES
430
+
431
+ const ctx: WalkContext = {
432
+ models: input.models,
433
+ stages: [],
434
+ internalFieldPaths: [],
435
+ maxDepth,
436
+ maxStages,
437
+ }
438
+
439
+ const result = walkRoot(
440
+ ctx,
441
+ input.rootModelName,
442
+ input.effectiveReadBody,
443
+ input.shape,
444
+ )
445
+
446
+ if (result.unsupportedReason) {
447
+ return {
448
+ rootArgs: input.effectiveReadBody,
449
+ rootShape: input.shape,
450
+ stages: [],
451
+ internalFieldPaths: [],
452
+ unsupportedReason: 'guarded auto-progressive fallback: ' + result.unsupportedReason,
453
+ }
454
+ }
455
+
456
+ const rootArgs: Record<string, unknown> = { ...input.effectiveReadBody }
457
+ const rootShape: Record<string, unknown> = { ...input.shape }
458
+
459
+ if (result.bodyProjectionAfterStrip) {
460
+ rootArgs.select = result.bodyProjectionAfterStrip
461
+ delete rootArgs.include
462
+ delete rootArgs.omit
463
+ }
464
+
465
+ if (result.shapeProjectionAfterStrip) {
466
+ rootShape.select = result.shapeProjectionAfterStrip
467
+ delete rootShape.include
468
+ delete rootShape.omit
469
+ }
470
+
471
+ return {
472
+ rootArgs,
473
+ rootShape,
474
+ stages: ctx.stages,
475
+ internalFieldPaths: ctx.internalFieldPaths,
476
+ }
477
+ }