prisma-generator-express 1.28.0 → 1.30.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 +73 -39
  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 +88 -44
  49. package/src/utils/writeFileSafely.ts +4 -0
@@ -25,8 +25,107 @@ function exampleValueForType(fieldType) {
25
25
  return 'example';
26
26
  }
27
27
  }
28
+ function generateExpressDocsExport(modelName) {
29
+ return `export function ${modelName}Docs(config: DocsConfig = {}) {
30
+ return (req: Request, res: Response) => {
31
+ const disabled = isOpenApiDisabled(config.disableOpenApi)
32
+ if (disabled) return res.status(404).send('OpenAPI documentation is disabled in production')
33
+
34
+ const rawUi = (req.query['ui'] as string | undefined) || config.docsUi || 'docs'
35
+ const validUis: DocsUI[] = ['docs', 'scalar', 'json', 'yaml', 'playground']
36
+ const ui: DocsUI = validUis.includes(rawUi as DocsUI) ? (rawUi as DocsUI) : 'docs'
37
+
38
+ if (ui === 'playground') {
39
+ if (!isPlaygroundAvailable(config)) {
40
+ return res.status(404).send('Query builder is disabled')
41
+ }
42
+ return res.type('html').send(renderPlayground('${modelName}', config))
43
+ }
44
+
45
+ if (ui === 'yaml') {
46
+ const yaml = buildModelOpenApi(
47
+ '${modelName}',
48
+ MODEL_FIELDS as any,
49
+ MODEL_ENUMS as any,
50
+ config,
51
+ { format: 'yaml' }
52
+ )
53
+ return res.type('application/yaml').send(yaml as string)
54
+ }
55
+
56
+ const spec = buildModelOpenApi(
57
+ '${modelName}',
58
+ MODEL_FIELDS as any,
59
+ MODEL_ENUMS as any,
60
+ config,
61
+ { format: 'json' }
62
+ )
63
+
64
+ if (ui === 'json') return res.json(spec)
65
+
66
+ const pageTitle = config.docsTitle || \`${modelName} API\`
67
+
68
+ if (ui === 'scalar') {
69
+ return res.type('html').send(renderScalar('${modelName}', spec, pageTitle, config.scalarCdnUrl))
70
+ }
71
+
72
+ const html = renderDocs('${modelName}', config, MODEL_CONTEXT)
73
+ return res.type('html').send(html)
74
+ }
75
+ }`;
76
+ }
77
+ function generateFastifyDocsExport(modelName) {
78
+ return `export function ${modelName}Docs(config: DocsConfig = {}) {
79
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
80
+ const disabled = isOpenApiDisabled(config.disableOpenApi)
81
+ if (disabled) { reply.code(404).send('OpenAPI documentation is disabled in production'); return }
82
+
83
+ const rawUi = ((request.query as any)?.ui as string | undefined) || config.docsUi || 'docs'
84
+ const validUis: DocsUI[] = ['docs', 'scalar', 'json', 'yaml', 'playground']
85
+ const ui: DocsUI = validUis.includes(rawUi as DocsUI) ? (rawUi as DocsUI) : 'docs'
86
+
87
+ if (ui === 'playground') {
88
+ if (!isPlaygroundAvailable(config)) {
89
+ reply.code(404).send('Query builder is disabled'); return
90
+ }
91
+ reply.type('text/html').send(renderPlayground('${modelName}', config)); return
92
+ }
93
+
94
+ if (ui === 'yaml') {
95
+ const yaml = buildModelOpenApi(
96
+ '${modelName}',
97
+ MODEL_FIELDS as any,
98
+ MODEL_ENUMS as any,
99
+ config,
100
+ { format: 'yaml' }
101
+ )
102
+ reply.type('application/yaml').send(yaml as string); return
103
+ }
104
+
105
+ const spec = buildModelOpenApi(
106
+ '${modelName}',
107
+ MODEL_FIELDS as any,
108
+ MODEL_ENUMS as any,
109
+ config,
110
+ { format: 'json' }
111
+ )
112
+
113
+ if (ui === 'json') { reply.send(spec); return }
114
+
115
+ const pageTitle = config.docsTitle || \`${modelName} API\`
116
+
117
+ if (ui === 'scalar') {
118
+ reply.type('text/html').send(renderScalar('${modelName}', spec, pageTitle, config.scalarCdnUrl)); return
119
+ }
120
+
121
+ const html = renderDocs('${modelName}', config, MODEL_CONTEXT)
122
+ reply.type('text/html').send(html)
123
+ }
124
+ }`;
125
+ }
28
126
  function generateScalarUIHandler(options) {
29
127
  const { model, enums } = options;
128
+ const target = options.target || 'express';
30
129
  const modelName = model.name;
31
130
  const fieldsMeta = model.fields.map((f) => ({
32
131
  name: f.name,
@@ -66,1342 +165,46 @@ function generateScalarUIHandler(options) {
66
165
  name: idx.name || idx.fields.join('_'),
67
166
  fields: idx.fields,
68
167
  }));
69
- return `import { Request, Response } from 'express'
168
+ const frameworkImport = target === 'fastify'
169
+ ? `import type { FastifyRequest, FastifyReply } from 'fastify'`
170
+ : `import { Request, Response } from 'express'`;
171
+ const docsExport = target === 'fastify'
172
+ ? generateFastifyDocsExport(modelName)
173
+ : generateExpressDocsExport(modelName);
174
+ return `${frameworkImport}
70
175
  import { buildModelOpenApi } from '../buildModelOpenApi.js'
71
- import type { RouteConfig } from '../routeConfig.js'
72
- import { OPERATION_DEFS, isOperationEnabled } from '../operationDefinitions.js'
73
-
74
- const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
75
-
76
- interface FieldMeta {
77
- name: string
78
- kind: string
79
- type: string
80
- isList: boolean
81
- isRequired: boolean
82
- hasDefaultValue: boolean
83
- isUpdatedAt: boolean
84
- documentation: string | null
85
- isId: boolean
86
- isUnique: boolean
87
- relationFromFields?: string[]
88
- }
89
-
90
- interface EnumMeta {
91
- name: string
92
- values: { name: string }[]
93
- }
176
+ import {
177
+ renderDocs,
178
+ renderScalar,
179
+ renderPlayground,
180
+ isOpenApiDisabled,
181
+ isPlaygroundAvailable,
182
+ type FieldMeta,
183
+ type EnumMeta,
184
+ type DocsUI,
185
+ type DocsConfig,
186
+ type DocsModelContext,
187
+ } from '../docsRenderer.js'
94
188
 
95
189
  export const MODEL_FIELDS: FieldMeta[] = ${JSON.stringify(fieldsMeta, null, 2)}
190
+
96
191
  export const MODEL_ENUMS: EnumMeta[] = ${JSON.stringify(enumsMeta, null, 2)}
97
192
 
98
193
  const COMPOUND_ID: { fields: string[] } | null = ${JSON.stringify(compoundIdMeta)}
99
194
 
100
- const COMPOUND_UNIQUES: { name: string, fields: string[] }[] = ${JSON.stringify(compoundUniquesMeta)}
195
+ const COMPOUND_UNIQUES: { name: string; fields: string[] }[] = ${JSON.stringify(compoundUniquesMeta)}
101
196
 
102
197
  const EXAMPLE_VALUES: Record<string, unknown> = ${exampleValueMap}
103
198
 
104
- const DEFAULT_SCALAR_CDN = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference'
105
- const PRISM_CSS = 'https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css'
106
- const PRISM_JS = 'https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js'
107
- const PRISM_JSON = 'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js'
108
-
109
- type DocsUI = 'docs' | 'scalar' | 'json' | 'yaml' | 'playground'
110
-
111
- type DocsConfig = RouteConfig & {
112
- docsTitle?: string
113
- docsUi?: DocsUI
114
- }
115
-
116
- interface OpDetail {
117
- transport: string
118
- required: string[]
119
- optional: string[]
120
- responseDesc: string
121
- errors: number[]
122
- supportsSelect: boolean
123
- supportsInclude: boolean
124
- supportsOmit: boolean
125
- notes: string
126
- }
127
-
128
- const OP_DETAIL_MAP: Record<string, OpDetail> = {
129
- findMany: {
130
- transport: 'GET query params',
131
- required: [],
132
- optional: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
133
- responseDesc: 'Array of records',
134
- errors: [400, 403, 500, 501, 503],
135
- supportsSelect: true,
136
- supportsInclude: true,
137
- supportsOmit: true,
138
- notes: 'Pagination limits may apply when configured.',
139
- },
140
- findUnique: {
141
- transport: 'GET query params',
142
- required: ['where'],
143
- optional: ['select', 'include', 'omit'],
144
- responseDesc: 'Single record or null',
145
- errors: [400, 403, 500, 501, 503],
146
- supportsSelect: true,
147
- supportsInclude: true,
148
- supportsOmit: true,
149
- notes: 'Returns null (not 404) when no record matches.',
150
- },
151
- findUniqueOrThrow: {
152
- transport: 'GET query params',
153
- required: ['where'],
154
- optional: ['select', 'include', 'omit'],
155
- responseDesc: 'Single record',
156
- errors: [400, 403, 404, 500, 501, 503],
157
- supportsSelect: true,
158
- supportsInclude: true,
159
- supportsOmit: true,
160
- notes: 'Returns 404 when no record matches.',
161
- },
162
- findFirst: {
163
- transport: 'GET query params',
164
- required: [],
165
- optional: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
166
- responseDesc: 'Single record or null',
167
- errors: [400, 403, 500, 501, 503],
168
- supportsSelect: true,
169
- supportsInclude: true,
170
- supportsOmit: true,
171
- notes: 'Returns null (not 404) when no record matches.',
172
- },
173
- findFirstOrThrow: {
174
- transport: 'GET query params',
175
- required: [],
176
- optional: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
177
- responseDesc: 'Single record',
178
- errors: [400, 403, 404, 500, 501, 503],
179
- supportsSelect: true,
180
- supportsInclude: true,
181
- supportsOmit: true,
182
- notes: 'Returns 404 when no record matches.',
183
- },
184
- findManyPaginated: {
185
- transport: 'GET query params',
186
- required: [],
187
- optional: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
188
- responseDesc: '{ data: Record[], total: number, hasMore: boolean }',
189
- errors: [400, 403, 409, 500, 501, 503],
190
- supportsSelect: true,
191
- supportsInclude: true,
192
- supportsOmit: true,
193
- notes: 'Wraps findMany with total count. hasMore is reliable for forward offset pagination (skip + take) only. Distinct count over 100k falls back to approximate total. 409 possible on transaction conflict.',
194
- },
195
- create: {
196
- transport: 'POST JSON body',
197
- required: ['data'],
198
- optional: ['select', 'include', 'omit'],
199
- responseDesc: 'Created record (201)',
200
- errors: [400, 403, 409, 500, 501, 503],
201
- supportsSelect: true,
202
- supportsInclude: true,
203
- supportsOmit: true,
204
- notes: '409 on unique constraint violation.',
205
- },
206
- createMany: {
207
- transport: 'POST JSON body',
208
- required: ['data'],
209
- optional: ['skipDuplicates'],
210
- responseDesc: '{ count: number } (201)',
211
- errors: [400, 403, 409, 500, 501, 503],
212
- supportsSelect: false,
213
- supportsInclude: false,
214
- supportsOmit: false,
215
- notes: 'data is an array of scalar-only inputs. Nested relation writes are not supported. skipDuplicates silently ignores conflicts (not supported on all providers).',
216
- },
217
- createManyAndReturn: {
218
- transport: 'POST JSON body',
219
- required: ['data'],
220
- optional: ['skipDuplicates', 'select', 'include', 'omit'],
221
- responseDesc: 'Array of created records (201)',
222
- errors: [400, 403, 409, 500, 501, 503],
223
- supportsSelect: true,
224
- supportsInclude: true,
225
- supportsOmit: true,
226
- notes: 'Like createMany but returns created records. data items are scalar-only. Requires Prisma 5.14.0+, PostgreSQL/CockroachDB/SQLite only. The order of returned records is not guaranteed.',
227
- },
228
- update: {
229
- transport: 'PUT JSON body',
230
- required: ['where', 'data'],
231
- optional: ['select', 'include', 'omit'],
232
- responseDesc: 'Updated record',
233
- errors: [400, 403, 404, 409, 500, 501, 503],
234
- supportsSelect: true,
235
- supportsInclude: true,
236
- supportsOmit: true,
237
- notes: '404 when the record to update is not found. 409 on unique constraint violation or transaction conflict.',
238
- },
239
- updateMany: {
240
- transport: 'PUT JSON body',
241
- required: ['where', 'data'],
242
- optional: [],
243
- responseDesc: '{ count: number }',
244
- errors: [400, 403, 409, 500, 501, 503],
245
- supportsSelect: false,
246
- supportsInclude: false,
247
- supportsOmit: false,
248
- notes: 'Updates all matching records with scalar-only data. Nested relation writes are not supported. Returns count, not records. 409 on unique constraint violation.',
249
- },
250
- updateManyAndReturn: {
251
- transport: 'PUT JSON body',
252
- required: ['where', 'data'],
253
- optional: ['select', 'include', 'omit'],
254
- responseDesc: 'Array of updated records',
255
- errors: [400, 403, 409, 500, 501, 503],
256
- supportsSelect: true,
257
- supportsInclude: true,
258
- supportsOmit: true,
259
- notes: 'Like updateMany but returns updated records. data is scalar-only. Requires Prisma 6.2.0+, PostgreSQL/CockroachDB/SQLite only. 409 on unique constraint violation.',
260
- },
261
- upsert: {
262
- transport: 'PATCH JSON body',
263
- required: ['where', 'create', 'update'],
264
- optional: ['select', 'include', 'omit'],
265
- responseDesc: 'Created or updated record',
266
- errors: [400, 403, 409, 500, 501, 503],
267
- supportsSelect: true,
268
- supportsInclude: true,
269
- supportsOmit: true,
270
- notes: 'Creates if not found, updates if found.',
271
- },
272
- delete: {
273
- transport: 'DELETE JSON body',
274
- required: ['where'],
275
- optional: ['select', 'include', 'omit'],
276
- responseDesc: 'Deleted record',
277
- errors: [400, 403, 404, 500, 501, 503],
278
- supportsSelect: true,
279
- supportsInclude: true,
280
- supportsOmit: true,
281
- notes: '404 when the record to delete is not found.',
282
- },
283
- deleteMany: {
284
- transport: 'DELETE JSON body',
285
- required: ['where'],
286
- optional: [],
287
- responseDesc: '{ count: number }',
288
- errors: [400, 403, 500, 501, 503],
289
- supportsSelect: false,
290
- supportsInclude: false,
291
- supportsOmit: false,
292
- notes: 'Deletes all matching records. Returns count, not records.',
293
- },
294
- count: {
295
- transport: 'GET query params',
296
- required: [],
297
- optional: ['where', 'orderBy', 'cursor', 'take', 'skip', 'select'],
298
- responseDesc: 'Integer, or per-field count object when select is provided',
299
- errors: [400, 403, 500, 501, 503],
300
- supportsSelect: false,
301
- supportsInclude: false,
302
- supportsOmit: false,
303
- notes: 'select here means count-specific field selection, not record field selection.',
304
- },
305
- aggregate: {
306
- transport: 'GET query params',
307
- required: [],
308
- optional: ['where', 'orderBy', 'cursor', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
309
- responseDesc: 'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
310
- errors: [400, 403, 500, 501, 503],
311
- supportsSelect: false,
312
- supportsInclude: false,
313
- supportsOmit: false,
314
- notes: '_avg, _sum only apply to numeric fields.',
315
- },
316
- groupBy: {
317
- transport: 'GET query params',
318
- required: ['by'],
319
- optional: ['where', 'orderBy', 'having', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
320
- responseDesc: 'Array of objects, each with grouped field values and requested aggregates',
321
- errors: [400, 403, 500, 501, 503],
322
- supportsSelect: false,
323
- supportsInclude: false,
324
- supportsOmit: false,
325
- notes: 'by is a JSON-encoded array of scalar field names. orderBy is required when using skip or take. Response contains only the by-fields plus requested aggregates.',
326
- },
327
- }
328
-
329
- function exampleValue(fieldName: string): unknown {
330
- return EXAMPLE_VALUES[fieldName] ?? 'example'
331
- }
332
-
333
- function compoundWhereExample(): Record<string, any> | null {
334
- if (COMPOUND_ID) {
335
- const keyName = COMPOUND_ID.fields.join('_')
336
- const val: Record<string, unknown> = {}
337
- for (const f of COMPOUND_ID.fields) val[f] = exampleValue(f)
338
- return { [keyName]: val }
339
- }
340
- if (COMPOUND_UNIQUES.length > 0) {
341
- const u = COMPOUND_UNIQUES[0]
342
- const keyName = u.name || u.fields.join('_')
343
- const val: Record<string, unknown> = {}
344
- for (const f of u.fields) val[f] = exampleValue(f)
345
- return { [keyName]: val }
346
- }
347
- return null
348
- }
349
-
350
- function isOpenApiDisabled(disableOpenApi?: boolean) {
351
- if (disableOpenApi === true) return true
352
- if (disableOpenApi === false) return false
353
- return _env.DISABLE_OPENAPI === 'true' || _env.NODE_ENV === 'production'
354
- }
355
-
356
- function isPlaygroundAvailable(config: DocsConfig) {
357
- if (config.queryBuilder === false) return false
358
- if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
359
- if (_env.NODE_ENV === 'production') return false
360
- return true
361
- }
362
-
363
- function escapeHtml(input: string) {
364
- return input
365
- .replace(/&/g, '&amp;')
366
- .replace(/</g, '&lt;')
367
- .replace(/>/g, '&gt;')
368
- .replace(/"/g, '&quot;')
369
- .replace(/'/g, '&#039;')
370
- }
371
-
372
- function safeJsonForHtml(obj: unknown): string {
373
- return JSON.stringify(obj).replace(/</g, '\\\\u003c')
374
- }
375
-
376
- function renderScalar(_modelName: string, spec: unknown, title: string, cdnUrl?: string) {
377
- const scalarSrc = cdnUrl || DEFAULT_SCALAR_CDN
378
- return \`<!DOCTYPE html>
379
- <html lang="en">
380
- <head>
381
- <meta charset="utf-8" />
382
- <meta name="viewport" content="width=device-width, initial-scale=1" />
383
- <title>\${escapeHtml(title)}</title>
384
- </head>
385
- <body>
386
- <script id="api-reference" type="application/json">\${safeJsonForHtml(spec)}</script>
387
- <script src="\${escapeHtml(scalarSrc)}"></script>
388
- </body>
389
- </html>\`
390
- }
391
-
392
- function renderPlayground(modelName: string, config: DocsConfig) {
393
- const qbConfig = (typeof config.queryBuilder === 'object' && config.queryBuilder) ? config.queryBuilder : {}
394
- const host = qbConfig.host || 'localhost'
395
- const port = qbConfig.port || 5173
396
- const baseUrl = \`http://\${host}:\${port}\`
397
- const iframeSrc = \`\${baseUrl}?embedded=true&hideHeader=true\`
398
- const title = config.docsTitle || \`\${modelName} Query Playground\`
399
-
400
- const openApiLinks =
401
- '<a class="inline-block border border-gray-200 bg-white rounded-full py-1 px-2.5 text-[11px] no-underline text-inherit hover:border-gray-400" href="?ui=docs">Docs</a>' +
402
- '<a class="inline-block border border-gray-200 bg-white rounded-full py-1 px-2.5 text-[11px] no-underline text-inherit hover:border-gray-400" href="?ui=scalar">Scalar</a>' +
403
- '<a class="inline-block border border-gray-200 bg-white rounded-full py-1 px-2.5 text-[11px] no-underline text-inherit hover:border-gray-400" href="?ui=json">JSON</a>' +
404
- '<a class="inline-block border border-gray-200 bg-white rounded-full py-1 px-2.5 text-[11px] no-underline text-inherit hover:border-gray-400" href="?ui=yaml">YAML</a>' +
405
- '<a class="inline-block border border-gray-900 bg-gray-900 text-white rounded-full py-1 px-2.5 text-[11px] no-underline" href="?ui=playground">Playground</a>'
406
-
407
- return \`<!DOCTYPE html>
408
- <html lang="en">
409
- <head>
410
- <meta charset="utf-8" />
411
- <meta name="viewport" content="width=device-width, initial-scale=1" />
412
- <title>\${escapeHtml(title)}</title>
413
- <script src="https://cdn.tailwindcss.com"></script>
414
- </head>
415
- <body class="m-0 bg-white text-gray-900 font-sans">
416
- <div class="flex items-center justify-between px-4 py-2.5 border-b border-gray-200 bg-gray-50 flex-wrap gap-2">
417
- <div class="flex items-center gap-3">
418
- <span class="text-sm font-bold">Query Playground</span>
419
- <span class="text-xs text-gray-500 font-mono">\${escapeHtml(modelName)}</span>
420
- </div>
421
- <div class="flex gap-1.5 flex-wrap">\${openApiLinks}</div>
422
- </div>
423
- <div class="h-[calc(100vh-52px)] w-full" id="frame-container">
424
- <iframe
425
- id="qb-frame"
426
- class="w-full h-full border-none"
427
- src="\${escapeHtml(iframeSrc)}"
428
- sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
429
- loading="lazy"
430
- ></iframe>
431
- </div>
432
- <script>
433
- (function() {
434
- var frame = document.getElementById('qb-frame');
435
- var container = document.getElementById('frame-container');
436
- var timer = null;
437
-
438
- frame.addEventListener('error', showError);
439
-
440
- timer = setTimeout(function() {
441
- try {
442
- if (!frame.contentWindow || !frame.contentWindow.document) {
443
- showError();
444
- }
445
- } catch(e) {}
446
- }, 5000);
447
-
448
- frame.addEventListener('load', function() {
449
- if (timer) clearTimeout(timer);
450
- });
451
-
452
- function showError() {
453
- if (timer) clearTimeout(timer);
454
- container.innerHTML =
455
- '<div class="flex items-center justify-center h-[calc(100vh-52px)] text-gray-500 text-sm flex-col gap-3">' +
456
- '<div>Query builder is not running</div>' +
457
- '<code class="font-mono bg-gray-50 py-2 px-3.5 rounded-md text-[13px]">npx prisma-query-builder</code>' +
458
- '<div class="text-xs mt-1">Expected at: \${escapeHtml(baseUrl)}</div>' +
459
- '</div>';
460
- }
461
- })();
462
- </script>
463
- </body>
464
- </html>\`
199
+ const MODEL_CONTEXT: DocsModelContext = {
200
+ fields: MODEL_FIELDS,
201
+ enums: MODEL_ENUMS,
202
+ compoundId: COMPOUND_ID,
203
+ compoundUniques: COMPOUND_UNIQUES,
204
+ exampleValues: EXAMPLE_VALUES,
465
205
  }
466
206
 
467
- function isScalarField(f: FieldMeta) {
468
- return f.kind === 'scalar'
469
- }
470
-
471
- function isRelationField(f: FieldMeta) {
472
- return f.kind === 'object'
473
- }
474
-
475
- function isEnumField(f: FieldMeta) {
476
- return f.kind === 'enum'
477
- }
478
-
479
- function scalarFilterOperators(scalarType: string) {
480
- if (scalarType === 'String') {
481
- return [
482
- 'equals',
483
- 'in',
484
- 'notIn',
485
- 'lt',
486
- 'lte',
487
- 'gt',
488
- 'gte',
489
- 'contains',
490
- 'startsWith',
491
- 'endsWith',
492
- 'mode',
493
- 'not',
494
- ]
495
- }
496
-
497
- if (
498
- scalarType === 'Int' ||
499
- scalarType === 'BigInt' ||
500
- scalarType === 'Float' ||
501
- scalarType === 'Decimal'
502
- ) {
503
- return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
504
- }
505
-
506
- if (scalarType === 'DateTime') {
507
- return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
508
- }
509
-
510
- if (scalarType === 'Boolean') {
511
- return ['equals', 'not']
512
- }
513
-
514
- if (scalarType === 'Json') {
515
- return ['equals', 'path', 'string_contains', 'array_contains', 'not']
516
- }
517
-
518
- if (scalarType === 'Bytes') {
519
- return ['equals', 'in', 'notIn', 'not']
520
- }
521
-
522
- return ['equals', 'in', 'notIn', 'not']
523
- }
524
-
525
- function listFilterOperators() {
526
- return ['has', 'hasEvery', 'hasSome', 'isEmpty']
527
- }
528
-
529
- function whereFieldKind(f: FieldMeta) {
530
- if (isRelationField(f)) return f.isList ? 'relation-list' : 'relation-single'
531
- if (isEnumField(f)) return 'enum'
532
- if (isScalarField(f)) return f.isList ? 'scalar-list' : 'scalar'
533
- return 'unknown'
534
- }
535
-
536
- function whereFieldShape(f: FieldMeta) {
537
- const kind = whereFieldKind(f)
538
-
539
- if (kind === 'relation-list') {
540
- return '{ some?: RelatedWhere, every?: RelatedWhere, none?: RelatedWhere }'
541
- }
542
-
543
- if (kind === 'relation-single') {
544
- return '{ is?: RelatedWhere, isNot?: RelatedWhere }'
545
- }
546
-
547
- if (kind === 'scalar-list') {
548
- return '{ has?, hasEvery?, hasSome?, isEmpty? }'
549
- }
550
-
551
- if (kind === 'scalar') {
552
- return 'scalar value OR filter object'
553
- }
554
-
555
- if (kind === 'enum') {
556
- return 'enum value OR enum filter'
557
- }
558
-
559
- return 'n/a'
560
- }
561
-
562
- function describeFieldType(f: FieldMeta) {
563
- const base = String(f.type) + (f.isList ? '[]' : '')
564
- const optional = f.isRequired ? '' : ' (optional/nullable)'
565
- const flags = [
566
- f.isId ? 'id' : '',
567
- f.isUnique ? 'unique' : '',
568
- f.hasDefaultValue ? 'default' : '',
569
- f.isUpdatedAt ? 'updatedAt' : '',
570
- ].filter(Boolean)
571
- const suffix = flags.length ? ' [' + flags.join(', ') + ']' : ''
572
- return base + optional + suffix
573
- }
574
-
575
- function jsonBlock(v: unknown) {
576
- return '<pre class="my-2 rounded-xl !p-3 overflow-auto text-xs"><code class="language-json">' + escapeHtml(JSON.stringify(v, null, 2)) + '</code></pre>'
577
- }
578
-
579
- function codeBlock(text: string) {
580
- return '<pre class="my-2 rounded-xl !p-3 overflow-auto text-xs"><code class="language-javascript">' + escapeHtml(text) + '</code></pre>'
581
- }
582
-
583
- function anchors() {
584
- return [
585
- { id: 'ops', label: '1. Operations' },
586
- { id: 'transport', label: '2. Transport' },
587
- { id: 'args-read', label: '3. Read Args Reference' },
588
- { id: 'args-write', label: '4. Write Args Reference' },
589
- { id: 'where', label: '5. where' },
590
- { id: 'select-include', label: '6. select, include & omit' },
591
- { id: 'nested-writes', label: '7. Nested Writes' },
592
- { id: 'order', label: '8. orderBy / cursor / distinct' },
593
- { id: 'pagination', label: '9. Pagination' },
594
- { id: 'errors', label: '10. Error Handling' },
595
- { id: 'examples', label: '11. Examples' },
596
- { id: 'guard-shapes', label: '12. Guard Shapes' },
597
- { id: 'runtime', label: '13. Runtime Notes' },
598
- ]
599
- }
600
-
601
- function normalizeExamplePrefix(p: string): string {
602
- if (!p) return ''
603
- let result = p.replace(/\\/$/, '')
604
- if (result && !result.startsWith('/')) result = '/' + result
605
- return result
606
- }
607
-
608
- function buildExampleBasePath(modelName: string, config: DocsConfig) {
609
- const prefixSource = config.specBasePath ?? config.customUrlPrefix ?? ''
610
- const prefix = normalizeExamplePrefix(prefixSource)
611
- const modelPrefix = config.addModelPrefix !== false ? '/' + modelName.toLowerCase() : ''
612
- return prefix + modelPrefix
613
- }
614
-
615
- function buildFullPath(basePath: string, suffix: string) {
616
- if (!suffix) return basePath || '/'
617
- return basePath ? basePath + suffix : suffix
618
- }
619
-
620
- function renderDocs(modelName: string, config: DocsConfig) {
621
- const title = config.docsTitle || modelName + ' API'
622
- const generatedAt = new Date().toISOString()
623
-
624
- const modelLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
625
- const exampleBasePath = buildExampleBasePath(modelName, config)
626
-
627
- const scalarFields = MODEL_FIELDS.filter((f) => isScalarField(f) || isEnumField(f))
628
- const relationFields = MODEL_FIELDS.filter((f) => isRelationField(f))
629
-
630
- const uniqueFields = MODEL_FIELDS.filter((f) => f.isId || f.isUnique)
631
- const requiredCreateFields = scalarFields.filter((sf) => sf.isRequired && !sf.hasDefaultValue && !sf.isUpdatedAt)
632
- const listRelations = relationFields.filter((f) => f.isList)
633
- const singleRelations = relationFields.filter((f) => !f.isList)
634
-
635
- const ops = OPERATION_DEFS
636
- .filter((d) => isOperationEnabled(config as Record<string, any>, d))
637
- .map((d) => {
638
- const detail = OP_DETAIL_MAP[d.name]
639
- return {
640
- op: d.name,
641
- method: d.method.toUpperCase(),
642
- path: buildFullPath(exampleBasePath, d.pathSuffix),
643
- transport: detail ? detail.transport : d.method === 'get' ? 'GET query params' : 'JSON body',
644
- responseDesc: detail ? detail.responseDesc : '',
645
- errors: detail ? detail.errors.join(', ') : '',
646
- required: detail ? detail.required : [],
647
- optional: detail ? detail.optional : [],
648
- supportsSelect: detail ? detail.supportsSelect : false,
649
- supportsInclude: detail ? detail.supportsInclude : false,
650
- supportsOmit: detail ? detail.supportsOmit : false,
651
- notes: detail ? detail.notes : '',
652
- }
653
- })
654
-
655
- const firstUnique = uniqueFields[0]
656
- const firstUniqueExample = firstUnique ? exampleValue(firstUnique.name) : null
657
- const compoundWhere = !firstUnique ? compoundWhereExample() : null
658
- const uniqueWhereExample = firstUnique
659
- ? { [firstUnique.name]: firstUniqueExample }
660
- : compoundWhere
661
-
662
- const firstFilterFieldName = firstUnique
663
- ? firstUnique.name
664
- : (COMPOUND_ID ? COMPOUND_ID.fields[0] : null)
665
- || (COMPOUND_UNIQUES.length > 0 ? COMPOUND_UNIQUES[0].fields[0] : null)
666
-
667
- const firstStringField = scalarFields.find((f) => f.type === 'String')
668
- const firstBooleanField = scalarFields.find((f) => f.type === 'Boolean')
669
-
670
- const whereExample: Record<string, any> = {}
671
- const andClauses: Record<string, any>[] = []
672
- if (firstFilterFieldName) {
673
- andClauses.push({ [firstFilterFieldName]: { equals: exampleValue(firstFilterFieldName) } })
674
- }
675
- if (firstStringField) {
676
- andClauses.push({ [firstStringField.name]: { contains: 'example', mode: 'insensitive' } })
677
- }
678
- if (andClauses.length > 0) {
679
- whereExample.AND = andClauses
680
- }
681
- if (firstBooleanField) {
682
- whereExample.OR = [{ [firstBooleanField.name]: { equals: true } }]
683
- }
684
-
685
- const selectExample: any = {}
686
- for (const f of scalarFields.slice(0, 10)) selectExample[f.name] = true
687
-
688
- const includeExample: any = {}
689
- for (const f of relationFields.slice(0, 6)) includeExample[f.name] = true
690
-
691
- const omitExample: any = {}
692
- const omitCandidates = scalarFields.filter((f) => !f.isId && !f.isUnique)
693
- for (const f of omitCandidates.slice(0, 3)) omitExample[f.name] = true
694
-
695
- const orderByField = firstUnique
696
- ? firstUnique.name
697
- : firstFilterFieldName
698
-
699
- const findManyQueryArgs: any = {
700
- where: whereExample,
701
- select: selectExample,
702
- orderBy: orderByField ? { [orderByField]: 'asc' } : undefined,
703
- take: 20,
704
- skip: 0,
705
- }
706
- if (!findManyQueryArgs.orderBy) delete findManyQueryArgs.orderBy
707
-
708
- const findManyFetchExample =
709
- 'import { encodeQueryParams } from "./client/encodeQueryParams"\\n\\n' +
710
- 'const params = encodeQueryParams(' + JSON.stringify(findManyQueryArgs, null, 2) + ')\\n\\n' +
711
- 'const res = await fetch(BASE_URL + "' + exampleBasePath + '?" + params)\\n' +
712
- 'const data = await res.json()'
713
-
714
- const findUniqueFetchExample = uniqueWhereExample
715
- ? 'const params = encodeQueryParams({\\n' +
716
- ' where: ' + JSON.stringify(uniqueWhereExample) + ',\\n' +
717
- ' include: ' + JSON.stringify(includeExample) + '\\n' +
718
- '})\\n\\n' +
719
- 'const res = await fetch(BASE_URL + "' + exampleBasePath + '/unique?" + params)\\n' +
720
- 'const data = await res.json()'
721
- : null
722
-
723
- const createBodyExample: any = { data: {} }
724
- for (const f of requiredCreateFields.slice(0, 5)) {
725
- createBodyExample.data[f.name] = exampleValue(f.name)
726
- }
727
-
728
- const createFetchExample =
729
- 'const res = await fetch(BASE_URL + "' + exampleBasePath + '", {\\n' +
730
- ' method: "POST",\\n' +
731
- ' headers: { "Content-Type": "application/json" },\\n' +
732
- ' body: JSON.stringify(' + JSON.stringify(createBodyExample, null, 2) + ')\\n' +
733
- '})\\n' +
734
- 'const created = await res.json()'
735
-
736
- const updateBodyExample: any = uniqueWhereExample
737
- ? { where: uniqueWhereExample, data: {} }
738
- : null
739
- if (updateBodyExample) {
740
- const firstEditableString = scalarFields.find((sf) => sf.type === 'String' && !sf.isId)
741
- if (firstEditableString) {
742
- updateBodyExample.data[firstEditableString.name] = 'updated'
743
- }
744
- }
745
-
746
- const updateFetchExample = updateBodyExample
747
- ? 'const res = await fetch(BASE_URL + "' + exampleBasePath + '", {\\n' +
748
- ' method: "PUT",\\n' +
749
- ' headers: { "Content-Type": "application/json" },\\n' +
750
- ' body: JSON.stringify(' + JSON.stringify(updateBodyExample, null, 2) + ')\\n' +
751
- '})\\n' +
752
- 'const updated = await res.json()'
753
- : null
754
-
755
- const deleteFetchExample = uniqueWhereExample
756
- ? 'const res = await fetch(BASE_URL + "' + exampleBasePath + '", {\\n' +
757
- ' method: "DELETE",\\n' +
758
- ' headers: { "Content-Type": "application/json" },\\n' +
759
- ' body: JSON.stringify({\\n' +
760
- ' where: ' + JSON.stringify(uniqueWhereExample) + '\\n' +
761
- ' })\\n' +
762
- '})\\n' +
763
- 'const deleted = await res.json()'
764
- : null
765
-
766
- const guardVariantHeader = config.guard?.variantHeader || 'x-api-variant'
767
-
768
- const guardFetchExample = uniqueWhereExample
769
- ? 'const params = encodeQueryParams({\\n' +
770
- ' where: ' + JSON.stringify(uniqueWhereExample) + '\\n' +
771
- '})\\n\\n' +
772
- 'const res = await fetch(BASE_URL + "' + exampleBasePath + '/unique?" + params, {\\n' +
773
- ' headers: { "' + guardVariantHeader + '": "admin" }\\n' +
774
- '})\\n' +
775
- 'const data = await res.json()'
776
- : null
777
-
778
- const writeFieldRows = MODEL_FIELDS.map((f) => {
779
- let writeContract = ''
780
- if (isRelationField(f)) {
781
- writeContract = f.isList ? 'Nested list write object' : 'Nested single write object'
782
- } else if (f.hasDefaultValue || f.isUpdatedAt) {
783
- writeContract = 'Optional on create (has default)'
784
- } else if (f.isRequired) {
785
- writeContract = 'Required on create'
786
- } else {
787
- writeContract = 'Optional (nullable)'
788
- }
789
- return {
790
- name: f.name,
791
- type: describeFieldType(f),
792
- writeContract,
793
- }
794
- })
795
-
796
- const argsReferenceRead = {
797
- findMany: {
798
- where: 'WhereInput',
799
- select: 'Select',
800
- include: 'Include',
801
- omit: 'Omit',
802
- orderBy: 'OrderByInput | OrderByInput[]',
803
- cursor: 'UniqueInput',
804
- take: 'number',
805
- skip: 'number',
806
- distinct: 'ScalarFieldEnum | ScalarFieldEnum[]',
807
- },
808
- findUnique: {
809
- where: 'UniqueInput (required)',
810
- select: 'Select',
811
- include: 'Include',
812
- omit: 'Omit',
813
- },
814
- findFirst: {
815
- where: 'WhereInput',
816
- select: 'Select',
817
- include: 'Include',
818
- omit: 'Omit',
819
- orderBy: 'OrderByInput | OrderByInput[]',
820
- cursor: 'UniqueInput',
821
- take: 'number',
822
- skip: 'number',
823
- distinct: 'ScalarFieldEnum | ScalarFieldEnum[]',
824
- },
825
- count: {
826
- where: 'WhereInput',
827
- orderBy: 'OrderByInput | OrderByInput[]',
828
- cursor: 'UniqueInput',
829
- take: 'number',
830
- skip: 'number',
831
- select: 'true | { _all?: true, fieldName?: true, ... }',
832
- },
833
- aggregate: {
834
- where: 'WhereInput',
835
- orderBy: 'OrderByInput | OrderByInput[]',
836
- cursor: 'UniqueInput',
837
- take: 'number',
838
- skip: 'number',
839
- _count: 'true | CountAggregateInput',
840
- _avg: 'AvgAggregateInput',
841
- _sum: 'SumAggregateInput',
842
- _min: 'MinAggregateInput',
843
- _max: 'MaxAggregateInput',
844
- },
845
- groupBy: {
846
- by: 'ScalarFieldEnum[] (required)',
847
- where: 'WhereInput',
848
- orderBy: 'OrderByWithAggregationInput | OrderByWithAggregationInput[] (required when using skip or take)',
849
- having: 'ScalarWhereWithAggregatesInput',
850
- take: 'number',
851
- skip: 'number',
852
- _count: 'true | CountAggregateInput',
853
- _avg: 'AvgAggregateInput',
854
- _sum: 'SumAggregateInput',
855
- _min: 'MinAggregateInput',
856
- _max: 'MaxAggregateInput',
857
- },
858
- }
859
-
860
- const argsReferenceWrite = {
861
- create: {
862
- data: modelName + 'CreateInput (required)',
863
- select: 'Select',
864
- include: 'Include',
865
- omit: 'Omit',
866
- },
867
- createMany: {
868
- data: modelName + 'CreateManyInput[] (required, scalar-only)',
869
- skipDuplicates: 'boolean',
870
- },
871
- createManyAndReturn: {
872
- data: modelName + 'CreateManyInput[] (required, scalar-only)',
873
- skipDuplicates: 'boolean',
874
- select: 'Select',
875
- include: 'Include',
876
- omit: 'Omit',
877
- },
878
- update: {
879
- where: 'UniqueInput (required)',
880
- data: modelName + 'UpdateInput (required)',
881
- select: 'Select',
882
- include: 'Include',
883
- omit: 'Omit',
884
- },
885
- updateMany: {
886
- where: 'WhereInput (required)',
887
- data: modelName + 'UpdateManyMutationInput (required, scalar-only)',
888
- },
889
- updateManyAndReturn: {
890
- where: 'WhereInput (required)',
891
- data: modelName + 'UpdateManyMutationInput (required, scalar-only)',
892
- select: 'Select',
893
- include: 'Include',
894
- omit: 'Omit',
895
- },
896
- upsert: {
897
- where: 'UniqueInput (required)',
898
- create: modelName + 'CreateInput (required)',
899
- update: modelName + 'UpdateInput (required)',
900
- select: 'Select',
901
- include: 'Include',
902
- omit: 'Omit',
903
- },
904
- delete: {
905
- where: 'UniqueInput (required)',
906
- select: 'Select',
907
- include: 'Include',
908
- omit: 'Omit',
909
- },
910
- deleteMany: {
911
- where: 'WhereInput (required)',
912
- },
913
- }
914
-
915
- const transportNotes = [
916
- 'GET endpoints: Prisma args as JSON-encoded query parameter strings via encodeQueryParams.',
917
- 'POST/PUT/DELETE/PATCH endpoints: Prisma args as JSON request body. Body must be a JSON object.',
918
- 'findManyPaginated returns { data, total, hasMore }. hasMore is reliable for forward offset pagination only.',
919
- 'Batch mutations (createMany, updateMany, deleteMany) return { count }. Batch data inputs are scalar-only — nested relation writes are not supported.',
920
- 'findUnique and findFirst return null (not 404) when no record matches. Use the OrThrow variants for 404 behavior.',
921
- 'createManyAndReturn requires Prisma 5.14.0+, updateManyAndReturn requires Prisma 6.2.0+. Both are limited to PostgreSQL, CockroachDB, and SQLite.',
922
- ]
923
-
924
- const errorRows = [
925
- { status: '400', description: 'Bad request', causes: 'Invalid JSON body, invalid query parameters, query validation failure, guard shape rejection, field value out of range, non-object request body.' },
926
- { status: '403', description: 'Forbidden', causes: 'Guard policy rejected the operation.' },
927
- { status: '404', description: 'Not found', causes: 'Record not found. Only from OrThrow operations, update, and delete.' },
928
- { status: '409', description: 'Conflict', causes: 'Unique constraint violation on create/update/upsert, or transaction conflict (e.g. in findManyPaginated).' },
929
- { status: '500', description: 'Internal server error', causes: 'Database error, table/column missing, raw query failure, or unhandled error.' },
930
- { status: '501', description: 'Not implemented', causes: 'Database provider does not support the requested feature.' },
931
- { status: '503', description: 'Service unavailable', causes: 'Database connection pool timeout.' },
932
- ]
933
-
934
- const hasPlayground = isPlaygroundAvailable(config)
935
-
936
- const chipClass = 'inline-block border border-gray-200 bg-gray-50 rounded-full py-[5px] px-2.5 text-xs no-underline text-inherit hover:border-gray-400'
937
-
938
- const openApiLinks =
939
- '<a class="' + chipClass + ' !bg-gray-900 !text-white !border-gray-900" href="?ui=docs">Docs</a>' +
940
- '<a class="' + chipClass + '" href="?ui=scalar">Scalar</a>' +
941
- '<a class="' + chipClass + '" href="?ui=json">JSON</a>' +
942
- '<a class="' + chipClass + '" href="?ui=yaml">YAML</a>' +
943
- (hasPlayground ? '<a class="' + chipClass + '" href="?ui=playground">Playground</a>' : '')
944
-
945
- const tocHtml = '<ol class="m-0 pl-[18px]">' + anchors().map((a) => '<li class="my-1.5"><a href="#' + escapeHtml(a.id) + '" class="text-inherit">' + escapeHtml(a.label) + '</a></li>').join('') + '</ol>'
946
-
947
- const whereRows = MODEL_FIELDS.map((f) => {
948
- const kind = whereFieldKind(f)
949
- const shape = whereFieldShape(f)
950
- const filterOps =
951
- kind === 'scalar'
952
- ? scalarFilterOperators(String(f.type)).map((x) => '<span class="inline-block border border-gray-200 bg-gray-50 rounded-full py-0.5 px-2 text-[11px] mr-1.5 mb-0.5 font-mono">' + escapeHtml(x) + '</span>').join(' ')
953
- : kind === 'enum'
954
- ? ['equals', 'in', 'notIn', 'not'].map((x) => '<span class="inline-block border border-gray-200 bg-gray-50 rounded-full py-0.5 px-2 text-[11px] mr-1.5 mb-0.5 font-mono">' + escapeHtml(x) + '</span>').join(' ')
955
- : kind === 'scalar-list'
956
- ? listFilterOperators().map((x) => '<span class="inline-block border border-gray-200 bg-gray-50 rounded-full py-0.5 px-2 text-[11px] mr-1.5 mb-0.5 font-mono">' + escapeHtml(x) + '</span>').join(' ')
957
- : kind === 'relation-single'
958
- ? ['is', 'isNot'].map((x) => '<span class="inline-block border border-gray-200 bg-gray-50 rounded-full py-0.5 px-2 text-[11px] mr-1.5 mb-0.5 font-mono">' + escapeHtml(x) + '</span>').join(' ')
959
- : kind === 'relation-list'
960
- ? ['some', 'every', 'none'].map((x) => '<span class="inline-block border border-gray-200 bg-gray-50 rounded-full py-0.5 px-2 text-[11px] mr-1.5 mb-0.5 font-mono">' + escapeHtml(x) + '</span>').join(' ')
961
- : '<span class="text-gray-500">n/a</span>'
962
-
963
- const doc = f.documentation ? '<div class="text-gray-500 mt-1.5">' + escapeHtml(String(f.documentation)) + '</div>' : ''
964
-
965
- return (
966
- '<tr>' +
967
- '<td class="text-left p-2 border-b border-gray-200 align-top font-mono">' + escapeHtml(f.name) + '</td>' +
968
- '<td class="text-left p-2 border-b border-gray-200 align-top font-mono">' + escapeHtml(kind) + '</td>' +
969
- '<td class="text-left p-2 border-b border-gray-200 align-top font-mono">' + escapeHtml(describeFieldType(f)) + '</td>' +
970
- '<td class="text-left p-2 border-b border-gray-200 align-top"><div class="text-gray-500 font-mono">' + escapeHtml(shape) + '</div><div class="mt-2">' + filterOps + '</div>' + doc + '</td>' +
971
- '</tr>'
972
- )
973
- }).join('')
974
-
975
- const whereCoreShapes = {
976
- where: {
977
- AND: ['WhereInput', 'WhereInput[]'],
978
- OR: ['WhereInput[]'],
979
- NOT: ['WhereInput', 'WhereInput[]'],
980
- field: 'ScalarFilter | scalar value | RelationFilter | EnumFilter',
981
- },
982
- }
983
-
984
- const selectRules = [
985
- 'select and include cannot be used together at the same level.',
986
- 'omit cannot be used together with select at the same level. omit can be used with include.',
987
- 'select: booleans for scalars, nested objects for relations.',
988
- 'include: booleans for relations, nested include/select for deep loading.',
989
- 'omit: booleans for scalar fields to exclude from the response.',
990
- ]
991
-
992
- const orderRules = [
993
- 'orderBy: { field: "asc" | "desc" } or array for multiple.',
994
- 'cursor: unique selector, e.g. { id: 123 }.',
995
- 'distinct: scalar field names, e.g. ["email", "status"].',
996
- 'take/skip applied after cursor/orderBy.',
997
- ]
998
-
999
- const paginationNotes = [
1000
- 'findMany and findManyPaginated support offset pagination via take and skip.',
1001
- 'Both also support cursor-based pagination via cursor + take.',
1002
- 'findManyPaginated wraps findMany with a total count query and returns { data, total, hasMore }.',
1003
- 'hasMore is reliable for forward offset pagination (skip + take) only. With cursor pagination or negative take, hasMore may be inaccurate.',
1004
- 'When the server config sets pagination.defaultLimit, take is automatically applied to findMany and findManyPaginated if omitted.',
1005
- 'When the server config sets pagination.maxLimit, take is capped by absolute value to that limit for findMany and findManyPaginated. This applies to both positive and negative take values.',
1006
- 'Clients cannot detect these server-side limits from the API alone. Check with the API provider for configured limits.',
1007
- ]
1008
-
1009
- const nestedWriteListOps = [
1010
- { key: 'create', desc: 'Create new related records inline. Accepts a single object or array.' },
1011
- { key: 'connect', desc: 'Connect existing records by unique identifier. Accepts a single object or array.' },
1012
- { key: 'connectOrCreate', desc: 'Connect if exists, create if not. Each item: { where, create }.' },
1013
- { key: 'createMany', desc: 'Bulk create related records. Shape: { data: [...], skipDuplicates?: boolean }.' },
1014
- { key: 'set', desc: 'Replace all connections. Provide an array of unique identifiers.' },
1015
- { key: 'disconnect', desc: 'Disconnect related records without deleting them.' },
1016
- { key: 'delete', desc: 'Delete related records by unique identifier.' },
1017
- { key: 'update', desc: 'Update related records. Each item: { where, data }.' },
1018
- { key: 'updateMany', desc: 'Bulk update related records matching a filter. Each item: { where, data }.' },
1019
- { key: 'deleteMany', desc: 'Bulk delete related records matching a filter.' },
1020
- { key: 'upsert', desc: 'Create or update related records. Each item: { where, create, update }.' },
1021
- ]
1022
-
1023
- const nestedWriteSingleOps = [
1024
- { key: 'create', desc: 'Create a new related record inline.' },
1025
- { key: 'connect', desc: 'Connect an existing record by unique identifier.' },
1026
- { key: 'connectOrCreate', desc: 'Connect if exists, create if not. Shape: { where, create }.' },
1027
- { key: 'disconnect', desc: 'Disconnect the related record (set relation to null). Pass true.' },
1028
- { key: 'delete', desc: 'Delete the related record. Pass true.' },
1029
- { key: 'update', desc: 'Update the related record inline with update input.' },
1030
- { key: 'upsert', desc: 'Create the related record if it does not exist, update if it does. Shape: { create, update }.' },
1031
- ]
1032
-
1033
- const guardShapeInfo = [
1034
- 'When a guard shape is configured on an operation, prisma-guard validates and enforces allowed query patterns before the query reaches the database.',
1035
- 'Named shapes route to different guard configs based on a caller value resolved from the <span class="font-mono">' + escapeHtml(guardVariantHeader) + '</span> header or a custom resolver function.',
1036
- 'Forced values (literals instead of true) are injected server-side and cannot be overridden by the client.',
1037
- ]
1038
-
1039
- const runtimeNotes = [
1040
- '<strong>Query parameter parsing:</strong> GET query values are parsed server-side. Strings starting with <span class="font-mono">{</span>, <span class="font-mono">[</span>, or <span class="font-mono">\\"</span> are JSON-parsed. The strings <span class="font-mono">true</span>, <span class="font-mono">false</span>, <span class="font-mono">null</span> are converted to their JS equivalents. Numeric conversion only applies to <span class="font-mono">take</span> and <span class="font-mono">skip</span>. Use <span class="font-mono">encodeQueryParams</span> to avoid encoding issues.',
1041
- '<strong>Request body validation:</strong> All write endpoints require a JSON object body. Sending <span class="font-mono">null</span>, arrays, or non-object JSON values returns 400.',
1042
- '<strong>Documentation in production:</strong> Docs endpoints are disabled by default when <span class="font-mono">NODE_ENV=production</span> or <span class="font-mono">DISABLE_OPENAPI=true</span>. To enable in production, set <span class="font-mono">disableOpenApi: false</span> in the route config.',
1043
- '<strong>Paginated query atomicity:</strong> findManyPaginated wraps data + count in a database transaction when available. If interactive transactions are not supported (e.g. some edge adapters), the queries run separately and data/total may be slightly inconsistent under concurrent writes.',
1044
- '<strong>Distinct count approximation:</strong> When findManyPaginated is used with distinct and the number of unique values exceeds 100,000, the total falls back to a non-distinct count which may overcount. The hasMore value is affected accordingly.',
1045
- '<strong>Serialization:</strong> BigInt values are serialized as strings. Bytes/Buffer values are serialized as base64 strings. Decimal values are serialized as strings. DateTime values are serialized as ISO 8601 strings.',
1046
- '<strong>Playground:</strong> The query playground embeds an iframe to a local prisma-query-builder-ui instance. It connects to your real database using the configured DATABASE_URL. It is disabled in production and when queryBuilder is set to false or queryBuilder.enabled is set to false.',
1047
- '<strong>Prototype pollution protection:</strong> All incoming JSON bodies and query parameters are sanitized to reject __proto__, constructor, and prototype keys.',
1048
- '<strong>Batch operation safety:</strong> deleteMany, updateMany, and updateManyAndReturn require a where field in the request body. Requests without where are rejected with 400 to prevent accidental mass operations.',
1049
- '<strong>Bulk write constraints:</strong> createMany, createManyAndReturn, updateMany, and updateManyAndReturn accept scalar-only data inputs. Nested relation writes are not supported in these operations.',
1050
- '<strong>Provider compatibility:</strong> createManyAndReturn requires Prisma 5.14.0+ and is limited to PostgreSQL, CockroachDB, and SQLite. updateManyAndReturn requires Prisma 6.2.0+ with the same provider restrictions. skipDuplicates is not supported on all database providers.',
1051
- '<strong>omit compatibility:</strong> The omit parameter requires Prisma 5.13.0+ (preview) or 6.2.0+ (GA). On older Prisma versions, requests using omit will return 400.',
1052
- ]
1053
-
1054
- const noUniqueFieldNote = '<div class="bg-gray-50 border border-gray-200 rounded-xl p-3 text-xs text-gray-500">This model has no unique or id fields suitable for a generated example. Use the unique constraint from your schema.</div>'
1055
-
1056
- const thClass = 'text-left p-2 border-b border-gray-200 align-top font-black'
1057
- const tdClass = 'text-left p-2 border-b border-gray-200 align-top'
1058
- const calloutClass = 'bg-gray-50 border border-gray-200 rounded-xl p-3'
1059
-
1060
- const html = \`<!DOCTYPE html>
1061
- <html lang="en">
1062
- <head>
1063
- <meta charset="utf-8" />
1064
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1065
- <title>\${escapeHtml(title)}</title>
1066
- <script src="https://cdn.tailwindcss.com"></script>
1067
- <link rel="stylesheet" href="\${PRISM_CSS}" />
1068
- <style>
1069
- .example :not(pre)>code[class*=language-], .example pre[class*=language-] { border: 1px solid rgb(229 230 234); }
1070
- :not(pre)>code[class*=language-], pre[class*=language-] { background: #f9fafb; }
1071
- pre[class*="language-"] { border-radius: 12px; padding: 12px; margin: 8px 0; font-size: 12px; }
1072
- code[class*="language-"] { font-size: 12px; }
1073
- </style>
1074
- </head>
1075
- <body class="m-0 bg-white text-gray-900 font-sans leading-relaxed">
1076
- <div class="max-w-[1120px] mx-auto px-5 pt-[30px] pb-20">
1077
- <div class="border-b-2 border-gray-900 pb-3 mb-[18px] flex gap-3.5 items-start justify-between flex-wrap">
1078
- <div class="min-w-[280px]">
1079
- <div class="text-xl font-black">\${escapeHtml(title)}</div>
1080
- <div class="mt-2 text-xs text-gray-500">
1081
- <span class="font-mono">\${escapeHtml(modelLower)}</span>
1082
- <span class="mx-2">·</span>
1083
- <span>\${escapeHtml(generatedAt)}</span>
1084
- </div>
1085
- </div>
1086
- <div class="flex gap-2 flex-wrap items-center pt-0.5">\${openApiLinks}</div>
1087
- </div>
1088
-
1089
- <div class="\${calloutClass}">\${tocHtml}</div>
1090
-
1091
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="ops">1. Operations</h2>
1092
- <div class="overflow-x-auto">
1093
- <table class="w-full border-collapse text-xs">
1094
- <thead>
1095
- <tr>
1096
- <th class="\${thClass}">Operation</th>
1097
- <th class="\${thClass}">Method</th>
1098
- <th class="\${thClass}">Path</th>
1099
- <th class="\${thClass}">Transport</th>
1100
- <th class="\${thClass}">Required Args</th>
1101
- <th class="\${thClass}">Response</th>
1102
- <th class="\${thClass}">Errors</th>
1103
- <th class="\${thClass}">Notes</th>
1104
- </tr>
1105
- </thead>
1106
- <tbody>
1107
- \${ops.map((o) => \`
1108
- <tr>
1109
- <td class="\${tdClass} font-mono">\${escapeHtml(o.op)}</td>
1110
- <td class="\${tdClass} font-mono">\${escapeHtml(o.method)}</td>
1111
- <td class="\${tdClass} font-mono">\${escapeHtml(o.path)}</td>
1112
- <td class="\${tdClass}">\${escapeHtml(o.transport)}</td>
1113
- <td class="\${tdClass} font-mono">\${o.required.length > 0 ? escapeHtml(o.required.join(', ')) : '<span class="text-gray-400">none</span>'}</td>
1114
- <td class="\${tdClass}">\${escapeHtml(o.responseDesc)}</td>
1115
- <td class="\${tdClass} font-mono">\${escapeHtml(o.errors)}</td>
1116
- <td class="\${tdClass} text-gray-500">\${escapeHtml(o.notes)}</td>
1117
- </tr>
1118
- \`).join('')}
1119
- </tbody>
1120
- </table>
1121
- </div>
1122
-
1123
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="transport">2. Transport</h2>
1124
- <div class="\${calloutClass}">
1125
- <ul class="text-[13px]">\${transportNotes.map((t) => '<li>' + escapeHtml(t) + '</li>').join('')}</ul>
1126
- </div>
1127
-
1128
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="args-read">3. Read Args Reference</h2>
1129
- <div class="grid grid-cols-2 gap-3.5 max-[920px]:grid-cols-1">
1130
- <div>
1131
- <h3 class="mt-3.5 mb-2 text-sm">3.1 Read operations</h3>
1132
- <div class="\${calloutClass}">
1133
- \${jsonBlock(argsReferenceRead)}
1134
- </div>
1135
- </div>
1136
- <div>
1137
- <h3 class="mt-3.5 mb-2 text-sm">3.2 Rules</h3>
1138
- <div class="\${calloutClass}">
1139
- <ul class="text-[13px]">
1140
- <li><span class="font-mono">where</span> — filtering</li>
1141
- <li><span class="font-mono">select</span> — field selection</li>
1142
- <li><span class="font-mono">include</span> — relation loading</li>
1143
- <li><span class="font-mono">omit</span> — exclude fields from response</li>
1144
- <li><span class="font-mono">select</span> and <span class="font-mono">include</span> cannot be combined at the same level</li>
1145
- <li><span class="font-mono">select</span> and <span class="font-mono">omit</span> cannot be combined at the same level</li>
1146
- <li><span class="font-mono">orderBy</span>, <span class="font-mono">cursor</span>, <span class="font-mono">take</span>, <span class="font-mono">skip</span>, <span class="font-mono">distinct</span> — pagination and ordering</li>
1147
- <li><span class="font-mono">groupBy</span>: <span class="font-mono">orderBy</span> is required when using <span class="font-mono">skip</span> or <span class="font-mono">take</span></li>
1148
- </ul>
1149
- </div>
1150
- </div>
1151
- </div>
1152
-
1153
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="args-write">4. Write Args Reference</h2>
1154
- <div class="grid grid-cols-2 gap-3.5 max-[920px]:grid-cols-1">
1155
- <div>
1156
- <h3 class="mt-3.5 mb-2 text-sm">4.1 Write operations</h3>
1157
- <div class="\${calloutClass}">
1158
- \${jsonBlock(argsReferenceWrite)}
1159
- </div>
1160
- </div>
1161
- <div>
1162
- <h3 class="mt-3.5 mb-2 text-sm">4.2 Field write contract</h3>
1163
- <div class="overflow-x-auto">
1164
- <table class="w-full border-collapse text-xs">
1165
- <thead>
1166
- <tr>
1167
- <th class="\${thClass}">Field</th>
1168
- <th class="\${thClass}">Type</th>
1169
- <th class="\${thClass}">Write Behavior</th>
1170
- </tr>
1171
- </thead>
1172
- <tbody>
1173
- \${writeFieldRows.map((r) => \`
1174
- <tr>
1175
- <td class="\${tdClass} font-mono">\${escapeHtml(r.name)}</td>
1176
- <td class="\${tdClass} font-mono">\${escapeHtml(r.type)}</td>
1177
- <td class="\${tdClass}">\${escapeHtml(r.writeContract)}</td>
1178
- </tr>
1179
- \`).join('')}
1180
- </tbody>
1181
- </table>
1182
- </div>
1183
- </div>
1184
- </div>
1185
-
1186
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="where">5. where</h2>
1187
-
1188
- <div class="grid grid-cols-2 gap-3.5 max-[920px]:grid-cols-1">
1189
- <div>
1190
- <h3 class="mt-3.5 mb-2 text-sm">5.1 Boolean composition</h3>
1191
- <div class="\${calloutClass}">
1192
- \${jsonBlock(whereCoreShapes)}
1193
- </div>
1194
- </div>
1195
- <div>
1196
- <h3 class="mt-3.5 mb-2 text-sm">5.2 Example</h3>
1197
- <div class="\${calloutClass}">
1198
- \${jsonBlock(whereExample)}
1199
- </div>
1200
- </div>
1201
- </div>
1202
-
1203
- <h3 class="mt-3.5 mb-2 text-sm">5.3 Per-field filters</h3>
1204
-
1205
- <table class="w-full border-collapse text-xs">
1206
- <thead>
1207
- <tr>
1208
- <th class="\${thClass}">Field</th>
1209
- <th class="\${thClass}">Kind</th>
1210
- <th class="\${thClass}">Type</th>
1211
- <th class="\${thClass}">Accepted filters</th>
1212
- </tr>
1213
- </thead>
1214
- <tbody>\${whereRows}</tbody>
1215
- </table>
1216
-
1217
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="select-include">6. select, include & omit</h2>
1218
- <div class="grid grid-cols-2 gap-3.5 max-[920px]:grid-cols-1">
1219
- <div>
1220
- <h3 class="mt-3.5 mb-2 text-sm">6.1 Rules</h3>
1221
- <div class="\${calloutClass}">
1222
- <ul class="text-[13px]">\${selectRules.map((r) => '<li>' + escapeHtml(r) + '</li>').join('')}</ul>
1223
- </div>
1224
- </div>
1225
- <div>
1226
- <h3 class="mt-3.5 mb-2 text-sm">6.2 Examples</h3>
1227
- <div class="\${calloutClass}">
1228
- <div class="text-gray-500 text-xs mb-1">select</div>
1229
- \${jsonBlock(selectExample)}
1230
- <div class="text-gray-500 text-xs mb-1 mt-2">include</div>
1231
- \${jsonBlock(includeExample)}
1232
- <div class="text-gray-500 text-xs mb-1 mt-2">omit</div>
1233
- \${jsonBlock(omitExample)}
1234
- </div>
1235
- </div>
1236
- </div>
1237
-
1238
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="nested-writes">7. Nested Writes</h2>
1239
- <div class="grid grid-cols-2 gap-3.5 max-[920px]:grid-cols-1">
1240
- <div>
1241
- <h3 class="mt-3.5 mb-2 text-sm">7.1 List relation operations</h3>
1242
- <div class="\${calloutClass}">
1243
- \${listRelations.length > 0
1244
- ? '<div class="text-xs text-gray-500 mb-2">Relations: ' + listRelations.map((f) => '<span class="font-mono">' + escapeHtml(f.name) + '</span> (' + escapeHtml(String(f.type)) + '[])').join(', ') + '</div>'
1245
- : '<div class="text-xs text-gray-500 mb-2">This model has no list relations.</div>'}
1246
- <table class="w-full border-collapse text-xs">
1247
- <tbody>
1248
- \${nestedWriteListOps.map((op) => '<tr><td class="py-1.5 pr-2 align-top font-mono border-b border-gray-200">' + escapeHtml(op.key) + '</td><td class="py-1.5 border-b border-gray-200">' + escapeHtml(op.desc) + '</td></tr>').join('')}
1249
- </tbody>
1250
- </table>
1251
- </div>
1252
- <div class="\${calloutClass} mt-2 text-xs text-gray-500">Nested write shapes depend on the related model's schema. See the related model's API docs for valid create and connect inputs. These operations are not available in bulk write endpoints (createMany, updateMany).</div>
1253
- </div>
1254
- <div>
1255
- <h3 class="mt-3.5 mb-2 text-sm">7.2 Single relation operations</h3>
1256
- <div class="\${calloutClass}">
1257
- \${singleRelations.length > 0
1258
- ? '<div class="text-xs text-gray-500 mb-2">Relations: ' + singleRelations.map((f) => '<span class="font-mono">' + escapeHtml(f.name) + '</span> (' + escapeHtml(String(f.type)) + ')').join(', ') + '</div>'
1259
- : '<div class="text-xs text-gray-500 mb-2">This model has no single relations.</div>'}
1260
- <table class="w-full border-collapse text-xs">
1261
- <tbody>
1262
- \${nestedWriteSingleOps.map((op) => '<tr><td class="py-1.5 pr-2 align-top font-mono border-b border-gray-200">' + escapeHtml(op.key) + '</td><td class="py-1.5 border-b border-gray-200">' + escapeHtml(op.desc) + '</td></tr>').join('')}
1263
- </tbody>
1264
- </table>
1265
- </div>
1266
- <div class="\${calloutClass} mt-2 text-xs text-gray-500">Nested write shapes depend on the related model's schema. See the related model's API docs for valid create and connect inputs. These operations are not available in bulk write endpoints (createMany, updateMany).</div>
1267
- </div>
1268
- </div>
1269
-
1270
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="order">8. orderBy / cursor / distinct</h2>
1271
- <div class="\${calloutClass}">
1272
- <ul class="text-[13px]">\${orderRules.map((r) => '<li>' + escapeHtml(r) + '</li>').join('')}</ul>
1273
- </div>
1274
-
1275
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="pagination">9. Pagination</h2>
1276
- <div class="\${calloutClass}">
1277
- <ul class="text-[13px]">\${paginationNotes.map((r) => '<li>' + escapeHtml(r) + '</li>').join('')}</ul>
1278
- </div>
1279
-
1280
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="errors">10. Error Handling</h2>
1281
- <div class="\${calloutClass}">
1282
- <div class="text-[13px] mb-2">All errors return JSON with a <span class="font-mono">message</span> field.</div>
1283
- </div>
1284
- <table class="w-full border-collapse text-xs mt-2">
1285
- <thead>
1286
- <tr>
1287
- <th class="\${thClass}">Status</th>
1288
- <th class="\${thClass}">Description</th>
1289
- <th class="\${thClass}">Common Causes</th>
1290
- </tr>
1291
- </thead>
1292
- <tbody>
1293
- \${errorRows.map((r) => \`
1294
- <tr>
1295
- <td class="\${tdClass} font-mono">\${escapeHtml(r.status)}</td>
1296
- <td class="\${tdClass}">\${escapeHtml(r.description)}</td>
1297
- <td class="\${tdClass} text-gray-500">\${escapeHtml(r.causes)}</td>
1298
- </tr>
1299
- \`).join('')}
1300
- </tbody>
1301
- </table>
1302
-
1303
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="examples">11. Examples</h2>
1304
- <div class="grid grid-cols-2 gap-3.5 max-[920px]:grid-cols-1 example">
1305
- <div>
1306
- <h3 class="mt-3.5 mb-2 text-sm">11.1 GET — findMany</h3>
1307
- \${codeBlock(findManyFetchExample)}
1308
- </div>
1309
- <div>
1310
- <h3 class="mt-3.5 mb-2 text-sm">11.2 GET — findUnique</h3>
1311
- \${findUniqueFetchExample
1312
- ? codeBlock(findUniqueFetchExample)
1313
- : noUniqueFieldNote}
1314
- </div>
1315
- <div>
1316
- <h3 class="mt-3.5 mb-2 text-sm">11.3 POST — create</h3>
1317
- \${codeBlock(createFetchExample)}
1318
- </div>
1319
- <div>
1320
- <h3 class="mt-3.5 mb-2 text-sm">11.4 PUT — update</h3>
1321
- \${updateFetchExample
1322
- ? codeBlock(updateFetchExample)
1323
- : noUniqueFieldNote}
1324
- </div>
1325
- <div>
1326
- <h3 class="mt-3.5 mb-2 text-sm">11.5 DELETE — delete</h3>
1327
- \${deleteFetchExample
1328
- ? codeBlock(deleteFetchExample)
1329
- : noUniqueFieldNote}
1330
- </div>
1331
- <div>
1332
- <h3 class="mt-3.5 mb-2 text-sm">11.6 Guard variant header</h3>
1333
- \${guardFetchExample
1334
- ? codeBlock(guardFetchExample)
1335
- : noUniqueFieldNote}
1336
- </div>
1337
- </div>
1338
-
1339
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="guard-shapes">12. Guard Shapes</h2>
1340
- <div class="\${calloutClass}">
1341
- <ul class="text-[13px]">\${guardShapeInfo.map((r) => '<li>' + r + '</li>').join('')}</ul>
1342
- </div>
1343
-
1344
- <h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="runtime">13. Runtime Notes</h2>
1345
- <div class="\${calloutClass}">
1346
- <ul class="text-[13px] [&>li]:mb-2">\${runtimeNotes.map((r) => '<li>' + r + '</li>').join('')}</ul>
1347
- </div>
1348
-
1349
- </div>
1350
- <script src="\${PRISM_JS}"></script>
1351
- <script src="\${PRISM_JSON}"></script>
1352
- </body>
1353
- </html>\`
1354
-
1355
- return html
1356
- }
1357
-
1358
- export function ${modelName}Docs(config: DocsConfig = {}) {
1359
- return (req: Request, res: Response) => {
1360
- const disabled = isOpenApiDisabled(config.disableOpenApi)
1361
- if (disabled) return res.status(404).send('OpenAPI documentation is disabled in production')
1362
-
1363
- const rawUi = (req.query['ui'] as string | undefined) || config.docsUi || 'docs'
1364
- const validUis: DocsUI[] = ['docs', 'scalar', 'json', 'yaml', 'playground']
1365
- const ui: DocsUI = validUis.includes(rawUi as DocsUI) ? (rawUi as DocsUI) : 'docs'
1366
-
1367
- if (ui === 'playground') {
1368
- if (!isPlaygroundAvailable(config)) {
1369
- return res.status(404).send('Query builder is disabled')
1370
- }
1371
- return res.type('html').send(renderPlayground('${modelName}', config))
1372
- }
1373
-
1374
- if (ui === 'yaml') {
1375
- const yaml = buildModelOpenApi(
1376
- '${modelName}',
1377
- MODEL_FIELDS as any,
1378
- MODEL_ENUMS as any,
1379
- config,
1380
- { format: 'yaml' }
1381
- )
1382
- return res.type('application/yaml').send(yaml as string)
1383
- }
1384
-
1385
- const spec = buildModelOpenApi(
1386
- '${modelName}',
1387
- MODEL_FIELDS as any,
1388
- MODEL_ENUMS as any,
1389
- config,
1390
- { format: 'json' }
1391
- )
1392
-
1393
- if (ui === 'json') return res.json(spec)
1394
-
1395
- const pageTitle = config.docsTitle || \`${modelName} API\`
1396
-
1397
- if (ui === 'scalar') {
1398
- return res.type('html').send(renderScalar('${modelName}', spec, pageTitle, config.scalarCdnUrl))
1399
- }
1400
-
1401
- const html = renderDocs('${modelName}', config)
1402
- return res.type('html').send(html)
1403
- }
1404
- }
207
+ ${docsExport}
1405
208
  `;
1406
209
  }
1407
210
  //# sourceMappingURL=generateUnifiedScalarUI.js.map