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.
- package/README.md +244 -14
- package/dist/constants.d.ts +1 -0
- package/dist/generators/generateFastifyHandler.d.ts +4 -0
- package/dist/generators/generateFastifyHandler.js +78 -0
- package/dist/generators/generateFastifyHandler.js.map +1 -0
- package/dist/generators/generateOperationCore.d.ts +6 -0
- package/dist/generators/generateOperationCore.js +534 -0
- package/dist/generators/generateOperationCore.js.map +1 -0
- package/dist/generators/generateQueryBuilderHelper.js +85 -69
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRouter.js +1 -25
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +5 -0
- package/dist/generators/generateRouterFastify.js +512 -0
- package/dist/generators/generateRouterFastify.js.map +1 -0
- package/dist/generators/generateUnifiedDocs.d.ts +2 -1
- package/dist/generators/generateUnifiedDocs.js +147 -82
- package/dist/generators/generateUnifiedDocs.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.d.ts +0 -1
- package/dist/generators/generateUnifiedHandler.js +47 -516
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.d.ts +2 -0
- package/dist/generators/generateUnifiedScalarUI.js +127 -1324
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js +33 -8
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.d.ts +2 -1
- package/dist/utils/copyFiles.js +73 -39
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/writeFileSafely.js +3 -0
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +4 -1
- package/src/client/encodeQueryParams.ts +1 -1
- package/src/constants.ts +2 -0
- package/src/copy/createOutputValidatorMiddleware.ts +9 -12
- package/src/copy/docsRenderer.ts +1285 -0
- package/src/copy/parseQueryParams.ts +4 -8
- package/src/copy/routeConfig.ts +10 -4
- package/src/generators/generateFastifyHandler.ts +86 -0
- package/src/generators/generateOperationCore.ts +545 -0
- package/src/generators/generateQueryBuilderHelper.ts +86 -70
- package/src/generators/generateRouter.ts +1 -25
- package/src/generators/generateRouterFastify.ts +522 -0
- package/src/generators/generateUnifiedDocs.ts +164 -81
- package/src/generators/generateUnifiedHandler.ts +45 -533
- package/src/generators/generateUnifiedScalarUI.ts +134 -1323
- package/src/index.ts +45 -9
- package/src/utils/copyFiles.ts +88 -44
- 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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
type
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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, '&')
|
|
366
|
-
.replace(/</g, '<')
|
|
367
|
-
.replace(/>/g, '>')
|
|
368
|
-
.replace(/"/g, '"')
|
|
369
|
-
.replace(/'/g, ''')
|
|
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
|
-
|
|
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
|