prisma-generator-express 1.18.0 → 1.20.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 (76) hide show
  1. package/README.md +399 -194
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +1 -1
  4. package/dist/bin.js.map +1 -1
  5. package/dist/client/encodeQueryParams.d.ts +1 -0
  6. package/dist/client/encodeQueryParams.js +33 -0
  7. package/dist/client/encodeQueryParams.js.map +1 -0
  8. package/dist/constants.d.ts +1 -0
  9. package/dist/copy/misc.d.ts +5 -0
  10. package/dist/copy/misc.js +52 -0
  11. package/dist/copy/misc.js.map +1 -0
  12. package/dist/generators/generateImportPrismaStatement.d.ts +3 -0
  13. package/dist/generators/generateImportPrismaStatement.js +55 -0
  14. package/dist/generators/generateImportPrismaStatement.js.map +1 -0
  15. package/dist/generators/generateQueryBuilderHelper.d.ts +2 -0
  16. package/dist/generators/generateQueryBuilderHelper.js +139 -0
  17. package/dist/generators/generateQueryBuilderHelper.js.map +1 -0
  18. package/dist/generators/generateRouter.d.ts +6 -0
  19. package/dist/generators/generateRouter.js +340 -0
  20. package/dist/generators/generateRouter.js.map +1 -0
  21. package/dist/generators/generateUnifiedDocs.d.ts +1 -0
  22. package/dist/generators/generateUnifiedDocs.js +171 -0
  23. package/dist/generators/generateUnifiedDocs.js.map +1 -0
  24. package/dist/generators/generateUnifiedHandler.d.ts +6 -0
  25. package/dist/generators/generateUnifiedHandler.js +444 -0
  26. package/dist/generators/generateUnifiedHandler.js.map +1 -0
  27. package/dist/generators/generateUnifiedScalarUI.d.ts +5 -0
  28. package/dist/generators/generateUnifiedScalarUI.js +1390 -0
  29. package/dist/generators/generateUnifiedScalarUI.js.map +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +80 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/utils/copyFiles.d.ts +6 -0
  34. package/dist/utils/copyFiles.js +123 -21
  35. package/dist/utils/copyFiles.js.map +1 -1
  36. package/dist/utils/strings.d.ts +2 -0
  37. package/dist/utils/writeFileSafely.d.ts +10 -0
  38. package/dist/utils/writeFileSafely.js +86 -14
  39. package/dist/utils/writeFileSafely.js.map +1 -1
  40. package/package.json +59 -28
  41. package/src/bin.ts +1 -1
  42. package/src/client/encodeQueryParams.ts +56 -0
  43. package/src/copy/buildModelOpenApi.ts +1569 -0
  44. package/src/copy/misc.ts +21 -0
  45. package/src/copy/operationDefinitions.ts +96 -0
  46. package/src/copy/parseQueryParams.ts +36 -21
  47. package/src/copy/routeConfig.ts +68 -28
  48. package/src/generators/generateImportPrismaStatement.ts +78 -0
  49. package/src/generators/generateQueryBuilderHelper.ts +138 -0
  50. package/src/generators/generateRouter.ts +352 -0
  51. package/src/generators/generateUnifiedDocs.ts +168 -0
  52. package/src/generators/generateUnifiedHandler.ts +469 -0
  53. package/src/generators/generateUnifiedScalarUI.ts +1409 -0
  54. package/src/index.ts +100 -0
  55. package/src/utils/copyFiles.ts +123 -16
  56. package/src/utils/writeFileSafely.ts +79 -25
  57. package/dist/generator.js +0 -47
  58. package/dist/generator.js.map +0 -1
  59. package/dist/helpers/generateImportPrismaStatement.js +0 -25
  60. package/dist/helpers/generateImportPrismaStatement.js.map +0 -1
  61. package/dist/helpers/generateOperation.js +0 -471
  62. package/dist/helpers/generateOperation.js.map +0 -1
  63. package/dist/helpers/generateRouteFile.js +0 -210
  64. package/dist/helpers/generateRouteFile.js.map +0 -1
  65. package/dist/utils/formatFile.js +0 -26
  66. package/dist/utils/formatFile.js.map +0 -1
  67. package/src/copy/encodeQueryParams.spec.ts +0 -303
  68. package/src/copy/encodeQueryParams.ts +0 -44
  69. package/src/copy/misc.spec.ts +0 -62
  70. package/src/copy/parseQueryParams.spec.ts +0 -187
  71. package/src/copy/transformZod.spec.ts +0 -763
  72. package/src/generator.ts +0 -54
  73. package/src/helpers/generateImportPrismaStatement.ts +0 -38
  74. package/src/helpers/generateOperation.ts +0 -515
  75. package/src/helpers/generateRouteFile.ts +0 -213
  76. package/src/utils/formatFile.ts +0 -22
@@ -0,0 +1,352 @@
1
+ import { DMMF } from '@prisma/generator-helper'
2
+
3
+ export function generateRouterFunction({
4
+ model,
5
+ enums,
6
+ relativeClientPath,
7
+ }: {
8
+ model: DMMF.Model
9
+ enums: DMMF.DatamodelEnum[]
10
+ relativeClientPath: string
11
+ }): string {
12
+ const modelName = model.name
13
+ const modelNameLower = modelName.toLowerCase()
14
+ const routerFunctionName = `${modelName}Router`
15
+
16
+ const fieldsMeta = model.fields.map((f) => ({
17
+ name: f.name,
18
+ kind: f.kind,
19
+ type: f.type,
20
+ isList: f.isList,
21
+ isRequired: f.isRequired,
22
+ hasDefaultValue: f.hasDefaultValue,
23
+ isUpdatedAt: f.isUpdatedAt ?? false,
24
+ documentation: f.documentation,
25
+ relationFromFields: f.relationFromFields,
26
+ }))
27
+
28
+ const referencedEnumTypes = new Set(
29
+ model.fields.filter((f) => f.kind === 'enum').map((f) => f.type),
30
+ )
31
+
32
+ const enumsMeta = enums
33
+ .filter((e) => referencedEnumTypes.has(e.name))
34
+ .map((e) => ({
35
+ name: e.name,
36
+ values: e.values.map((v) => ({ name: v.name })),
37
+ }))
38
+
39
+ return `import express, { Request, Response, NextFunction, RequestHandler } from 'express'
40
+ import type { PrismaClient } from '${relativeClientPath}'
41
+ import {
42
+ ${modelName}FindUnique,
43
+ ${modelName}FindUniqueOrThrow,
44
+ ${modelName}FindFirst,
45
+ ${modelName}FindFirstOrThrow,
46
+ ${modelName}FindMany,
47
+ ${modelName}FindManyPaginated,
48
+ ${modelName}Create,
49
+ ${modelName}CreateMany,
50
+ ${modelName}CreateManyAndReturn,
51
+ ${modelName}Update,
52
+ ${modelName}UpdateMany,
53
+ ${modelName}UpdateManyAndReturn,
54
+ ${modelName}Upsert,
55
+ ${modelName}Delete,
56
+ ${modelName}DeleteMany,
57
+ ${modelName}Aggregate,
58
+ ${modelName}Count,
59
+ ${modelName}GroupBy
60
+ } from './${modelName}Handlers'
61
+ import type { RouteConfig } from '../routeConfig'
62
+ import { parseQueryParams } from '../parseQueryParams'
63
+ import { buildModelOpenApi } from '../buildModelOpenApi'
64
+
65
+ const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
66
+
67
+ const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
68
+
69
+ const MODEL_ENUMS = ${JSON.stringify(enumsMeta, null, 2)} as const
70
+
71
+ const defaultOpConfig = {
72
+ before: [] as RequestHandler[],
73
+ after: [] as RequestHandler[],
74
+ }
75
+
76
+ function normalizePrefix(p: string): string {
77
+ if (!p) return ''
78
+ let result = p
79
+ if (!result.startsWith('/')) result = '/' + result
80
+ while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
81
+ if (result === '/') return ''
82
+ return result
83
+ }
84
+
85
+ function transformResult(value: unknown): unknown {
86
+ if (value === null || value === undefined) return value
87
+ if (typeof value === 'bigint') return value.toString()
88
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
89
+ return value.toString('base64')
90
+ }
91
+ if (value instanceof Uint8Array) {
92
+ let binary = ''
93
+ for (let i = 0; i < value.length; i++) binary += String.fromCharCode(value[i])
94
+ return btoa(binary)
95
+ }
96
+ if (value instanceof Date) return value
97
+ if (Array.isArray(value)) return value.map(transformResult)
98
+ if (typeof value === 'object') {
99
+ const proto = Object.getPrototypeOf(value)
100
+ if (proto !== Object.prototype && proto !== null) return value
101
+ const out: Record<string, unknown> = {}
102
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
103
+ out[k] = transformResult(v)
104
+ }
105
+ return out
106
+ }
107
+ return value
108
+ }
109
+
110
+ function isQueryBuilderEnabled(config: RouteConfig): boolean {
111
+ if (config.queryBuilder === false) return false
112
+ if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
113
+ if (_env.NODE_ENV === 'production') return false
114
+ return true
115
+ }
116
+
117
+ function getQueryBuilderConfig(config: RouteConfig) {
118
+ if (config.queryBuilder === false) return null
119
+ if (typeof config.queryBuilder === 'object') return config.queryBuilder
120
+ return {}
121
+ }
122
+
123
+ export function ${routerFunctionName}(config: RouteConfig = {}) {
124
+ const router = express.Router()
125
+
126
+ router.use(express.json())
127
+
128
+ const customPrefix = normalizePrefix(config.customUrlPrefix || '')
129
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
130
+ const basePath = customPrefix + modelPrefix
131
+
132
+ const openApiDisabled = config.disableOpenApi === true
133
+ || (config.disableOpenApi !== false && (
134
+ _env.DISABLE_OPENAPI === 'true'
135
+ || _env.NODE_ENV === 'production'
136
+ ))
137
+
138
+ const qbEnabled = isQueryBuilderEnabled(config)
139
+
140
+ if (qbEnabled) {
141
+ const qbConfig = getQueryBuilderConfig(config)
142
+ if (qbConfig) {
143
+ import('../queryBuilder').then(mod => mod.startQueryBuilder(qbConfig)).catch(() => {})
144
+ }
145
+ }
146
+
147
+ const parseQuery: RequestHandler = (req, res, next) => {
148
+ const rawQuery = req.query
149
+ if (rawQuery && Object.keys(rawQuery).length > 0) {
150
+ res.locals.parsedQuery = parseQueryParams(rawQuery as Record<string, unknown>)
151
+ }
152
+ next()
153
+ }
154
+
155
+ const setShape = (opConfig: any): RequestHandler => {
156
+ return (req, res, next) => {
157
+ res.locals.routeConfig = config
158
+ if (opConfig.shape) {
159
+ res.locals.guardShape = opConfig.shape
160
+ const caller = config.guard?.resolveVariant?.(req)
161
+ ?? req.get(config.guard?.variantHeader || 'x-api-variant')
162
+ ?? undefined
163
+ if (caller) {
164
+ res.locals.guardCaller = caller
165
+ }
166
+ }
167
+ next()
168
+ }
169
+ }
170
+
171
+ const respond: RequestHandler = (_req, res) => {
172
+ const data = res.locals.data
173
+ if (data === undefined) {
174
+ return res.status(500).json({ message: 'No data set by handler' })
175
+ }
176
+ return res.json(transformResult(data))
177
+ }
178
+
179
+ const respondCreated: RequestHandler = (_req, res) => {
180
+ const data = res.locals.data
181
+ if (data === undefined) {
182
+ return res.status(500).json({ message: 'No data set by handler' })
183
+ }
184
+ return res.status(201).json(transformResult(data))
185
+ }
186
+
187
+ if (!openApiDisabled) {
188
+ const openapiJsonPath = basePath ? \`\${basePath}/openapi.json\` : '/openapi.json'
189
+ const openapiYamlPath = basePath ? \`\${basePath}/openapi.yaml\` : '/openapi.yaml'
190
+
191
+ router.get(openapiJsonPath, (_req, res) => {
192
+ const spec = buildModelOpenApi(
193
+ '${modelName}',
194
+ MODEL_FIELDS as any,
195
+ MODEL_ENUMS as any,
196
+ config,
197
+ { format: 'json' }
198
+ )
199
+ res.json(spec)
200
+ })
201
+
202
+ router.get(openapiYamlPath, (_req, res) => {
203
+ const spec = buildModelOpenApi(
204
+ '${modelName}',
205
+ MODEL_FIELDS as any,
206
+ MODEL_ENUMS as any,
207
+ config,
208
+ { format: 'yaml' }
209
+ )
210
+ res.type('application/yaml').send(spec as string)
211
+ })
212
+ }
213
+
214
+ if (config.enableAll || config.findFirst) {
215
+ const opConfig = config.findFirst || defaultOpConfig
216
+ const { before = [], after = [] } = opConfig
217
+ const path = basePath ? \`\${basePath}/first\` : '/first'
218
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}FindFirst as RequestHandler, ...after, respond)
219
+ }
220
+
221
+ if (config.enableAll || config.findFirstOrThrow) {
222
+ const opConfig = config.findFirstOrThrow || defaultOpConfig
223
+ const { before = [], after = [] } = opConfig
224
+ const path = basePath ? \`\${basePath}/first/strict\` : '/first/strict'
225
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}FindFirstOrThrow as RequestHandler, ...after, respond)
226
+ }
227
+
228
+ if (config.enableAll || config.findManyPaginated) {
229
+ const opConfig = config.findManyPaginated || defaultOpConfig
230
+ const { before = [], after = [] } = opConfig
231
+ const path = basePath ? \`\${basePath}/paginated\` : '/paginated'
232
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}FindManyPaginated as RequestHandler, ...after, respond)
233
+ }
234
+
235
+ if (config.enableAll || config.aggregate) {
236
+ const opConfig = config.aggregate || defaultOpConfig
237
+ const { before = [], after = [] } = opConfig
238
+ const path = basePath ? \`\${basePath}/aggregate\` : '/aggregate'
239
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}Aggregate as RequestHandler, ...after, respond)
240
+ }
241
+
242
+ if (config.enableAll || config.count) {
243
+ const opConfig = config.count || defaultOpConfig
244
+ const { before = [], after = [] } = opConfig
245
+ const path = basePath ? \`\${basePath}/count\` : '/count'
246
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}Count as RequestHandler, ...after, respond)
247
+ }
248
+
249
+ if (config.enableAll || config.groupBy) {
250
+ const opConfig = config.groupBy || defaultOpConfig
251
+ const { before = [], after = [] } = opConfig
252
+ const path = basePath ? \`\${basePath}/groupby\` : '/groupby'
253
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}GroupBy as RequestHandler, ...after, respond)
254
+ }
255
+
256
+ if (config.enableAll || config.findUniqueOrThrow) {
257
+ const opConfig = config.findUniqueOrThrow || defaultOpConfig
258
+ const { before = [], after = [] } = opConfig
259
+ const path = basePath ? \`\${basePath}/unique/strict\` : '/unique/strict'
260
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}FindUniqueOrThrow as RequestHandler, ...after, respond)
261
+ }
262
+
263
+ if (config.enableAll || config.findUnique) {
264
+ const opConfig = config.findUnique || defaultOpConfig
265
+ const { before = [], after = [] } = opConfig
266
+ const path = basePath ? \`\${basePath}/unique\` : '/unique'
267
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}FindUnique as RequestHandler, ...after, respond)
268
+ }
269
+
270
+ if (config.enableAll || config.findMany) {
271
+ const opConfig = config.findMany || defaultOpConfig
272
+ const { before = [], after = [] } = opConfig
273
+ const path = basePath || '/'
274
+ router.get(path, parseQuery, setShape(opConfig), ...before, ${modelName}FindMany as RequestHandler, ...after, respond)
275
+ }
276
+
277
+ if (config.enableAll || config.createManyAndReturn) {
278
+ const opConfig = config.createManyAndReturn || defaultOpConfig
279
+ const { before = [], after = [] } = opConfig
280
+ const path = basePath ? \`\${basePath}/many/return\` : '/many/return'
281
+ router.post(path, setShape(opConfig), ...before, ${modelName}CreateManyAndReturn as RequestHandler, ...after, respondCreated)
282
+ }
283
+
284
+ if (config.enableAll || config.createMany) {
285
+ const opConfig = config.createMany || defaultOpConfig
286
+ const { before = [], after = [] } = opConfig
287
+ const path = basePath ? \`\${basePath}/many\` : '/many'
288
+ router.post(path, setShape(opConfig), ...before, ${modelName}CreateMany as RequestHandler, ...after, respondCreated)
289
+ }
290
+
291
+ if (config.enableAll || config.create) {
292
+ const opConfig = config.create || defaultOpConfig
293
+ const { before = [], after = [] } = opConfig
294
+ const path = basePath || '/'
295
+ router.post(path, setShape(opConfig), ...before, ${modelName}Create as RequestHandler, ...after, respondCreated)
296
+ }
297
+
298
+ if (config.enableAll || config.updateManyAndReturn) {
299
+ const opConfig = config.updateManyAndReturn || defaultOpConfig
300
+ const { before = [], after = [] } = opConfig
301
+ const path = basePath ? \`\${basePath}/many/return\` : '/many/return'
302
+ router.put(path, setShape(opConfig), ...before, ${modelName}UpdateManyAndReturn as RequestHandler, ...after, respond)
303
+ }
304
+
305
+ if (config.enableAll || config.updateMany) {
306
+ const opConfig = config.updateMany || defaultOpConfig
307
+ const { before = [], after = [] } = opConfig
308
+ const path = basePath ? \`\${basePath}/many\` : '/many'
309
+ router.put(path, setShape(opConfig), ...before, ${modelName}UpdateMany as RequestHandler, ...after, respond)
310
+ }
311
+
312
+ if (config.enableAll || config.update) {
313
+ const opConfig = config.update || defaultOpConfig
314
+ const { before = [], after = [] } = opConfig
315
+ const path = basePath || '/'
316
+ router.put(path, setShape(opConfig), ...before, ${modelName}Update as RequestHandler, ...after, respond)
317
+ }
318
+
319
+ if (config.enableAll || config.upsert) {
320
+ const opConfig = config.upsert || defaultOpConfig
321
+ const { before = [], after = [] } = opConfig
322
+ const path = basePath || '/'
323
+ router.patch(path, setShape(opConfig), ...before, ${modelName}Upsert as RequestHandler, ...after, respond)
324
+ }
325
+
326
+ if (config.enableAll || config.deleteMany) {
327
+ const opConfig = config.deleteMany || defaultOpConfig
328
+ const { before = [], after = [] } = opConfig
329
+ const path = basePath ? \`\${basePath}/many\` : '/many'
330
+ router.delete(path, setShape(opConfig), ...before, ${modelName}DeleteMany as RequestHandler, ...after, respond)
331
+ }
332
+
333
+ if (config.enableAll || config.delete) {
334
+ const opConfig = config.delete || defaultOpConfig
335
+ const { before = [], after = [] } = opConfig
336
+ const path = basePath || '/'
337
+ router.delete(path, setShape(opConfig), ...before, ${modelName}Delete as RequestHandler, ...after, respond)
338
+ }
339
+
340
+ router.use((err: any, _req: Request, res: Response, next: NextFunction) => {
341
+ const status = typeof err.status === 'number' ? err.status : 500
342
+ const message = err.message || 'Internal server error'
343
+ if (!res.headersSent) {
344
+ return res.status(status).json({ message })
345
+ }
346
+ next(err)
347
+ })
348
+
349
+ return router
350
+ }
351
+ `
352
+ }
@@ -0,0 +1,168 @@
1
+ export function generateUnifiedDocs(models: string[]): string {
2
+ const imports = models
3
+ .map((model) => `import { ${model}Docs } from './${model}/${model}Docs'`)
4
+ .join('\n')
5
+
6
+ return `${imports}
7
+ import { Request, Response, RequestHandler } from 'express'
8
+ import type { RouteConfig } from './routeConfig'
9
+
10
+ const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
11
+
12
+ const docsHandlers: Record<string, (config: any) => (req: Request, res: Response) => any> = {
13
+ ${models.map((model) => ` ${model}: ${model}Docs`).join(',\n')}
14
+ }
15
+
16
+ type DocsUI = 'docs' | 'scalar' | 'json' | 'yaml' | 'playground'
17
+
18
+ interface ModelDocsConfig extends RouteConfig {
19
+ docsTitle?: string
20
+ docsUi?: DocsUI
21
+ }
22
+
23
+ export interface CombinedDocsConfig {
24
+ disableOpenApi?: boolean
25
+ title?: string
26
+ description?: string
27
+ basePath?: string
28
+ version?: string
29
+ modelConfigs: {
30
+ [modelName: string]: ModelDocsConfig
31
+ }
32
+ }
33
+
34
+ function escapeHtml(input: string) {
35
+ return input
36
+ .replaceAll('&', '&amp;')
37
+ .replaceAll('<', '&lt;')
38
+ .replaceAll('>', '&gt;')
39
+ .replaceAll('"', '&quot;')
40
+ .replaceAll("'", '&#039;')
41
+ }
42
+
43
+ function removeTrailingSlash(p: string): string {
44
+ if (p === '/') return ''
45
+ return p.endsWith('/') ? p.slice(0, -1) : p
46
+ }
47
+
48
+ function isOpenApiDisabled(disableOpenApi?: boolean) {
49
+ if (disableOpenApi === true) return true
50
+ if (disableOpenApi === false) return false
51
+ return _env.DISABLE_OPENAPI === 'true' || _env.NODE_ENV === 'production'
52
+ }
53
+
54
+ function isPlaygroundAvailable(config?: ModelDocsConfig) {
55
+ if (_env.NODE_ENV === 'production') return false
56
+ if (!config) return true
57
+ if (config.queryBuilder === false) return false
58
+ if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
59
+ return true
60
+ }
61
+
62
+ export function generateCombinedDocs(config: CombinedDocsConfig) {
63
+ const title = config.title || 'API Documentation'
64
+ const description = config.description || ''
65
+ const version = config.version || ''
66
+
67
+ return (req: Request, res: Response) => {
68
+ const registeredModels = Object.keys(config.modelConfigs).filter((m) => {
69
+ const cfg = config.modelConfigs[m]
70
+ return m in docsHandlers && !isOpenApiDisabled(cfg?.disableOpenApi ?? config.disableOpenApi)
71
+ })
72
+
73
+ if (registeredModels.length === 0) {
74
+ return res.status(404).send('OpenAPI documentation is disabled')
75
+ }
76
+
77
+ const basePath = removeTrailingSlash(config.basePath || '/docs')
78
+ const generatedAt = new Date().toISOString()
79
+
80
+ const html = \`<!DOCTYPE html>
81
+ <html lang="en">
82
+ <head>
83
+ <meta charset="utf-8" />
84
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
85
+ <title>\${escapeHtml(title)}</title>
86
+ <script src="https://cdn.tailwindcss.com"></script>
87
+ </head>
88
+ <body class="m-0 bg-white text-gray-900 font-serif leading-normal">
89
+ <div class="max-w-[980px] mx-auto px-7 pt-10 pb-16">
90
+ <div class="border-b-2 border-gray-900 pb-3.5 mb-[18px]">
91
+ <div class="text-[28px] font-bold tracking-wide">\${escapeHtml(title)}</div>
92
+ \${description ? '<div class="mt-1.5 text-gray-500 text-sm">' + escapeHtml(description) + '</div>' : ''}
93
+ <div class="mt-3 flex gap-x-5 text-[13px] text-gray-500">
94
+ \${version ? '<div>Version: ' + escapeHtml(version) + '</div>' : ''}
95
+ <div>Generated: \${escapeHtml(generatedAt)}</div>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="mt-[22px]">
100
+ <h2 class="m-0 mb-2.5 text-lg border-t border-gray-300 pt-3.5">Models</h2>
101
+ <table class="w-full border-collapse text-[13px]">
102
+ <thead>
103
+ <tr>
104
+ <th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Model</th>
105
+ <th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Documentation</th>
106
+ <th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Views</th>
107
+ </tr>
108
+ </thead>
109
+ <tbody>
110
+ \${registeredModels.map((m) => {
111
+ const lower = m.toLowerCase()
112
+ const docsUrl = \\\`\\\${basePath}/\\\${lower}\\\`
113
+ const scalarUrl = \\\`\\\${basePath}/\\\${lower}?ui=scalar\\\`
114
+ const jsonUrl = \\\`\\\${basePath}/\\\${lower}?ui=json\\\`
115
+ const yamlUrl = \\\`\\\${basePath}/\\\${lower}?ui=yaml\\\`
116
+ const playgroundUrl = \\\`\\\${basePath}/\\\${lower}?ui=playground\\\`
117
+ const modelCfg = config.modelConfigs[m]
118
+ const modelPlayground = isPlaygroundAvailable(modelCfg)
119
+ const playgroundLink = modelPlayground
120
+ ? \\\`, <a href="\\\${playgroundUrl}" class="text-inherit underline">playground</a>\\\`
121
+ : ''
122
+ return \\\`
123
+ <tr>
124
+ <td class="text-left py-2 px-2 border-b border-gray-300 align-top">\\\${escapeHtml(m)}</td>
125
+ <td class="text-left py-2 px-2 border-b border-gray-300 align-top"><a href="\\\${docsUrl}" class="text-inherit underline">\\\${escapeHtml(docsUrl)}</a></td>
126
+ <td class="text-left py-2 px-2 border-b border-gray-300 align-top">
127
+ <a href="\\\${scalarUrl}" class="text-inherit underline">scalar</a>,
128
+ <a href="\\\${jsonUrl}" class="text-inherit underline">json</a>,
129
+ <a href="\\\${yamlUrl}" class="text-inherit underline">yaml</a>\\\${playgroundLink}
130
+ </td>
131
+ </tr>
132
+ \\\`
133
+ }).join('')}
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ </div>
138
+ </body>
139
+ </html>\`
140
+
141
+ res.type('html').send(html)
142
+ }
143
+ }
144
+
145
+ export function registerModelDocs(
146
+ app: any,
147
+ basePath: string = '/docs',
148
+ configs: CombinedDocsConfig['modelConfigs'] = {},
149
+ options?: { disableOpenApi?: boolean }
150
+ ) {
151
+ const normalizedBase = removeTrailingSlash(basePath)
152
+ const registeredModels = Object.keys(configs).filter((m) => {
153
+ const cfg = configs[m]
154
+ return m in docsHandlers && !isOpenApiDisabled(cfg?.disableOpenApi ?? options?.disableOpenApi)
155
+ })
156
+
157
+ if (registeredModels.length === 0) return
158
+
159
+ registeredModels.forEach((model) => {
160
+ const handler = docsHandlers[model]
161
+ const cfg = configs[model] || {}
162
+ const path = \\\`\\\${normalizedBase}/\\\${model.toLowerCase()}\\\`
163
+ console.log(\\\` Registered docs: \\\${path}\\\`)
164
+ app.get(path, handler(cfg))
165
+ })
166
+ }
167
+ `
168
+ }