prisma-generator-express 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +399 -194
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +1 -1
  4. package/dist/bin.js.map +1 -1
  5. package/dist/client/encodeQueryParams.d.ts +1 -0
  6. package/dist/client/encodeQueryParams.js +33 -0
  7. package/dist/client/encodeQueryParams.js.map +1 -0
  8. package/dist/constants.d.ts +1 -0
  9. package/dist/copy/misc.d.ts +5 -0
  10. package/dist/copy/misc.js +52 -0
  11. package/dist/copy/misc.js.map +1 -0
  12. package/dist/generators/generateImportPrismaStatement.d.ts +3 -0
  13. package/dist/generators/generateImportPrismaStatement.js +55 -0
  14. package/dist/generators/generateImportPrismaStatement.js.map +1 -0
  15. package/dist/generators/generateQueryBuilderHelper.d.ts +2 -0
  16. package/dist/generators/generateQueryBuilderHelper.js +139 -0
  17. package/dist/generators/generateQueryBuilderHelper.js.map +1 -0
  18. package/dist/generators/generateRouter.d.ts +6 -0
  19. package/dist/generators/generateRouter.js +340 -0
  20. package/dist/generators/generateRouter.js.map +1 -0
  21. package/dist/generators/generateUnifiedDocs.d.ts +1 -0
  22. package/dist/generators/generateUnifiedDocs.js +171 -0
  23. package/dist/generators/generateUnifiedDocs.js.map +1 -0
  24. package/dist/generators/generateUnifiedHandler.d.ts +6 -0
  25. package/dist/generators/generateUnifiedHandler.js +444 -0
  26. package/dist/generators/generateUnifiedHandler.js.map +1 -0
  27. package/dist/generators/generateUnifiedScalarUI.d.ts +5 -0
  28. package/dist/generators/generateUnifiedScalarUI.js +1390 -0
  29. package/dist/generators/generateUnifiedScalarUI.js.map +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +80 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/utils/copyFiles.d.ts +6 -0
  34. package/dist/utils/copyFiles.js +123 -21
  35. package/dist/utils/copyFiles.js.map +1 -1
  36. package/dist/utils/strings.d.ts +2 -0
  37. package/dist/utils/writeFileSafely.d.ts +10 -0
  38. package/dist/utils/writeFileSafely.js +86 -14
  39. package/dist/utils/writeFileSafely.js.map +1 -1
  40. package/package.json +59 -28
  41. package/src/bin.ts +1 -1
  42. package/src/client/encodeQueryParams.ts +56 -0
  43. package/src/copy/buildModelOpenApi.ts +1569 -0
  44. package/src/copy/misc.ts +21 -0
  45. package/src/copy/operationDefinitions.ts +96 -0
  46. package/src/copy/parseQueryParams.ts +36 -21
  47. package/src/copy/routeConfig.ts +68 -28
  48. package/src/generators/generateImportPrismaStatement.ts +78 -0
  49. package/src/generators/generateQueryBuilderHelper.ts +138 -0
  50. package/src/generators/generateRouter.ts +352 -0
  51. package/src/generators/generateUnifiedDocs.ts +168 -0
  52. package/src/generators/generateUnifiedHandler.ts +469 -0
  53. package/src/generators/generateUnifiedScalarUI.ts +1409 -0
  54. package/src/index.ts +100 -0
  55. package/src/utils/copyFiles.ts +123 -16
  56. package/src/utils/writeFileSafely.ts +79 -25
  57. package/dist/generator.js +0 -47
  58. package/dist/generator.js.map +0 -1
  59. package/dist/helpers/generateImportPrismaStatement.js +0 -25
  60. package/dist/helpers/generateImportPrismaStatement.js.map +0 -1
  61. package/dist/helpers/generateOperation.js +0 -471
  62. package/dist/helpers/generateOperation.js.map +0 -1
  63. package/dist/helpers/generateRouteFile.js +0 -210
  64. package/dist/helpers/generateRouteFile.js.map +0 -1
  65. package/dist/utils/formatFile.js +0 -26
  66. package/dist/utils/formatFile.js.map +0 -1
  67. package/src/copy/encodeQueryParams.spec.ts +0 -303
  68. package/src/copy/encodeQueryParams.ts +0 -44
  69. package/src/copy/misc.spec.ts +0 -62
  70. package/src/copy/parseQueryParams.spec.ts +0 -187
  71. package/src/copy/transformZod.spec.ts +0 -763
  72. package/src/generator.ts +0 -54
  73. package/src/helpers/generateImportPrismaStatement.ts +0 -38
  74. package/src/helpers/generateOperation.ts +0 -515
  75. package/src/helpers/generateRouteFile.ts +0 -213
  76. package/src/utils/formatFile.ts +0 -22
@@ -0,0 +1,1569 @@
1
+ import type { RouteConfig } from './routeConfig'
2
+ import { OPERATION_DEFS, isOperationEnabled } from './operationDefinitions'
3
+
4
+ type SchemaObject = {
5
+ type?: string | string[]
6
+ format?: string
7
+ enum?: string[]
8
+ items?: SchemaObject | RefObject
9
+ properties?: Record<string, SchemaObject | RefObject>
10
+ required?: string[]
11
+ description?: string
12
+ oneOf?: (SchemaObject | RefObject)[]
13
+ allOf?: (SchemaObject | RefObject)[]
14
+ nullable?: boolean
15
+ }
16
+
17
+ type RefObject = { $ref: string; description?: string }
18
+
19
+ type OpenApiSpec = {
20
+ openapi: string
21
+ info: { title: string; description: string; version: string }
22
+ servers?: Array<{ url: string; description?: string }>
23
+ paths: Record<string, any>
24
+ components: {
25
+ schemas: Record<string, SchemaObject>
26
+ securitySchemes?: Record<string, any>
27
+ }
28
+ security?: Array<Record<string, string[]>>
29
+ }
30
+
31
+ type ModelField = {
32
+ name: string
33
+ kind: string
34
+ type: string
35
+ isList: boolean
36
+ isRequired: boolean
37
+ hasDefaultValue: boolean
38
+ isUpdatedAt?: boolean
39
+ documentation?: string
40
+ relationFromFields?: string[]
41
+ }
42
+
43
+ type EnumDef = {
44
+ name: string
45
+ values: { name: string }[]
46
+ }
47
+
48
+ type BuildOptions = {
49
+ format: 'json' | 'yaml'
50
+ title?: string
51
+ description?: string
52
+ version?: string
53
+ }
54
+
55
+ const OP_MAP = new Map(OPERATION_DEFS.map((d) => [d.name, d]))
56
+
57
+ const NUMERIC_SCALAR_TYPES = new Set(['Int', 'BigInt', 'Float', 'Decimal'])
58
+
59
+ const STRING_NUMERIC_TYPES = new Set(['BigInt', 'Decimal'])
60
+
61
+ function opEnabled(config: RouteConfig, name: string): boolean {
62
+ const def = OP_MAP.get(name)
63
+ return def ? isOperationEnabled(config as Record<string, any>, def) : false
64
+ }
65
+
66
+ function opPath(basePath: string, name: string): string {
67
+ const def = OP_MAP.get(name)!
68
+ if (!def.pathSuffix) return basePath || '/'
69
+ return `${basePath}${def.pathSuffix}`
70
+ }
71
+
72
+ function errorRef(): RefObject {
73
+ return { $ref: '#/components/schemas/ErrorResponse' }
74
+ }
75
+
76
+ function errorResponse(description: string) {
77
+ return {
78
+ description,
79
+ content: { 'application/json': { schema: errorRef() } },
80
+ }
81
+ }
82
+
83
+ const COMMON_ERRORS: Record<number, string> = {
84
+ 400: 'Bad request — invalid parameters, malformed JSON, or query validation failure',
85
+ 403: 'Forbidden — guard policy rejected the request',
86
+ 404: 'Not found — record does not exist',
87
+ 409: 'Conflict — unique constraint or transaction conflict',
88
+ 500: 'Internal server error',
89
+ 501: 'Not implemented — feature not supported by the current database provider',
90
+ 503: 'Service unavailable — database connection pool timeout',
91
+ }
92
+
93
+ function addErrorResponses(operation: any, codes: number[]): void {
94
+ for (const code of codes) {
95
+ operation.responses[String(code)] = errorResponse(
96
+ COMMON_ERRORS[code] || 'Error',
97
+ )
98
+ }
99
+ }
100
+
101
+ function normalizePrefix(p: string): string {
102
+ if (!p) return ''
103
+ let result = p
104
+ if (!result.startsWith('/')) result = '/' + result
105
+ while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
106
+ if (result === '/') return ''
107
+ return result
108
+ }
109
+
110
+ function removeTrailingSlash(path: string): string {
111
+ if (path === '/') return ''
112
+ return path.endsWith('/') ? path.slice(0, -1) : path
113
+ }
114
+
115
+ function queryParam(
116
+ name: string,
117
+ description: string,
118
+ schema: Record<string, string> = { type: 'string' },
119
+ required?: boolean,
120
+ ) {
121
+ const param: any = { name, in: 'query' as const, schema, description }
122
+ if (required) param.required = true
123
+ return param
124
+ }
125
+
126
+ export function buildModelOpenApi(
127
+ modelName: string,
128
+ modelFields: ModelField[],
129
+ enums: EnumDef[],
130
+ config: RouteConfig,
131
+ options: BuildOptions,
132
+ ): string | OpenApiSpec {
133
+ const spec: OpenApiSpec = {
134
+ openapi: '3.1.0',
135
+ info: {
136
+ title: options.title || config.openApiTitle || `${modelName} API`,
137
+ description: options.description || config.openApiDescription || '',
138
+ version: options.version || config.openApiVersion || '1.0.0',
139
+ },
140
+ paths: {},
141
+ components: { schemas: {} },
142
+ }
143
+
144
+ if (config.openApiServers?.length) {
145
+ spec.servers = config.openApiServers
146
+ }
147
+
148
+ if (config.openApiSecuritySchemes) {
149
+ spec.components.securitySchemes = config.openApiSecuritySchemes
150
+ }
151
+
152
+ if (config.openApiSecurity?.length) {
153
+ spec.security = config.openApiSecurity
154
+ }
155
+
156
+ const prefixSource = config.specBasePath ?? config.customUrlPrefix ?? ''
157
+ const basePath =
158
+ normalizePrefix(prefixSource) +
159
+ removeTrailingSlash(
160
+ config.addModelPrefix !== false ? `/${modelName.toLowerCase()}` : '',
161
+ )
162
+
163
+ const referencedEnumTypes = new Set(
164
+ modelFields.filter((f) => f.kind === 'enum').map((f) => f.type),
165
+ )
166
+
167
+ for (const enumDef of enums) {
168
+ if (!referencedEnumTypes.has(enumDef.name)) continue
169
+ spec.components.schemas[enumDef.name] = {
170
+ type: 'string',
171
+ enum: enumDef.values.map((v) => v.name),
172
+ }
173
+ }
174
+
175
+ spec.components.schemas['ErrorResponse'] = {
176
+ type: 'object',
177
+ properties: {
178
+ message: {
179
+ type: 'string',
180
+ description: 'Human-readable error description',
181
+ },
182
+ },
183
+ required: ['message'],
184
+ }
185
+
186
+ generateOperationSchemas(spec, modelName, modelFields)
187
+
188
+ generatePaths(spec, modelName, basePath, config, modelFields)
189
+
190
+ if (options.format === 'yaml') {
191
+ return toYaml(spec)
192
+ }
193
+ return spec
194
+ }
195
+
196
+ function generateOperationSchemas(
197
+ spec: OpenApiSpec,
198
+ modelName: string,
199
+ fields: ModelField[],
200
+ ) {
201
+ const relatedModels = new Set<string>()
202
+ fields.forEach((field) => {
203
+ if (field.kind === 'object') {
204
+ relatedModels.add(field.type)
205
+ }
206
+ })
207
+
208
+ relatedModels.forEach((relatedModel) => {
209
+ if (!spec.components.schemas[`${relatedModel}Response`]) {
210
+ spec.components.schemas[`${relatedModel}Response`] = {
211
+ type: 'object',
212
+ description: `Related ${relatedModel} object. See the ${relatedModel} docs endpoint for full schema. Shape depends on select/include parameters.`,
213
+ }
214
+ }
215
+ })
216
+
217
+ const requiredScalars = fields
218
+ .filter(
219
+ (f) =>
220
+ (f.kind === 'scalar' || f.kind === 'enum') &&
221
+ f.isRequired &&
222
+ !f.hasDefaultValue &&
223
+ !f.isUpdatedAt,
224
+ )
225
+ .map((f) => f.name)
226
+
227
+ const createInputSchema: SchemaObject = {
228
+ type: 'object',
229
+ properties: fieldsToWriteProperties(fields),
230
+ }
231
+ if (requiredScalars.length > 0) {
232
+ createInputSchema.required = [...requiredScalars]
233
+ }
234
+
235
+ spec.components.schemas[`${modelName}CreateInput`] = createInputSchema
236
+
237
+ spec.components.schemas[`${modelName}UpdateInput`] = {
238
+ type: 'object',
239
+ properties: fieldsToWriteProperties(fields),
240
+ }
241
+
242
+ const createManyInputSchema: SchemaObject = {
243
+ type: 'object',
244
+ properties: fieldsToBulkWriteProperties(fields),
245
+ description:
246
+ 'Scalar-only input for bulk create. Nested relation writes are not supported in createMany operations.',
247
+ }
248
+ if (requiredScalars.length > 0) {
249
+ createManyInputSchema.required = [...requiredScalars]
250
+ }
251
+
252
+ spec.components.schemas[`${modelName}CreateManyInput`] = createManyInputSchema
253
+
254
+ spec.components.schemas[`${modelName}UpdateManyMutationInput`] = {
255
+ type: 'object',
256
+ properties: fieldsToBulkWriteProperties(fields),
257
+ description:
258
+ 'Scalar-only input for bulk update. Nested relation writes are not supported in updateMany operations.',
259
+ }
260
+
261
+ spec.components.schemas[`${modelName}Response`] = {
262
+ type: 'object',
263
+ properties: fieldsToProperties(fields),
264
+ description:
265
+ 'Response shape depends on select/include/omit parameters. Relations are only present when explicitly included.',
266
+ }
267
+
268
+ spec.components.schemas[`${modelName}ListResponse`] = {
269
+ type: 'object',
270
+ properties: {
271
+ data: {
272
+ type: 'array',
273
+ items: { $ref: `#/components/schemas/${modelName}Response` },
274
+ },
275
+ total: {
276
+ type: 'integer',
277
+ description:
278
+ 'Total number of matching records. May be approximate when using distinct with large result sets.',
279
+ },
280
+ hasMore: {
281
+ type: 'boolean',
282
+ description:
283
+ 'Whether more records exist beyond the current page. Reliable for forward offset pagination (skip + take) only.',
284
+ },
285
+ },
286
+ required: ['data', 'total', 'hasMore'],
287
+ }
288
+
289
+ spec.components.schemas[`${modelName}BatchCountResponse`] = {
290
+ type: 'object',
291
+ properties: {
292
+ count: {
293
+ type: 'integer',
294
+ description: 'Number of records affected by the batch operation',
295
+ },
296
+ },
297
+ required: ['count'],
298
+ }
299
+
300
+ const numericFields = fields.filter(
301
+ (f) => f.kind === 'scalar' && NUMERIC_SCALAR_TYPES.has(f.type),
302
+ )
303
+
304
+ const numericFieldSelection: Record<string, SchemaObject> = {}
305
+ for (const f of numericFields) {
306
+ numericFieldSelection[f.name] = { type: 'boolean' }
307
+ }
308
+
309
+ const avgResultProps: Record<string, SchemaObject> = {}
310
+ const numericResultProps: Record<string, SchemaObject> = {}
311
+ for (const f of numericFields) {
312
+ avgResultProps[f.name] = STRING_NUMERIC_TYPES.has(f.type)
313
+ ? {
314
+ oneOf: [{ type: 'string' }, { type: 'null' }],
315
+ description: 'Decimal serialized as string',
316
+ }
317
+ : { oneOf: [{ type: 'number' }, { type: 'null' }] }
318
+ numericResultProps[f.name] = STRING_NUMERIC_TYPES.has(f.type)
319
+ ? { oneOf: [{ type: 'string' }, { type: 'null' }] }
320
+ : { oneOf: [{ type: 'number' }, { type: 'null' }] }
321
+ }
322
+
323
+ const allFieldSelection: Record<string, SchemaObject> = {
324
+ _all: { type: 'boolean' },
325
+ }
326
+ for (const f of fields) {
327
+ if (f.kind === 'scalar' || f.kind === 'enum') {
328
+ allFieldSelection[f.name] = { type: 'boolean' }
329
+ }
330
+ }
331
+
332
+ const countResultProps: Record<string, SchemaObject> = {
333
+ _all: { type: 'integer' },
334
+ }
335
+ for (const f of fields) {
336
+ if (f.kind === 'scalar' || f.kind === 'enum') {
337
+ countResultProps[f.name] = { type: 'integer' }
338
+ }
339
+ }
340
+
341
+ const aggregateProps: Record<string, SchemaObject | RefObject> = {
342
+ _count: {
343
+ oneOf: [
344
+ { type: 'integer' },
345
+ { type: 'object', properties: countResultProps },
346
+ ],
347
+ },
348
+ }
349
+
350
+ if (numericFields.length > 0) {
351
+ aggregateProps._avg = { type: 'object', properties: avgResultProps }
352
+ aggregateProps._sum = { type: 'object', properties: numericResultProps }
353
+ aggregateProps._min = { type: 'object', properties: numericResultProps }
354
+ aggregateProps._max = { type: 'object', properties: numericResultProps }
355
+ }
356
+
357
+ spec.components.schemas[`${modelName}AggregateResponse`] = {
358
+ type: 'object',
359
+ properties: aggregateProps,
360
+ }
361
+
362
+ const groupByItemProps: Record<string, SchemaObject | RefObject> = {}
363
+ for (const f of fields) {
364
+ if (f.kind === 'scalar' || f.kind === 'enum') {
365
+ groupByItemProps[f.name] = mapFieldToSchema(f)
366
+ }
367
+ }
368
+ groupByItemProps._count = {
369
+ oneOf: [
370
+ { type: 'integer' },
371
+ { type: 'object', properties: countResultProps },
372
+ ],
373
+ }
374
+ if (numericFields.length > 0) {
375
+ groupByItemProps._avg = { type: 'object', properties: avgResultProps }
376
+ groupByItemProps._sum = { type: 'object', properties: numericResultProps }
377
+ groupByItemProps._min = { type: 'object', properties: numericResultProps }
378
+ groupByItemProps._max = { type: 'object', properties: numericResultProps }
379
+ }
380
+
381
+ spec.components.schemas[`${modelName}GroupByItem`] = {
382
+ type: 'object',
383
+ properties: groupByItemProps,
384
+ description:
385
+ 'Each item contains the field values for the fields specified in the by parameter plus any requested aggregates. Only by-fields appear as scalar properties in the response — this schema lists all possible fields for reference.',
386
+ }
387
+ }
388
+
389
+ function getFindManyParams() {
390
+ return [
391
+ queryParam('where', 'Filter conditions (JSON-encoded string)'),
392
+ queryParam('orderBy', 'Sort order (JSON-encoded string)'),
393
+ queryParam('take', 'Limit results', { type: 'integer' }),
394
+ queryParam('skip', 'Skip results', { type: 'integer' }),
395
+ queryParam('select', 'Select fields (JSON-encoded string)'),
396
+ queryParam('include', 'Include relations (JSON-encoded string)'),
397
+ queryParam('omit', 'Omit fields from response (JSON-encoded string)'),
398
+ queryParam('cursor', 'Cursor for pagination (JSON-encoded string)'),
399
+ queryParam('distinct', 'Distinct fields (JSON-encoded string)'),
400
+ ]
401
+ }
402
+
403
+ function getFindUniqueParams() {
404
+ return [
405
+ queryParam(
406
+ 'where',
407
+ 'Unique selector (JSON-encoded string)',
408
+ { type: 'string' },
409
+ true,
410
+ ),
411
+ queryParam('select', 'Select fields (JSON-encoded string)'),
412
+ queryParam('include', 'Include relations (JSON-encoded string)'),
413
+ queryParam('omit', 'Omit fields from response (JSON-encoded string)'),
414
+ ]
415
+ }
416
+
417
+ function getCountParams() {
418
+ return [
419
+ queryParam('where', 'Filter conditions (JSON-encoded string)'),
420
+ queryParam('orderBy', 'Sort order (JSON-encoded string)'),
421
+ queryParam('take', 'Limit results', { type: 'integer' }),
422
+ queryParam('skip', 'Skip results', { type: 'integer' }),
423
+ queryParam('cursor', 'Cursor for pagination (JSON-encoded string)'),
424
+ queryParam(
425
+ 'select',
426
+ 'Count specific fields (JSON-encoded). When provided, returns per-field counts as an object instead of a single integer.',
427
+ ),
428
+ ]
429
+ }
430
+
431
+ function getAggregateParams() {
432
+ return [
433
+ queryParam('where', 'Filter conditions (JSON-encoded string)'),
434
+ queryParam('orderBy', 'Sort order (JSON-encoded string)'),
435
+ queryParam('cursor', 'Cursor for pagination (JSON-encoded string)'),
436
+ queryParam('take', 'Limit results', { type: 'integer' }),
437
+ queryParam('skip', 'Skip results', { type: 'integer' }),
438
+ queryParam(
439
+ '_count',
440
+ 'Count aggregate (JSON-encoded: true or field selection object)',
441
+ ),
442
+ queryParam(
443
+ '_avg',
444
+ 'Average aggregate (JSON-encoded field selection object)',
445
+ ),
446
+ queryParam('_sum', 'Sum aggregate (JSON-encoded field selection object)'),
447
+ queryParam('_min', 'Min aggregate (JSON-encoded field selection object)'),
448
+ queryParam('_max', 'Max aggregate (JSON-encoded field selection object)'),
449
+ ]
450
+ }
451
+
452
+ function getGroupByParams() {
453
+ return [
454
+ queryParam(
455
+ 'by',
456
+ 'Fields to group by (JSON-encoded string array)',
457
+ { type: 'string' },
458
+ true,
459
+ ),
460
+ queryParam('where', 'Filter conditions (JSON-encoded string)'),
461
+ queryParam(
462
+ 'orderBy',
463
+ 'Sort order (JSON-encoded string). Required when using skip or take.',
464
+ ),
465
+ queryParam('having', 'Having conditions (JSON-encoded filter object)'),
466
+ queryParam('take', 'Limit results', { type: 'integer' }),
467
+ queryParam('skip', 'Skip results', { type: 'integer' }),
468
+ queryParam(
469
+ '_count',
470
+ 'Count aggregate (JSON-encoded: true or field selection object)',
471
+ ),
472
+ queryParam(
473
+ '_avg',
474
+ 'Average aggregate (JSON-encoded field selection object)',
475
+ ),
476
+ queryParam('_sum', 'Sum aggregate (JSON-encoded field selection object)'),
477
+ queryParam('_min', 'Min aggregate (JSON-encoded field selection object)'),
478
+ queryParam('_max', 'Max aggregate (JSON-encoded field selection object)'),
479
+ ]
480
+ }
481
+
482
+ function generatePaths(
483
+ spec: OpenApiSpec,
484
+ modelName: string,
485
+ basePath: string,
486
+ config: RouteConfig,
487
+ fields: ModelField[],
488
+ ) {
489
+ const createInputRef = {
490
+ $ref: `#/components/schemas/${modelName}CreateInput`,
491
+ }
492
+ const updateInputRef = {
493
+ $ref: `#/components/schemas/${modelName}UpdateInput`,
494
+ }
495
+ const createManyInputRef = {
496
+ $ref: `#/components/schemas/${modelName}CreateManyInput`,
497
+ }
498
+ const updateManyMutationRef = {
499
+ $ref: `#/components/schemas/${modelName}UpdateManyMutationInput`,
500
+ }
501
+ const responseRef = { $ref: `#/components/schemas/${modelName}Response` }
502
+ const nullableResponseSchema = {
503
+ oneOf: [responseRef, { type: 'null' as const }],
504
+ }
505
+ const batchCountRef = {
506
+ $ref: `#/components/schemas/${modelName}BatchCountResponse`,
507
+ }
508
+ const listRef = { $ref: `#/components/schemas/${modelName}ListResponse` }
509
+ const aggregateRef = {
510
+ $ref: `#/components/schemas/${modelName}AggregateResponse`,
511
+ }
512
+ const groupByItemRef = {
513
+ $ref: `#/components/schemas/${modelName}GroupByItem`,
514
+ }
515
+
516
+ if (opEnabled(config, 'findMany')) {
517
+ const op: any = {
518
+ tags: [modelName],
519
+ summary: `List ${modelName}`,
520
+ operationId: `${modelName}FindMany`,
521
+ parameters: getFindManyParams(),
522
+ responses: {
523
+ '200': {
524
+ description: 'Success',
525
+ content: {
526
+ 'application/json': {
527
+ schema: { type: 'array', items: responseRef },
528
+ },
529
+ },
530
+ },
531
+ },
532
+ }
533
+ addErrorResponses(op, [400, 403, 500, 501, 503])
534
+ addPath(spec, opPath(basePath, 'findMany'), 'get', op)
535
+ }
536
+
537
+ if (opEnabled(config, 'findUnique')) {
538
+ const op: any = {
539
+ tags: [modelName],
540
+ summary: `Get ${modelName} by unique constraint`,
541
+ operationId: `${modelName}FindUnique`,
542
+ description:
543
+ 'Returns null with status 200 when no record matches the unique constraint.',
544
+ parameters: getFindUniqueParams(),
545
+ responses: {
546
+ '200': {
547
+ description: 'Success (returns the record or null)',
548
+ content: { 'application/json': { schema: nullableResponseSchema } },
549
+ },
550
+ },
551
+ }
552
+ addErrorResponses(op, [400, 403, 500, 501, 503])
553
+ addPath(spec, opPath(basePath, 'findUnique'), 'get', op)
554
+ }
555
+
556
+ if (opEnabled(config, 'findUniqueOrThrow')) {
557
+ const op: any = {
558
+ tags: [modelName],
559
+ summary: `Get ${modelName} by unique constraint (throws if not found)`,
560
+ operationId: `${modelName}FindUniqueOrThrow`,
561
+ parameters: getFindUniqueParams(),
562
+ responses: {
563
+ '200': {
564
+ description: 'Success',
565
+ content: { 'application/json': { schema: responseRef } },
566
+ },
567
+ },
568
+ }
569
+ addErrorResponses(op, [400, 403, 404, 500, 501, 503])
570
+ addPath(spec, opPath(basePath, 'findUniqueOrThrow'), 'get', op)
571
+ }
572
+
573
+ if (opEnabled(config, 'findFirst')) {
574
+ const op: any = {
575
+ tags: [modelName],
576
+ summary: `Get first ${modelName}`,
577
+ operationId: `${modelName}FindFirst`,
578
+ description: 'Returns null with status 200 when no record matches.',
579
+ parameters: getFindManyParams(),
580
+ responses: {
581
+ '200': {
582
+ description: 'Success (returns the record or null)',
583
+ content: { 'application/json': { schema: nullableResponseSchema } },
584
+ },
585
+ },
586
+ }
587
+ addErrorResponses(op, [400, 403, 500, 501, 503])
588
+ addPath(spec, opPath(basePath, 'findFirst'), 'get', op)
589
+ }
590
+
591
+ if (opEnabled(config, 'findFirstOrThrow')) {
592
+ const op: any = {
593
+ tags: [modelName],
594
+ summary: `Get first ${modelName} (throws if not found)`,
595
+ operationId: `${modelName}FindFirstOrThrow`,
596
+ parameters: getFindManyParams(),
597
+ responses: {
598
+ '200': {
599
+ description: 'Success',
600
+ content: { 'application/json': { schema: responseRef } },
601
+ },
602
+ },
603
+ }
604
+ addErrorResponses(op, [400, 403, 404, 500, 501, 503])
605
+ addPath(spec, opPath(basePath, 'findFirstOrThrow'), 'get', op)
606
+ }
607
+
608
+ if (opEnabled(config, 'findManyPaginated')) {
609
+ const op: any = {
610
+ tags: [modelName],
611
+ summary: `List ${modelName} with pagination`,
612
+ operationId: `${modelName}FindManyPaginated`,
613
+ description:
614
+ 'Returns paginated results with total count. The hasMore field is reliable for forward offset pagination (skip + take) only. When using distinct with very large result sets (>100k unique values), total may fall back to an approximate non-distinct count.',
615
+ parameters: getFindManyParams(),
616
+ responses: {
617
+ '200': {
618
+ description: 'Success',
619
+ content: { 'application/json': { schema: listRef } },
620
+ },
621
+ },
622
+ }
623
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
624
+ addPath(spec, opPath(basePath, 'findManyPaginated'), 'get', op)
625
+ }
626
+
627
+ if (opEnabled(config, 'create')) {
628
+ const op: any = {
629
+ tags: [modelName],
630
+ summary: `Create ${modelName}`,
631
+ operationId: `${modelName}Create`,
632
+ requestBody: {
633
+ required: true,
634
+ content: {
635
+ 'application/json': {
636
+ schema: {
637
+ type: 'object',
638
+ properties: {
639
+ data: createInputRef,
640
+ select: {
641
+ type: 'object',
642
+ description: 'Select fields to return',
643
+ },
644
+ include: {
645
+ type: 'object',
646
+ description: 'Include relations to return',
647
+ },
648
+ omit: {
649
+ type: 'object',
650
+ description: 'Omit fields from response',
651
+ },
652
+ },
653
+ required: ['data'],
654
+ },
655
+ },
656
+ },
657
+ },
658
+ responses: {
659
+ '201': {
660
+ description: 'Created',
661
+ content: { 'application/json': { schema: responseRef } },
662
+ },
663
+ },
664
+ }
665
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
666
+ addPath(spec, opPath(basePath, 'create'), 'post', op)
667
+ }
668
+
669
+ if (opEnabled(config, 'createMany')) {
670
+ const op: any = {
671
+ tags: [modelName],
672
+ summary: `Create many ${modelName}`,
673
+ operationId: `${modelName}CreateMany`,
674
+ requestBody: {
675
+ required: true,
676
+ content: {
677
+ 'application/json': {
678
+ schema: {
679
+ type: 'object',
680
+ properties: {
681
+ data: { type: 'array', items: createManyInputRef },
682
+ skipDuplicates: {
683
+ type: 'boolean',
684
+ description:
685
+ 'Skip records that would cause unique constraint violations. Not supported on all database providers.',
686
+ },
687
+ },
688
+ required: ['data'],
689
+ },
690
+ },
691
+ },
692
+ },
693
+ responses: {
694
+ '201': {
695
+ description: 'Created',
696
+ content: { 'application/json': { schema: batchCountRef } },
697
+ },
698
+ },
699
+ }
700
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
701
+ addPath(spec, opPath(basePath, 'createMany'), 'post', op)
702
+ }
703
+
704
+ if (opEnabled(config, 'createManyAndReturn')) {
705
+ const op: any = {
706
+ tags: [modelName],
707
+ summary: `Create many ${modelName} and return records`,
708
+ operationId: `${modelName}CreateManyAndReturn`,
709
+ requestBody: {
710
+ required: true,
711
+ content: {
712
+ 'application/json': {
713
+ schema: {
714
+ type: 'object',
715
+ properties: {
716
+ data: { type: 'array', items: createManyInputRef },
717
+ skipDuplicates: {
718
+ type: 'boolean',
719
+ description:
720
+ 'Skip records that would cause unique constraint violations. Not supported on all database providers.',
721
+ },
722
+ select: {
723
+ type: 'object',
724
+ description: 'Select fields to return',
725
+ },
726
+ include: {
727
+ type: 'object',
728
+ description: 'Include relations to return',
729
+ },
730
+ omit: {
731
+ type: 'object',
732
+ description: 'Omit fields from response',
733
+ },
734
+ },
735
+ required: ['data'],
736
+ },
737
+ },
738
+ },
739
+ },
740
+ responses: {
741
+ '201': {
742
+ description: 'Created',
743
+ content: {
744
+ 'application/json': {
745
+ schema: { type: 'array', items: responseRef },
746
+ },
747
+ },
748
+ },
749
+ },
750
+ }
751
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
752
+ addPath(spec, opPath(basePath, 'createManyAndReturn'), 'post', op)
753
+ }
754
+
755
+ if (opEnabled(config, 'update')) {
756
+ const op: any = {
757
+ tags: [modelName],
758
+ summary: `Update ${modelName}`,
759
+ operationId: `${modelName}Update`,
760
+ requestBody: {
761
+ required: true,
762
+ content: {
763
+ 'application/json': {
764
+ schema: {
765
+ type: 'object',
766
+ properties: {
767
+ where: { type: 'object' },
768
+ data: updateInputRef,
769
+ select: {
770
+ type: 'object',
771
+ description: 'Select fields to return',
772
+ },
773
+ include: {
774
+ type: 'object',
775
+ description: 'Include relations to return',
776
+ },
777
+ omit: {
778
+ type: 'object',
779
+ description: 'Omit fields from response',
780
+ },
781
+ },
782
+ required: ['where', 'data'],
783
+ },
784
+ },
785
+ },
786
+ },
787
+ responses: {
788
+ '200': {
789
+ description: 'Success',
790
+ content: { 'application/json': { schema: responseRef } },
791
+ },
792
+ },
793
+ }
794
+ addErrorResponses(op, [400, 403, 404, 409, 500, 501, 503])
795
+ addPath(spec, opPath(basePath, 'update'), 'put', op)
796
+ }
797
+
798
+ if (opEnabled(config, 'updateMany')) {
799
+ const op: any = {
800
+ tags: [modelName],
801
+ summary: `Update many ${modelName}`,
802
+ operationId: `${modelName}UpdateMany`,
803
+ requestBody: {
804
+ required: true,
805
+ content: {
806
+ 'application/json': {
807
+ schema: {
808
+ type: 'object',
809
+ properties: {
810
+ where: { type: 'object' },
811
+ data: updateManyMutationRef,
812
+ },
813
+ required: ['where', 'data'],
814
+ },
815
+ },
816
+ },
817
+ },
818
+ responses: {
819
+ '200': {
820
+ description: 'Success',
821
+ content: { 'application/json': { schema: batchCountRef } },
822
+ },
823
+ },
824
+ }
825
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
826
+ addPath(spec, opPath(basePath, 'updateMany'), 'put', op)
827
+ }
828
+
829
+ if (opEnabled(config, 'updateManyAndReturn')) {
830
+ const op: any = {
831
+ tags: [modelName],
832
+ summary: `Update many ${modelName} and return records`,
833
+ operationId: `${modelName}UpdateManyAndReturn`,
834
+ requestBody: {
835
+ required: true,
836
+ content: {
837
+ 'application/json': {
838
+ schema: {
839
+ type: 'object',
840
+ properties: {
841
+ where: { type: 'object' },
842
+ data: updateManyMutationRef,
843
+ select: {
844
+ type: 'object',
845
+ description: 'Select fields to return',
846
+ },
847
+ include: {
848
+ type: 'object',
849
+ description: 'Include relations to return',
850
+ },
851
+ omit: {
852
+ type: 'object',
853
+ description: 'Omit fields from response',
854
+ },
855
+ },
856
+ required: ['where', 'data'],
857
+ },
858
+ },
859
+ },
860
+ },
861
+ responses: {
862
+ '200': {
863
+ description: 'Success',
864
+ content: {
865
+ 'application/json': {
866
+ schema: { type: 'array', items: responseRef },
867
+ },
868
+ },
869
+ },
870
+ },
871
+ }
872
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
873
+ addPath(spec, opPath(basePath, 'updateManyAndReturn'), 'put', op)
874
+ }
875
+
876
+ if (opEnabled(config, 'upsert')) {
877
+ const op: any = {
878
+ tags: [modelName],
879
+ summary: `Upsert ${modelName}`,
880
+ operationId: `${modelName}Upsert`,
881
+ requestBody: {
882
+ required: true,
883
+ content: {
884
+ 'application/json': {
885
+ schema: {
886
+ type: 'object',
887
+ properties: {
888
+ where: { type: 'object' },
889
+ create: createInputRef,
890
+ update: updateInputRef,
891
+ select: {
892
+ type: 'object',
893
+ description: 'Select fields to return',
894
+ },
895
+ include: {
896
+ type: 'object',
897
+ description: 'Include relations to return',
898
+ },
899
+ omit: {
900
+ type: 'object',
901
+ description: 'Omit fields from response',
902
+ },
903
+ },
904
+ required: ['where', 'create', 'update'],
905
+ },
906
+ },
907
+ },
908
+ },
909
+ responses: {
910
+ '200': {
911
+ description: 'Success',
912
+ content: { 'application/json': { schema: responseRef } },
913
+ },
914
+ },
915
+ }
916
+ addErrorResponses(op, [400, 403, 409, 500, 501, 503])
917
+ addPath(spec, opPath(basePath, 'upsert'), 'patch', op)
918
+ }
919
+
920
+ if (opEnabled(config, 'delete')) {
921
+ const op: any = {
922
+ tags: [modelName],
923
+ summary: `Delete ${modelName}`,
924
+ operationId: `${modelName}Delete`,
925
+ requestBody: {
926
+ required: true,
927
+ content: {
928
+ 'application/json': {
929
+ schema: {
930
+ type: 'object',
931
+ properties: {
932
+ where: { type: 'object' },
933
+ select: {
934
+ type: 'object',
935
+ description: 'Select fields to return',
936
+ },
937
+ include: {
938
+ type: 'object',
939
+ description: 'Include relations to return',
940
+ },
941
+ omit: {
942
+ type: 'object',
943
+ description: 'Omit fields from response',
944
+ },
945
+ },
946
+ required: ['where'],
947
+ },
948
+ },
949
+ },
950
+ },
951
+ responses: {
952
+ '200': {
953
+ description: 'Deleted',
954
+ content: { 'application/json': { schema: responseRef } },
955
+ },
956
+ },
957
+ }
958
+ addErrorResponses(op, [400, 403, 404, 500, 501, 503])
959
+ addPath(spec, opPath(basePath, 'delete'), 'delete', op)
960
+ }
961
+
962
+ if (opEnabled(config, 'deleteMany')) {
963
+ const op: any = {
964
+ tags: [modelName],
965
+ summary: `Delete many ${modelName}`,
966
+ operationId: `${modelName}DeleteMany`,
967
+ requestBody: {
968
+ required: true,
969
+ content: {
970
+ 'application/json': {
971
+ schema: {
972
+ type: 'object',
973
+ properties: { where: { type: 'object' } },
974
+ required: ['where'],
975
+ },
976
+ },
977
+ },
978
+ },
979
+ responses: {
980
+ '200': {
981
+ description: 'Deleted',
982
+ content: { 'application/json': { schema: batchCountRef } },
983
+ },
984
+ },
985
+ }
986
+ addErrorResponses(op, [400, 403, 500, 501, 503])
987
+ addPath(spec, opPath(basePath, 'deleteMany'), 'delete', op)
988
+ }
989
+
990
+ if (opEnabled(config, 'count')) {
991
+ const op: any = {
992
+ tags: [modelName],
993
+ summary: `Count ${modelName}`,
994
+ operationId: `${modelName}Count`,
995
+ parameters: getCountParams(),
996
+ responses: {
997
+ '200': {
998
+ description: 'Success',
999
+ content: {
1000
+ 'application/json': {
1001
+ schema: {
1002
+ oneOf: [
1003
+ {
1004
+ type: 'integer',
1005
+ description: 'Total count when select is not provided',
1006
+ },
1007
+ {
1008
+ type: 'object',
1009
+ description:
1010
+ 'Per-field count object when select is provided',
1011
+ },
1012
+ ],
1013
+ },
1014
+ },
1015
+ },
1016
+ },
1017
+ },
1018
+ }
1019
+ addErrorResponses(op, [400, 403, 500, 501, 503])
1020
+ addPath(spec, opPath(basePath, 'count'), 'get', op)
1021
+ }
1022
+
1023
+ if (opEnabled(config, 'aggregate')) {
1024
+ const op: any = {
1025
+ tags: [modelName],
1026
+ summary: `Aggregate ${modelName}`,
1027
+ operationId: `${modelName}Aggregate`,
1028
+ parameters: getAggregateParams(),
1029
+ responses: {
1030
+ '200': {
1031
+ description: 'Success',
1032
+ content: { 'application/json': { schema: aggregateRef } },
1033
+ },
1034
+ },
1035
+ }
1036
+ addErrorResponses(op, [400, 403, 500, 501, 503])
1037
+ addPath(spec, opPath(basePath, 'aggregate'), 'get', op)
1038
+ }
1039
+
1040
+ if (opEnabled(config, 'groupBy')) {
1041
+ const op: any = {
1042
+ tags: [modelName],
1043
+ summary: `Group ${modelName}`,
1044
+ operationId: `${modelName}GroupBy`,
1045
+ description:
1046
+ 'Groups records by the specified fields and returns aggregates. When using skip or take, orderBy is required. Response items contain only the fields listed in by plus any requested aggregates.',
1047
+ parameters: getGroupByParams(),
1048
+ responses: {
1049
+ '200': {
1050
+ description: 'Success',
1051
+ content: {
1052
+ 'application/json': {
1053
+ schema: { type: 'array', items: groupByItemRef },
1054
+ },
1055
+ },
1056
+ },
1057
+ },
1058
+ }
1059
+ addErrorResponses(op, [400, 403, 500, 501, 503])
1060
+ addPath(spec, opPath(basePath, 'groupBy'), 'get', op)
1061
+ }
1062
+ }
1063
+
1064
+ function addPath(
1065
+ spec: OpenApiSpec,
1066
+ path: string,
1067
+ method: string,
1068
+ operation: any,
1069
+ ) {
1070
+ if (!spec.paths[path]) {
1071
+ spec.paths[path] = {}
1072
+ }
1073
+ spec.paths[path][method] = operation
1074
+ }
1075
+
1076
+ function fieldsToProperties(
1077
+ fields: ModelField[],
1078
+ ): Record<string, SchemaObject | RefObject> {
1079
+ const props: Record<string, SchemaObject | RefObject> = {}
1080
+ for (const field of fields) {
1081
+ props[field.name] = mapFieldToSchema(field)
1082
+ }
1083
+ return props
1084
+ }
1085
+
1086
+ function fieldsToWriteProperties(
1087
+ fields: ModelField[],
1088
+ ): Record<string, SchemaObject | RefObject> {
1089
+ const props: Record<string, SchemaObject | RefObject> = {}
1090
+ for (const field of fields) {
1091
+ props[field.name] = mapFieldToWriteSchema(field)
1092
+ }
1093
+ return props
1094
+ }
1095
+
1096
+ function fieldsToBulkWriteProperties(
1097
+ fields: ModelField[],
1098
+ ): Record<string, SchemaObject | RefObject> {
1099
+ const props: Record<string, SchemaObject | RefObject> = {}
1100
+ for (const field of fields) {
1101
+ if (field.kind === 'object') continue
1102
+ props[field.name] = mapFieldToWriteSchema(field)
1103
+ }
1104
+ return props
1105
+ }
1106
+
1107
+ function wrapNullable(schema: SchemaObject | RefObject): SchemaObject {
1108
+ if ('$ref' in schema) {
1109
+ return { oneOf: [schema, { type: 'null' }] }
1110
+ }
1111
+ if (
1112
+ schema.type &&
1113
+ typeof schema.type === 'string' &&
1114
+ schema.type !== 'array'
1115
+ ) {
1116
+ return { ...schema, type: [schema.type, 'null'] }
1117
+ }
1118
+ return { oneOf: [schema, { type: 'null' }] }
1119
+ }
1120
+
1121
+ function mapFieldToSchema(field: ModelField): SchemaObject | RefObject {
1122
+ let schema: SchemaObject | RefObject
1123
+
1124
+ switch (field.kind) {
1125
+ case 'scalar':
1126
+ schema = mapScalarType(field.type)
1127
+ break
1128
+ case 'enum':
1129
+ schema = { $ref: `#/components/schemas/${field.type}` }
1130
+ break
1131
+ case 'object':
1132
+ if (field.isList) {
1133
+ schema = {
1134
+ type: 'array',
1135
+ items: { $ref: `#/components/schemas/${field.type}Response` },
1136
+ }
1137
+ } else {
1138
+ schema = { $ref: `#/components/schemas/${field.type}Response` }
1139
+ }
1140
+ break
1141
+ default:
1142
+ schema = { type: 'string' }
1143
+ }
1144
+
1145
+ if (field.isList && field.kind !== 'object') {
1146
+ schema = { type: 'array', items: schema }
1147
+ }
1148
+
1149
+ if (!field.isRequired && !field.isList) {
1150
+ schema = wrapNullable(schema)
1151
+ }
1152
+
1153
+ if (field.documentation) {
1154
+ if ('$ref' in schema && !('oneOf' in schema) && !('allOf' in schema)) {
1155
+ schema = {
1156
+ allOf: [{ $ref: (schema as RefObject).$ref }],
1157
+ description: field.documentation,
1158
+ }
1159
+ } else if (!('$ref' in schema)) {
1160
+ schema.description = field.documentation
1161
+ }
1162
+ }
1163
+
1164
+ return schema
1165
+ }
1166
+
1167
+ function mapFieldToWriteSchema(field: ModelField): SchemaObject | RefObject {
1168
+ if (field.kind === 'object') {
1169
+ if (field.isList) {
1170
+ return {
1171
+ type: 'object',
1172
+ description: `Nested ${field.type} write operations for list relation`,
1173
+ properties: {
1174
+ create: {
1175
+ oneOf: [
1176
+ { type: 'object', description: `${field.type} create input` },
1177
+ {
1178
+ type: 'array',
1179
+ items: {
1180
+ type: 'object',
1181
+ description: `${field.type} create input`,
1182
+ },
1183
+ },
1184
+ ],
1185
+ },
1186
+ connect: {
1187
+ oneOf: [
1188
+ { type: 'object', description: 'Unique identifier to connect' },
1189
+ {
1190
+ type: 'array',
1191
+ items: {
1192
+ type: 'object',
1193
+ description: 'Unique identifier to connect',
1194
+ },
1195
+ },
1196
+ ],
1197
+ },
1198
+ connectOrCreate: {
1199
+ oneOf: [
1200
+ {
1201
+ type: 'object',
1202
+ description: '{ where, create } pair',
1203
+ properties: {
1204
+ where: { type: 'object' },
1205
+ create: { type: 'object' },
1206
+ },
1207
+ },
1208
+ {
1209
+ type: 'array',
1210
+ items: {
1211
+ type: 'object',
1212
+ properties: {
1213
+ where: { type: 'object' },
1214
+ create: { type: 'object' },
1215
+ },
1216
+ },
1217
+ },
1218
+ ],
1219
+ },
1220
+ createMany: {
1221
+ type: 'object',
1222
+ properties: {
1223
+ data: {
1224
+ type: 'array',
1225
+ items: {
1226
+ type: 'object',
1227
+ description: `${field.type} create input`,
1228
+ },
1229
+ },
1230
+ skipDuplicates: { type: 'boolean' },
1231
+ },
1232
+ },
1233
+ set: {
1234
+ type: 'array',
1235
+ items: { type: 'object', description: 'Unique identifier' },
1236
+ description: 'Replace all connected records',
1237
+ },
1238
+ disconnect: {
1239
+ oneOf: [
1240
+ {
1241
+ type: 'object',
1242
+ description: 'Unique identifier to disconnect',
1243
+ },
1244
+ {
1245
+ type: 'array',
1246
+ items: {
1247
+ type: 'object',
1248
+ description: 'Unique identifier to disconnect',
1249
+ },
1250
+ },
1251
+ ],
1252
+ },
1253
+ delete: {
1254
+ oneOf: [
1255
+ { type: 'object', description: 'Unique identifier to delete' },
1256
+ {
1257
+ type: 'array',
1258
+ items: {
1259
+ type: 'object',
1260
+ description: 'Unique identifier to delete',
1261
+ },
1262
+ },
1263
+ ],
1264
+ },
1265
+ update: {
1266
+ oneOf: [
1267
+ {
1268
+ type: 'object',
1269
+ description: '{ where, data } pair',
1270
+ properties: {
1271
+ where: { type: 'object' },
1272
+ data: { type: 'object' },
1273
+ },
1274
+ },
1275
+ {
1276
+ type: 'array',
1277
+ items: {
1278
+ type: 'object',
1279
+ properties: {
1280
+ where: { type: 'object' },
1281
+ data: { type: 'object' },
1282
+ },
1283
+ },
1284
+ },
1285
+ ],
1286
+ },
1287
+ updateMany: {
1288
+ oneOf: [
1289
+ {
1290
+ type: 'object',
1291
+ description: '{ where, data } pair',
1292
+ properties: {
1293
+ where: { type: 'object' },
1294
+ data: { type: 'object' },
1295
+ },
1296
+ },
1297
+ {
1298
+ type: 'array',
1299
+ items: {
1300
+ type: 'object',
1301
+ properties: {
1302
+ where: { type: 'object' },
1303
+ data: { type: 'object' },
1304
+ },
1305
+ },
1306
+ },
1307
+ ],
1308
+ },
1309
+ deleteMany: {
1310
+ oneOf: [
1311
+ { type: 'object', description: 'Where filter' },
1312
+ {
1313
+ type: 'array',
1314
+ items: { type: 'object', description: 'Where filter' },
1315
+ },
1316
+ ],
1317
+ },
1318
+ upsert: {
1319
+ oneOf: [
1320
+ {
1321
+ type: 'object',
1322
+ description: '{ where, create, update } triple',
1323
+ properties: {
1324
+ where: { type: 'object' },
1325
+ create: { type: 'object' },
1326
+ update: { type: 'object' },
1327
+ },
1328
+ },
1329
+ {
1330
+ type: 'array',
1331
+ items: {
1332
+ type: 'object',
1333
+ description: '{ where, create, update } triple',
1334
+ properties: {
1335
+ where: { type: 'object' },
1336
+ create: { type: 'object' },
1337
+ update: { type: 'object' },
1338
+ },
1339
+ },
1340
+ },
1341
+ ],
1342
+ },
1343
+ },
1344
+ }
1345
+ }
1346
+ return {
1347
+ type: 'object',
1348
+ description: `Nested ${field.type} write operations for single relation`,
1349
+ properties: {
1350
+ create: { type: 'object', description: `${field.type} create input` },
1351
+ connect: {
1352
+ type: 'object',
1353
+ description: 'Unique identifier to connect',
1354
+ },
1355
+ connectOrCreate: {
1356
+ type: 'object',
1357
+ description: '{ where, create } pair',
1358
+ properties: { where: { type: 'object' }, create: { type: 'object' } },
1359
+ },
1360
+ disconnect: {
1361
+ type: 'boolean',
1362
+ description: 'Disconnect the related record',
1363
+ },
1364
+ delete: { type: 'boolean', description: 'Delete the related record' },
1365
+ update: { type: 'object', description: `${field.type} update input` },
1366
+ upsert: {
1367
+ type: 'object',
1368
+ description:
1369
+ '{ create, update } pair — create if not exists, update if exists',
1370
+ properties: {
1371
+ create: {
1372
+ type: 'object',
1373
+ description: `${field.type} create input`,
1374
+ },
1375
+ update: {
1376
+ type: 'object',
1377
+ description: `${field.type} update input`,
1378
+ },
1379
+ },
1380
+ },
1381
+ },
1382
+ }
1383
+ }
1384
+
1385
+ let schema: SchemaObject | RefObject
1386
+
1387
+ switch (field.kind) {
1388
+ case 'scalar':
1389
+ schema = mapScalarType(field.type)
1390
+ break
1391
+ case 'enum':
1392
+ schema = { $ref: `#/components/schemas/${field.type}` }
1393
+ break
1394
+ default:
1395
+ schema = { type: 'string' }
1396
+ }
1397
+
1398
+ if (field.isList) {
1399
+ schema = { type: 'array', items: schema }
1400
+ }
1401
+
1402
+ if (!field.isRequired && !field.isList) {
1403
+ schema = wrapNullable(schema)
1404
+ }
1405
+
1406
+ if (field.documentation) {
1407
+ if ('$ref' in schema && !('oneOf' in schema) && !('allOf' in schema)) {
1408
+ schema = {
1409
+ allOf: [{ $ref: (schema as RefObject).$ref }],
1410
+ description: field.documentation,
1411
+ }
1412
+ } else if (!('$ref' in schema)) {
1413
+ schema.description = field.documentation
1414
+ }
1415
+ }
1416
+
1417
+ return schema
1418
+ }
1419
+
1420
+ function mapScalarType(type: string): SchemaObject {
1421
+ const typeMap: Record<string, SchemaObject> = {
1422
+ String: { type: 'string' },
1423
+ Int: { type: 'integer', format: 'int32' },
1424
+ BigInt: { type: 'string', description: 'BigInt serialized as string' },
1425
+ Float: { type: 'number', format: 'double' },
1426
+ Decimal: { type: 'string', description: 'Decimal serialized as string' },
1427
+ Boolean: { type: 'boolean' },
1428
+ DateTime: { type: 'string', format: 'date-time' },
1429
+ Json: { description: 'Arbitrary JSON value' },
1430
+ Bytes: {
1431
+ type: 'string',
1432
+ format: 'byte',
1433
+ description: 'Binary data serialized as base64 string',
1434
+ },
1435
+ }
1436
+ return typeMap[type] || { type: 'string' }
1437
+ }
1438
+
1439
+ function yamlEscapeValue(value: unknown, indent: number = 0): string {
1440
+ if (value === null) return 'null'
1441
+ if (value === undefined) return 'null'
1442
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
1443
+ if (typeof value === 'number') return String(value)
1444
+
1445
+ const str = String(value)
1446
+ if (str === '') return "''"
1447
+
1448
+ const needsQuote =
1449
+ str === '~' ||
1450
+ str === '.inf' ||
1451
+ str === '-.inf' ||
1452
+ str === '.nan' ||
1453
+ str.includes(':') ||
1454
+ str.includes('#') ||
1455
+ str.includes('{') ||
1456
+ str.includes('}') ||
1457
+ str.includes('[') ||
1458
+ str.includes(']') ||
1459
+ str.includes(',') ||
1460
+ str.includes('&') ||
1461
+ str.includes('*') ||
1462
+ str.includes('!') ||
1463
+ str.includes('|') ||
1464
+ str.includes('>') ||
1465
+ str.includes("'") ||
1466
+ str.includes('"') ||
1467
+ str.includes('%') ||
1468
+ str.includes('@') ||
1469
+ str.includes('`') ||
1470
+ str.startsWith(' ') ||
1471
+ str.endsWith(' ') ||
1472
+ str === 'true' ||
1473
+ str === 'false' ||
1474
+ str === 'null' ||
1475
+ str === 'yes' ||
1476
+ str === 'no' ||
1477
+ str === 'on' ||
1478
+ str === 'off' ||
1479
+ (!isNaN(Number(str)) && str !== '')
1480
+
1481
+ if (str.includes('\n')) {
1482
+ const blockIndent = ' '.repeat(indent + 1)
1483
+ return (
1484
+ '|\n' +
1485
+ str
1486
+ .split('\n')
1487
+ .map((l) => blockIndent + l)
1488
+ .join('\n')
1489
+ )
1490
+ }
1491
+
1492
+ if (needsQuote) {
1493
+ return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'
1494
+ }
1495
+
1496
+ return str
1497
+ }
1498
+
1499
+ function yamlEscapeKey(key: string): string {
1500
+ if (key === '') return "''"
1501
+
1502
+ const needsQuote =
1503
+ key === '~' ||
1504
+ key === '.inf' ||
1505
+ key === '-.inf' ||
1506
+ key === '.nan' ||
1507
+ key.includes(':') ||
1508
+ key.includes('#') ||
1509
+ key.includes('{') ||
1510
+ key.includes('}') ||
1511
+ key.includes('[') ||
1512
+ key.includes(']') ||
1513
+ key.includes(',') ||
1514
+ key.includes('&') ||
1515
+ key.includes('*') ||
1516
+ key.includes('!') ||
1517
+ key.includes('|') ||
1518
+ key.includes('>') ||
1519
+ key.includes("'") ||
1520
+ key.includes('"') ||
1521
+ key.includes('%') ||
1522
+ key.includes('@') ||
1523
+ key.includes('`') ||
1524
+ key.includes(' ') ||
1525
+ key === 'true' ||
1526
+ key === 'false' ||
1527
+ key === 'null' ||
1528
+ key === 'yes' ||
1529
+ key === 'no' ||
1530
+ key === 'on' ||
1531
+ key === 'off' ||
1532
+ (!isNaN(Number(key)) && key !== '')
1533
+
1534
+ if (needsQuote) {
1535
+ return '"' + key.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'
1536
+ }
1537
+
1538
+ return key
1539
+ }
1540
+
1541
+ function toYaml(obj: any, indent = 0): string {
1542
+ const spaces = ' '.repeat(indent)
1543
+ let yaml = ''
1544
+
1545
+ if (Array.isArray(obj)) {
1546
+ if (obj.length === 0) return `${spaces}[]\n`
1547
+ for (const item of obj) {
1548
+ if (typeof item === 'object' && item !== null) {
1549
+ const inner = toYaml(item, indent + 1).trimStart()
1550
+ yaml += `${spaces}- ${inner}`
1551
+ } else {
1552
+ yaml += `${spaces}- ${yamlEscapeValue(item, indent)}\n`
1553
+ }
1554
+ }
1555
+ } else if (typeof obj === 'object' && obj !== null) {
1556
+ if (Object.keys(obj).length === 0) return `${spaces}{}\n`
1557
+ for (const [key, value] of Object.entries(obj)) {
1558
+ if (value === undefined) continue
1559
+ const safeKey = yamlEscapeKey(key)
1560
+ if (typeof value === 'object' && value !== null) {
1561
+ yaml += `${spaces}${safeKey}:\n${toYaml(value, indent + 1)}`
1562
+ } else {
1563
+ yaml += `${spaces}${safeKey}: ${yamlEscapeValue(value, indent)}\n`
1564
+ }
1565
+ }
1566
+ }
1567
+
1568
+ return yaml
1569
+ }