prisma-generator-express 1.18.0 → 1.19.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 +399 -194
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/client/encodeQueryParams.d.ts +1 -0
- package/dist/client/encodeQueryParams.js +33 -0
- package/dist/client/encodeQueryParams.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/copy/misc.d.ts +5 -0
- package/dist/copy/misc.js +52 -0
- package/dist/copy/misc.js.map +1 -0
- package/dist/generators/generateImportPrismaStatement.d.ts +3 -0
- package/dist/generators/generateImportPrismaStatement.js +55 -0
- package/dist/generators/generateImportPrismaStatement.js.map +1 -0
- package/dist/generators/generateQueryBuilderHelper.d.ts +2 -0
- package/dist/generators/generateQueryBuilderHelper.js +139 -0
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -0
- package/dist/generators/generateRouter.d.ts +6 -0
- package/dist/generators/generateRouter.js +340 -0
- package/dist/generators/generateRouter.js.map +1 -0
- package/dist/generators/generateUnifiedDocs.d.ts +1 -0
- package/dist/generators/generateUnifiedDocs.js +171 -0
- package/dist/generators/generateUnifiedDocs.js.map +1 -0
- package/dist/generators/generateUnifiedHandler.d.ts +6 -0
- package/dist/generators/generateUnifiedHandler.js +444 -0
- package/dist/generators/generateUnifiedHandler.js.map +1 -0
- package/dist/generators/generateUnifiedScalarUI.d.ts +5 -0
- package/dist/generators/generateUnifiedScalarUI.js +1390 -0
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/copyFiles.d.ts +6 -0
- package/dist/utils/copyFiles.js +123 -21
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/strings.d.ts +2 -0
- package/dist/utils/writeFileSafely.d.ts +10 -0
- package/dist/utils/writeFileSafely.js +86 -14
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +64 -31
- package/src/client/encodeQueryParams.ts +56 -0
- package/src/copy/buildModelOpenApi.ts +1569 -0
- package/src/copy/misc.ts +21 -0
- package/src/copy/operationDefinitions.ts +96 -0
- package/src/copy/parseQueryParams.ts +36 -21
- package/src/copy/routeConfig.ts +68 -28
- package/dist/generator.js +0 -47
- package/dist/generator.js.map +0 -1
- package/dist/helpers/generateImportPrismaStatement.js +0 -25
- package/dist/helpers/generateImportPrismaStatement.js.map +0 -1
- package/dist/helpers/generateOperation.js +0 -471
- package/dist/helpers/generateOperation.js.map +0 -1
- package/dist/helpers/generateRouteFile.js +0 -210
- package/dist/helpers/generateRouteFile.js.map +0 -1
- package/dist/utils/formatFile.js +0 -26
- package/dist/utils/formatFile.js.map +0 -1
- package/src/bin.ts +0 -2
- package/src/constants.ts +0 -1
- package/src/copy/encodeQueryParams.spec.ts +0 -303
- package/src/copy/encodeQueryParams.ts +0 -44
- package/src/copy/misc.spec.ts +0 -62
- package/src/copy/parseQueryParams.spec.ts +0 -187
- package/src/copy/transformZod.spec.ts +0 -763
- package/src/generator.ts +0 -54
- package/src/helpers/generateImportPrismaStatement.ts +0 -38
- package/src/helpers/generateOperation.ts +0 -515
- package/src/helpers/generateRouteFile.ts +0 -213
- package/src/utils/copyFiles.ts +0 -27
- package/src/utils/formatFile.ts +0 -22
- package/src/utils/strings.ts +0 -7
- package/src/utils/writeFileSafely.ts +0 -29
|
@@ -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
|
+
}
|