prisma-generator-express 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.
Files changed (49) hide show
  1. package/README.md +244 -14
  2. package/dist/constants.d.ts +1 -0
  3. package/dist/generators/generateFastifyHandler.d.ts +4 -0
  4. package/dist/generators/generateFastifyHandler.js +78 -0
  5. package/dist/generators/generateFastifyHandler.js.map +1 -0
  6. package/dist/generators/generateOperationCore.d.ts +6 -0
  7. package/dist/generators/generateOperationCore.js +534 -0
  8. package/dist/generators/generateOperationCore.js.map +1 -0
  9. package/dist/generators/generateQueryBuilderHelper.js +85 -69
  10. package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
  11. package/dist/generators/generateRouter.js +1 -25
  12. package/dist/generators/generateRouter.js.map +1 -1
  13. package/dist/generators/generateRouterFastify.d.ts +5 -0
  14. package/dist/generators/generateRouterFastify.js +512 -0
  15. package/dist/generators/generateRouterFastify.js.map +1 -0
  16. package/dist/generators/generateUnifiedDocs.d.ts +2 -1
  17. package/dist/generators/generateUnifiedDocs.js +147 -82
  18. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  19. package/dist/generators/generateUnifiedHandler.d.ts +0 -1
  20. package/dist/generators/generateUnifiedHandler.js +47 -516
  21. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  22. package/dist/generators/generateUnifiedScalarUI.d.ts +2 -0
  23. package/dist/generators/generateUnifiedScalarUI.js +127 -1324
  24. package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
  25. package/dist/index.js +33 -8
  26. package/dist/index.js.map +1 -1
  27. package/dist/utils/copyFiles.d.ts +2 -1
  28. package/dist/utils/copyFiles.js +64 -38
  29. package/dist/utils/copyFiles.js.map +1 -1
  30. package/dist/utils/writeFileSafely.js +3 -0
  31. package/dist/utils/writeFileSafely.js.map +1 -1
  32. package/package.json +4 -1
  33. package/src/client/encodeQueryParams.ts +1 -1
  34. package/src/constants.ts +2 -0
  35. package/src/copy/createOutputValidatorMiddleware.ts +9 -12
  36. package/src/copy/docsRenderer.ts +1285 -0
  37. package/src/copy/parseQueryParams.ts +4 -8
  38. package/src/copy/routeConfig.ts +10 -4
  39. package/src/generators/generateFastifyHandler.ts +86 -0
  40. package/src/generators/generateOperationCore.ts +545 -0
  41. package/src/generators/generateQueryBuilderHelper.ts +86 -70
  42. package/src/generators/generateRouter.ts +1 -25
  43. package/src/generators/generateRouterFastify.ts +522 -0
  44. package/src/generators/generateUnifiedDocs.ts +164 -81
  45. package/src/generators/generateUnifiedHandler.ts +45 -533
  46. package/src/generators/generateUnifiedScalarUI.ts +134 -1323
  47. package/src/index.ts +45 -9
  48. package/src/utils/copyFiles.ts +79 -45
  49. package/src/utils/writeFileSafely.ts +4 -0
@@ -0,0 +1,522 @@
1
+ import { DMMF } from '@prisma/generator-helper'
2
+
3
+ export function generateFastifyRouterFunction({
4
+ model,
5
+ enums,
6
+ }: {
7
+ model: DMMF.Model
8
+ enums: DMMF.DatamodelEnum[]
9
+ }): string {
10
+ const modelName = model.name
11
+ const modelNameLower = modelName.toLowerCase()
12
+ const routerFunctionName = `${modelName}Routes`
13
+
14
+ const fieldsMeta = model.fields.map((f) => ({
15
+ name: f.name,
16
+ kind: f.kind,
17
+ type: f.type,
18
+ isList: f.isList,
19
+ isRequired: f.isRequired,
20
+ hasDefaultValue: f.hasDefaultValue,
21
+ isUpdatedAt: f.isUpdatedAt ?? false,
22
+ documentation: f.documentation,
23
+ relationFromFields: f.relationFromFields,
24
+ }))
25
+
26
+ const referencedEnumTypes = new Set(
27
+ model.fields.filter((f) => f.kind === 'enum').map((f) => f.type),
28
+ )
29
+
30
+ const enumsMeta = enums
31
+ .filter((e) => referencedEnumTypes.has(e.name))
32
+ .map((e) => ({
33
+ name: e.name,
34
+ values: e.values.map((v) => ({ name: v.name })),
35
+ }))
36
+
37
+ return `import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
38
+ import {
39
+ ${modelName}FindUnique,
40
+ ${modelName}FindUniqueOrThrow,
41
+ ${modelName}FindFirst,
42
+ ${modelName}FindFirstOrThrow,
43
+ ${modelName}FindMany,
44
+ ${modelName}FindManyPaginated,
45
+ ${modelName}Create,
46
+ ${modelName}CreateMany,
47
+ ${modelName}CreateManyAndReturn,
48
+ ${modelName}Update,
49
+ ${modelName}UpdateMany,
50
+ ${modelName}UpdateManyAndReturn,
51
+ ${modelName}Upsert,
52
+ ${modelName}Delete,
53
+ ${modelName}DeleteMany,
54
+ ${modelName}Aggregate,
55
+ ${modelName}Count,
56
+ ${modelName}GroupBy,
57
+ } from './${modelName}Handlers.js'
58
+ import type { RouteConfig, FastifyHookHandler } from '../routeConfig.js'
59
+ import { parseQueryParams } from '../parseQueryParams.js'
60
+ import { buildModelOpenApi } from '../buildModelOpenApi.js'
61
+ import { mapError, transformResult } from '../operationRuntime.js'
62
+
63
+ const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
64
+
65
+ const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
66
+
67
+ const MODEL_ENUMS = ${JSON.stringify(enumsMeta, null, 2)} as const
68
+
69
+ const defaultOpConfig = {
70
+ before: [] as FastifyHookHandler[],
71
+ after: [] as FastifyHookHandler[],
72
+ }
73
+
74
+ function normalizePrefix(p: string): string {
75
+ if (!p) return ''
76
+ let result = p
77
+ if (!result.startsWith('/')) result = '/' + result
78
+ while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
79
+ if (result === '/') return ''
80
+ return result
81
+ }
82
+
83
+ function isQueryBuilderEnabled(config: RouteConfig): boolean {
84
+ if (config.queryBuilder === false) return false
85
+ if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
86
+ if (_env.NODE_ENV === 'production') return false
87
+ return true
88
+ }
89
+
90
+ function getQueryBuilderConfig(config: RouteConfig) {
91
+ if (config.queryBuilder === false) return null
92
+ if (typeof config.queryBuilder === 'object') return config.queryBuilder
93
+ return {}
94
+ }
95
+
96
+ function parseQueryHook(request: FastifyRequest): void {
97
+ const raw = request.query as Record<string, unknown>
98
+ if (raw && Object.keys(raw).length > 0) {
99
+ ;(request as any).parsedQuery = parseQueryParams(raw)
100
+ }
101
+ }
102
+
103
+ function makeShapeHook(config: RouteConfig, opConfig: any): (request: FastifyRequest) => void {
104
+ return (request: FastifyRequest) => {
105
+ ;(request as any).routeConfig = config
106
+ if (opConfig.shape) {
107
+ ;(request as any).guardShape = opConfig.shape
108
+ const headerName = config.guard?.variantHeader || 'x-api-variant'
109
+ const headerValue = request.headers[headerName]
110
+ const caller = config.guard?.resolveVariant?.(request)
111
+ ?? (Array.isArray(headerValue) ? headerValue[0] : headerValue)
112
+ ?? undefined
113
+ if (caller) {
114
+ ;(request as any).guardCaller = caller
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ async function runHooks(
121
+ hooks: FastifyHookHandler[],
122
+ request: FastifyRequest,
123
+ reply: FastifyReply,
124
+ ): Promise<boolean> {
125
+ for (const hook of hooks) {
126
+ if (reply.sent) return true
127
+ await hook(request, reply)
128
+ }
129
+ return reply.sent
130
+ }
131
+
132
+ function sendResult(request: FastifyRequest, reply: FastifyReply): void {
133
+ const req = request as any
134
+ const data = req.resultData
135
+ const status = req.resultStatus ?? 200
136
+ if (data === undefined) {
137
+ reply.code(500).send({ message: 'No data set by handler' })
138
+ return
139
+ }
140
+ reply.code(status).send(transformResult(data))
141
+ }
142
+
143
+ function sendError(reply: FastifyReply, error: unknown): void {
144
+ const httpError = mapError(error)
145
+ reply.code(httpError.status).send({ message: httpError.message })
146
+ }
147
+
148
+ export async function ${routerFunctionName}(
149
+ fastify: FastifyInstance,
150
+ config: RouteConfig = {},
151
+ ) {
152
+ const customPrefix = normalizePrefix(config.customUrlPrefix || '')
153
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
154
+ const basePath = customPrefix + modelPrefix
155
+
156
+ const openApiDisabled = config.disableOpenApi === true
157
+ || (config.disableOpenApi !== false && (
158
+ _env.DISABLE_OPENAPI === 'true'
159
+ || _env.NODE_ENV === 'production'
160
+ ))
161
+
162
+ const qbEnabled = isQueryBuilderEnabled(config)
163
+
164
+ if (qbEnabled) {
165
+ const qbConfig = getQueryBuilderConfig(config)
166
+ if (qbConfig) {
167
+ try { require('../queryBuilder').startQueryBuilder(qbConfig) } catch (err) { if (_env.NODE_ENV !== 'production') console.warn('[query-builder]', err) }
168
+ }
169
+ }
170
+
171
+ fastify.setErrorHandler((error, _request, reply) => {
172
+ const status = (error as any).status ?? error.statusCode ?? 500
173
+ const message = error.message || 'Internal server error'
174
+ if (!reply.sent) {
175
+ reply.code(status).send({ message })
176
+ }
177
+ })
178
+
179
+ if (!openApiDisabled) {
180
+ const openapiJsonPath = basePath ? \`\${basePath}/openapi.json\` : '/openapi.json'
181
+ const openapiYamlPath = basePath ? \`\${basePath}/openapi.yaml\` : '/openapi.yaml'
182
+
183
+ fastify.get(openapiJsonPath, async (_request, reply) => {
184
+ const spec = buildModelOpenApi(
185
+ '${modelName}',
186
+ MODEL_FIELDS as any,
187
+ MODEL_ENUMS as any,
188
+ config,
189
+ { format: 'json' },
190
+ )
191
+ return reply.send(spec)
192
+ })
193
+
194
+ fastify.get(openapiYamlPath, async (_request, reply) => {
195
+ const spec = buildModelOpenApi(
196
+ '${modelName}',
197
+ MODEL_FIELDS as any,
198
+ MODEL_ENUMS as any,
199
+ config,
200
+ { format: 'yaml' },
201
+ )
202
+ return reply.type('application/yaml').send(spec as string)
203
+ })
204
+ }
205
+
206
+ if (config.enableAll || config.findFirst) {
207
+ const opConfig = config.findFirst || defaultOpConfig
208
+ const { before = [], after = [] } = opConfig
209
+ const path = basePath ? \`\${basePath}/first\` : '/first'
210
+ fastify.get(path, async (request, reply) => {
211
+ try {
212
+ parseQueryHook(request)
213
+ makeShapeHook(config, opConfig)(request)
214
+ if (await runHooks(before, request, reply)) return
215
+ await ${modelName}FindFirst(request, reply)
216
+ if (await runHooks(after, request, reply)) return
217
+ sendResult(request, reply)
218
+ } catch (error: unknown) {
219
+ sendError(reply, error)
220
+ }
221
+ })
222
+ }
223
+
224
+ if (config.enableAll || config.findFirstOrThrow) {
225
+ const opConfig = config.findFirstOrThrow || defaultOpConfig
226
+ const { before = [], after = [] } = opConfig
227
+ const path = basePath ? \`\${basePath}/first/strict\` : '/first/strict'
228
+ fastify.get(path, async (request, reply) => {
229
+ try {
230
+ parseQueryHook(request)
231
+ makeShapeHook(config, opConfig)(request)
232
+ if (await runHooks(before, request, reply)) return
233
+ await ${modelName}FindFirstOrThrow(request, reply)
234
+ if (await runHooks(after, request, reply)) return
235
+ sendResult(request, reply)
236
+ } catch (error: unknown) {
237
+ sendError(reply, error)
238
+ }
239
+ })
240
+ }
241
+
242
+ if (config.enableAll || config.findManyPaginated) {
243
+ const opConfig = config.findManyPaginated || defaultOpConfig
244
+ const { before = [], after = [] } = opConfig
245
+ const path = basePath ? \`\${basePath}/paginated\` : '/paginated'
246
+ fastify.get(path, async (request, reply) => {
247
+ try {
248
+ parseQueryHook(request)
249
+ makeShapeHook(config, opConfig)(request)
250
+ if (await runHooks(before, request, reply)) return
251
+ await ${modelName}FindManyPaginated(request, reply)
252
+ if (await runHooks(after, request, reply)) return
253
+ sendResult(request, reply)
254
+ } catch (error: unknown) {
255
+ sendError(reply, error)
256
+ }
257
+ })
258
+ }
259
+
260
+ if (config.enableAll || config.aggregate) {
261
+ const opConfig = config.aggregate || defaultOpConfig
262
+ const { before = [], after = [] } = opConfig
263
+ const path = basePath ? \`\${basePath}/aggregate\` : '/aggregate'
264
+ fastify.get(path, async (request, reply) => {
265
+ try {
266
+ parseQueryHook(request)
267
+ makeShapeHook(config, opConfig)(request)
268
+ if (await runHooks(before, request, reply)) return
269
+ await ${modelName}Aggregate(request, reply)
270
+ if (await runHooks(after, request, reply)) return
271
+ sendResult(request, reply)
272
+ } catch (error: unknown) {
273
+ sendError(reply, error)
274
+ }
275
+ })
276
+ }
277
+
278
+ if (config.enableAll || config.count) {
279
+ const opConfig = config.count || defaultOpConfig
280
+ const { before = [], after = [] } = opConfig
281
+ const path = basePath ? \`\${basePath}/count\` : '/count'
282
+ fastify.get(path, async (request, reply) => {
283
+ try {
284
+ parseQueryHook(request)
285
+ makeShapeHook(config, opConfig)(request)
286
+ if (await runHooks(before, request, reply)) return
287
+ await ${modelName}Count(request, reply)
288
+ if (await runHooks(after, request, reply)) return
289
+ sendResult(request, reply)
290
+ } catch (error: unknown) {
291
+ sendError(reply, error)
292
+ }
293
+ })
294
+ }
295
+
296
+ if (config.enableAll || config.groupBy) {
297
+ const opConfig = config.groupBy || defaultOpConfig
298
+ const { before = [], after = [] } = opConfig
299
+ const path = basePath ? \`\${basePath}/groupby\` : '/groupby'
300
+ fastify.get(path, async (request, reply) => {
301
+ try {
302
+ parseQueryHook(request)
303
+ makeShapeHook(config, opConfig)(request)
304
+ if (await runHooks(before, request, reply)) return
305
+ await ${modelName}GroupBy(request, reply)
306
+ if (await runHooks(after, request, reply)) return
307
+ sendResult(request, reply)
308
+ } catch (error: unknown) {
309
+ sendError(reply, error)
310
+ }
311
+ })
312
+ }
313
+
314
+ if (config.enableAll || config.findUniqueOrThrow) {
315
+ const opConfig = config.findUniqueOrThrow || defaultOpConfig
316
+ const { before = [], after = [] } = opConfig
317
+ const path = basePath ? \`\${basePath}/unique/strict\` : '/unique/strict'
318
+ fastify.get(path, async (request, reply) => {
319
+ try {
320
+ parseQueryHook(request)
321
+ makeShapeHook(config, opConfig)(request)
322
+ if (await runHooks(before, request, reply)) return
323
+ await ${modelName}FindUniqueOrThrow(request, reply)
324
+ if (await runHooks(after, request, reply)) return
325
+ sendResult(request, reply)
326
+ } catch (error: unknown) {
327
+ sendError(reply, error)
328
+ }
329
+ })
330
+ }
331
+
332
+ if (config.enableAll || config.findUnique) {
333
+ const opConfig = config.findUnique || defaultOpConfig
334
+ const { before = [], after = [] } = opConfig
335
+ const path = basePath ? \`\${basePath}/unique\` : '/unique'
336
+ fastify.get(path, async (request, reply) => {
337
+ try {
338
+ parseQueryHook(request)
339
+ makeShapeHook(config, opConfig)(request)
340
+ if (await runHooks(before, request, reply)) return
341
+ await ${modelName}FindUnique(request, reply)
342
+ if (await runHooks(after, request, reply)) return
343
+ sendResult(request, reply)
344
+ } catch (error: unknown) {
345
+ sendError(reply, error)
346
+ }
347
+ })
348
+ }
349
+
350
+ if (config.enableAll || config.findMany) {
351
+ const opConfig = config.findMany || defaultOpConfig
352
+ const { before = [], after = [] } = opConfig
353
+ const path = basePath || '/'
354
+ fastify.get(path, async (request, reply) => {
355
+ try {
356
+ parseQueryHook(request)
357
+ makeShapeHook(config, opConfig)(request)
358
+ if (await runHooks(before, request, reply)) return
359
+ await ${modelName}FindMany(request, reply)
360
+ if (await runHooks(after, request, reply)) return
361
+ sendResult(request, reply)
362
+ } catch (error: unknown) {
363
+ sendError(reply, error)
364
+ }
365
+ })
366
+ }
367
+
368
+ if (config.enableAll || config.createManyAndReturn) {
369
+ const opConfig = config.createManyAndReturn || defaultOpConfig
370
+ const { before = [], after = [] } = opConfig
371
+ const path = basePath ? \`\${basePath}/many/return\` : '/many/return'
372
+ fastify.post(path, async (request, reply) => {
373
+ try {
374
+ makeShapeHook(config, opConfig)(request)
375
+ if (await runHooks(before, request, reply)) return
376
+ await ${modelName}CreateManyAndReturn(request, reply)
377
+ if (await runHooks(after, request, reply)) return
378
+ sendResult(request, reply)
379
+ } catch (error: unknown) {
380
+ sendError(reply, error)
381
+ }
382
+ })
383
+ }
384
+
385
+ if (config.enableAll || config.createMany) {
386
+ const opConfig = config.createMany || defaultOpConfig
387
+ const { before = [], after = [] } = opConfig
388
+ const path = basePath ? \`\${basePath}/many\` : '/many'
389
+ fastify.post(path, async (request, reply) => {
390
+ try {
391
+ makeShapeHook(config, opConfig)(request)
392
+ if (await runHooks(before, request, reply)) return
393
+ await ${modelName}CreateMany(request, reply)
394
+ if (await runHooks(after, request, reply)) return
395
+ sendResult(request, reply)
396
+ } catch (error: unknown) {
397
+ sendError(reply, error)
398
+ }
399
+ })
400
+ }
401
+
402
+ if (config.enableAll || config.create) {
403
+ const opConfig = config.create || defaultOpConfig
404
+ const { before = [], after = [] } = opConfig
405
+ const path = basePath || '/'
406
+ fastify.post(path, async (request, reply) => {
407
+ try {
408
+ makeShapeHook(config, opConfig)(request)
409
+ if (await runHooks(before, request, reply)) return
410
+ await ${modelName}Create(request, reply)
411
+ if (await runHooks(after, request, reply)) return
412
+ sendResult(request, reply)
413
+ } catch (error: unknown) {
414
+ sendError(reply, error)
415
+ }
416
+ })
417
+ }
418
+
419
+ if (config.enableAll || config.updateManyAndReturn) {
420
+ const opConfig = config.updateManyAndReturn || defaultOpConfig
421
+ const { before = [], after = [] } = opConfig
422
+ const path = basePath ? \`\${basePath}/many/return\` : '/many/return'
423
+ fastify.put(path, async (request, reply) => {
424
+ try {
425
+ makeShapeHook(config, opConfig)(request)
426
+ if (await runHooks(before, request, reply)) return
427
+ await ${modelName}UpdateManyAndReturn(request, reply)
428
+ if (await runHooks(after, request, reply)) return
429
+ sendResult(request, reply)
430
+ } catch (error: unknown) {
431
+ sendError(reply, error)
432
+ }
433
+ })
434
+ }
435
+
436
+ if (config.enableAll || config.updateMany) {
437
+ const opConfig = config.updateMany || defaultOpConfig
438
+ const { before = [], after = [] } = opConfig
439
+ const path = basePath ? \`\${basePath}/many\` : '/many'
440
+ fastify.put(path, async (request, reply) => {
441
+ try {
442
+ makeShapeHook(config, opConfig)(request)
443
+ if (await runHooks(before, request, reply)) return
444
+ await ${modelName}UpdateMany(request, reply)
445
+ if (await runHooks(after, request, reply)) return
446
+ sendResult(request, reply)
447
+ } catch (error: unknown) {
448
+ sendError(reply, error)
449
+ }
450
+ })
451
+ }
452
+
453
+ if (config.enableAll || config.update) {
454
+ const opConfig = config.update || defaultOpConfig
455
+ const { before = [], after = [] } = opConfig
456
+ const path = basePath || '/'
457
+ fastify.put(path, async (request, reply) => {
458
+ try {
459
+ makeShapeHook(config, opConfig)(request)
460
+ if (await runHooks(before, request, reply)) return
461
+ await ${modelName}Update(request, reply)
462
+ if (await runHooks(after, request, reply)) return
463
+ sendResult(request, reply)
464
+ } catch (error: unknown) {
465
+ sendError(reply, error)
466
+ }
467
+ })
468
+ }
469
+
470
+ if (config.enableAll || config.upsert) {
471
+ const opConfig = config.upsert || defaultOpConfig
472
+ const { before = [], after = [] } = opConfig
473
+ const path = basePath || '/'
474
+ fastify.patch(path, async (request, reply) => {
475
+ try {
476
+ makeShapeHook(config, opConfig)(request)
477
+ if (await runHooks(before, request, reply)) return
478
+ await ${modelName}Upsert(request, reply)
479
+ if (await runHooks(after, request, reply)) return
480
+ sendResult(request, reply)
481
+ } catch (error: unknown) {
482
+ sendError(reply, error)
483
+ }
484
+ })
485
+ }
486
+
487
+ if (config.enableAll || config.deleteMany) {
488
+ const opConfig = config.deleteMany || defaultOpConfig
489
+ const { before = [], after = [] } = opConfig
490
+ const path = basePath ? \`\${basePath}/many\` : '/many'
491
+ fastify.delete(path, async (request, reply) => {
492
+ try {
493
+ makeShapeHook(config, opConfig)(request)
494
+ if (await runHooks(before, request, reply)) return
495
+ await ${modelName}DeleteMany(request, reply)
496
+ if (await runHooks(after, request, reply)) return
497
+ sendResult(request, reply)
498
+ } catch (error: unknown) {
499
+ sendError(reply, error)
500
+ }
501
+ })
502
+ }
503
+
504
+ if (config.enableAll || config.delete) {
505
+ const opConfig = config.delete || defaultOpConfig
506
+ const { before = [], after = [] } = opConfig
507
+ const path = basePath || '/'
508
+ fastify.delete(path, async (request, reply) => {
509
+ try {
510
+ makeShapeHook(config, opConfig)(request)
511
+ if (await runHooks(before, request, reply)) return
512
+ await ${modelName}Delete(request, reply)
513
+ if (await runHooks(after, request, reply)) return
514
+ sendResult(request, reply)
515
+ } catch (error: unknown) {
516
+ sendError(reply, error)
517
+ }
518
+ })
519
+ }
520
+ }
521
+ `
522
+ }