@orpc/openapi 0.10.0 → 0.11.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/dist/index.js +0 -1
- package/dist/src/generator.d.ts +0 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/zod-to-json-schema.d.ts +0 -1
- package/package.json +13 -17
- package/dist/index.js.map +0 -1
- package/dist/src/generator.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/zod-to-json-schema.d.ts.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/generator.test.ts +0 -643
- package/src/generator.ts +0 -338
- package/src/index.ts +0 -3
- package/src/zod-to-json-schema.test.ts +0 -391
- package/src/zod-to-json-schema.ts +0 -655
package/src/generator.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
2
|
-
import { type ContractRouter, eachContractRouterLeaf } from '@orpc/contract'
|
|
3
|
-
import { type Router, toContractRouter } from '@orpc/server'
|
|
4
|
-
import { findDeepMatches, isPlainObject, omit } from '@orpc/shared'
|
|
5
|
-
import { preSerialize } from '@orpc/transformer'
|
|
6
|
-
import {
|
|
7
|
-
type MediaTypeObject,
|
|
8
|
-
OpenApiBuilder,
|
|
9
|
-
type OpenAPIObject,
|
|
10
|
-
type OperationObject,
|
|
11
|
-
type ParameterObject,
|
|
12
|
-
type RequestBodyObject,
|
|
13
|
-
type ResponseObject,
|
|
14
|
-
} from 'openapi3-ts/oas31'
|
|
15
|
-
import {
|
|
16
|
-
extractJSONSchema,
|
|
17
|
-
UNSUPPORTED_JSON_SCHEMA,
|
|
18
|
-
zodToJsonSchema,
|
|
19
|
-
} from './zod-to-json-schema'
|
|
20
|
-
|
|
21
|
-
// Reference: https://spec.openapis.org/oas/v3.1.0.html#style-values
|
|
22
|
-
|
|
23
|
-
export interface GenerateOpenAPIOptions {
|
|
24
|
-
/**
|
|
25
|
-
* Throw error when you missing define tag definition on OpenAPI root tags
|
|
26
|
-
*
|
|
27
|
-
* Example: if procedure has tags ['foo', 'bar'], and OpenAPI root tags is ['foo'], then error will be thrown
|
|
28
|
-
* Because OpenAPI root tags is missing 'bar' tag
|
|
29
|
-
*
|
|
30
|
-
* @default false
|
|
31
|
-
*/
|
|
32
|
-
throwOnMissingTagDefinition?: boolean
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Weather ignore procedures that has no path defined.
|
|
36
|
-
*
|
|
37
|
-
* @default false
|
|
38
|
-
*/
|
|
39
|
-
ignoreUndefinedPathProcedures?: boolean
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function generateOpenAPI(
|
|
43
|
-
opts: {
|
|
44
|
-
router: ContractRouter | Router<any>
|
|
45
|
-
} & Omit<OpenAPIObject, 'openapi'>,
|
|
46
|
-
options?: GenerateOpenAPIOptions,
|
|
47
|
-
): OpenAPIObject {
|
|
48
|
-
const throwOnMissingTagDefinition
|
|
49
|
-
= options?.throwOnMissingTagDefinition ?? false
|
|
50
|
-
const ignoreUndefinedPathProcedures
|
|
51
|
-
= options?.ignoreUndefinedPathProcedures ?? false
|
|
52
|
-
|
|
53
|
-
const builder = new OpenApiBuilder({
|
|
54
|
-
...omit(opts, ['router']),
|
|
55
|
-
openapi: '3.1.0',
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const rootTags = opts.tags?.map(tag => tag.name) ?? []
|
|
59
|
-
const router = toContractRouter(opts.router)
|
|
60
|
-
|
|
61
|
-
eachContractRouterLeaf(router, (procedure, path_) => {
|
|
62
|
-
const internal = procedure.zz$cp
|
|
63
|
-
|
|
64
|
-
if (ignoreUndefinedPathProcedures && internal.path === undefined) {
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const path = internal.path ?? `/${path_.map(encodeURIComponent).join('/')}`
|
|
69
|
-
const method = internal.method ?? 'POST'
|
|
70
|
-
|
|
71
|
-
let inputSchema = internal.InputSchema
|
|
72
|
-
? zodToJsonSchema(internal.InputSchema, { mode: 'input' })
|
|
73
|
-
: {}
|
|
74
|
-
const outputSchema = internal.OutputSchema
|
|
75
|
-
? zodToJsonSchema(internal.OutputSchema, { mode: 'output' })
|
|
76
|
-
: {}
|
|
77
|
-
|
|
78
|
-
const params: ParameterObject[] | undefined = (() => {
|
|
79
|
-
const names = path.match(/\{([^}]+)\}/g)
|
|
80
|
-
|
|
81
|
-
if (!names || !names.length) {
|
|
82
|
-
return undefined
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (typeof inputSchema !== 'object' || inputSchema.type !== 'object') {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`When path has parameters, input schema must be an object [${path_.join('.')}]`,
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return names
|
|
92
|
-
.map(raw => raw.slice(1, -1))
|
|
93
|
-
.map((name) => {
|
|
94
|
-
let schema = inputSchema.properties?.[name]
|
|
95
|
-
const required = inputSchema.required?.includes(name)
|
|
96
|
-
|
|
97
|
-
if (schema === undefined) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Parameter ${name} is missing in input schema [${path_.join('.')}]`,
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (!required) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Parameter ${name} must be required in input schema [${path_.join('.')}]`,
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const examples = inputSchema.examples
|
|
110
|
-
?.filter((example) => {
|
|
111
|
-
return isPlainObject(example) && name in example
|
|
112
|
-
})
|
|
113
|
-
.map((example) => {
|
|
114
|
-
return example[name]
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
schema = {
|
|
118
|
-
examples: examples?.length ? examples : undefined,
|
|
119
|
-
...(schema === true
|
|
120
|
-
? {}
|
|
121
|
-
: schema === false
|
|
122
|
-
? UNSUPPORTED_JSON_SCHEMA
|
|
123
|
-
: schema),
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
inputSchema = {
|
|
127
|
-
...inputSchema,
|
|
128
|
-
properties: inputSchema.properties
|
|
129
|
-
? Object.entries(inputSchema.properties).reduce(
|
|
130
|
-
(acc, [key, value]) => {
|
|
131
|
-
if (key !== name) {
|
|
132
|
-
acc[key] = value
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return acc
|
|
136
|
-
},
|
|
137
|
-
{} as Record<string, JSONSchema>,
|
|
138
|
-
)
|
|
139
|
-
: undefined,
|
|
140
|
-
required: inputSchema.required?.filter(v => v !== name),
|
|
141
|
-
examples: inputSchema.examples?.map((example) => {
|
|
142
|
-
if (!isPlainObject(example))
|
|
143
|
-
return example
|
|
144
|
-
|
|
145
|
-
return Object.entries(example).reduce(
|
|
146
|
-
(acc, [key, value]) => {
|
|
147
|
-
if (key !== name) {
|
|
148
|
-
acc[key] = value
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return acc
|
|
152
|
-
},
|
|
153
|
-
{} as Record<string, unknown>,
|
|
154
|
-
)
|
|
155
|
-
}),
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
name,
|
|
160
|
-
in: 'path',
|
|
161
|
-
required: true,
|
|
162
|
-
schema: schema as any,
|
|
163
|
-
example: internal.inputExample?.[name],
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
})()
|
|
167
|
-
|
|
168
|
-
const query: ParameterObject[] | undefined = (() => {
|
|
169
|
-
if (method !== 'GET' || Object.keys(inputSchema).length === 0) {
|
|
170
|
-
return undefined
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (typeof inputSchema !== 'object' || inputSchema.type !== 'object') {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`When method is GET, input schema must be an object [${path_.join('.')}]`,
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return Object.entries(inputSchema.properties ?? {}).map(
|
|
180
|
-
([name, schema]) => {
|
|
181
|
-
const examples = inputSchema.examples
|
|
182
|
-
?.filter((example) => {
|
|
183
|
-
return isPlainObject(example) && name in example
|
|
184
|
-
})
|
|
185
|
-
.map((example) => {
|
|
186
|
-
return example[name]
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const schema_ = {
|
|
190
|
-
examples: examples?.length ? examples : undefined,
|
|
191
|
-
...(schema === true
|
|
192
|
-
? {}
|
|
193
|
-
: schema === false
|
|
194
|
-
? UNSUPPORTED_JSON_SCHEMA
|
|
195
|
-
: schema),
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
name,
|
|
200
|
-
in: 'query',
|
|
201
|
-
style: 'deepObject',
|
|
202
|
-
required: inputSchema?.required?.includes(name) ?? false,
|
|
203
|
-
schema: schema_ as any,
|
|
204
|
-
example: internal.inputExample?.[name],
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
)
|
|
208
|
-
})()
|
|
209
|
-
|
|
210
|
-
const parameters = [...(params ?? []), ...(query ?? [])]
|
|
211
|
-
|
|
212
|
-
const requestBody: RequestBodyObject | undefined = (() => {
|
|
213
|
-
if (method === 'GET') {
|
|
214
|
-
return undefined
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const { schema, matches } = extractJSONSchema(inputSchema, isFileSchema)
|
|
218
|
-
|
|
219
|
-
const files = matches as (JSONSchema & {
|
|
220
|
-
type: 'string'
|
|
221
|
-
contentMediaType: string
|
|
222
|
-
})[]
|
|
223
|
-
|
|
224
|
-
const isStillHasFileSchema
|
|
225
|
-
= findDeepMatches(isFileSchema, schema).values.length > 0
|
|
226
|
-
|
|
227
|
-
if (files.length) {
|
|
228
|
-
parameters.push({
|
|
229
|
-
name: 'content-disposition',
|
|
230
|
-
in: 'header',
|
|
231
|
-
required: schema === undefined,
|
|
232
|
-
schema: {
|
|
233
|
-
type: 'string',
|
|
234
|
-
pattern: 'filename',
|
|
235
|
-
example: 'filename="file.png"',
|
|
236
|
-
description:
|
|
237
|
-
'To define the file name. Required when the request body is a file.',
|
|
238
|
-
},
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const content: Record<string, MediaTypeObject> = {}
|
|
243
|
-
|
|
244
|
-
for (const file of files) {
|
|
245
|
-
content[file.contentMediaType] = {
|
|
246
|
-
schema: file as any,
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (schema !== undefined) {
|
|
251
|
-
content[
|
|
252
|
-
isStillHasFileSchema ? 'multipart/form-data' : 'application/json'
|
|
253
|
-
] = {
|
|
254
|
-
schema: schema as any,
|
|
255
|
-
example: internal.inputExample,
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
required: Boolean(internal.InputSchema?.isOptional()),
|
|
261
|
-
content,
|
|
262
|
-
}
|
|
263
|
-
})()
|
|
264
|
-
|
|
265
|
-
const successResponse: ResponseObject = (() => {
|
|
266
|
-
const { schema, matches } = extractJSONSchema(outputSchema, isFileSchema)
|
|
267
|
-
const files = matches as (JSONSchema & {
|
|
268
|
-
type: 'string'
|
|
269
|
-
contentMediaType: string
|
|
270
|
-
})[]
|
|
271
|
-
|
|
272
|
-
const isStillHasFileSchema
|
|
273
|
-
= findDeepMatches(isFileSchema, schema).values.length > 0
|
|
274
|
-
|
|
275
|
-
const content: Record<string, MediaTypeObject> = {}
|
|
276
|
-
|
|
277
|
-
for (const file of files) {
|
|
278
|
-
content[file.contentMediaType] = {
|
|
279
|
-
schema: file as any,
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (schema !== undefined) {
|
|
284
|
-
content[
|
|
285
|
-
isStillHasFileSchema ? 'multipart/form-data' : 'application/json'
|
|
286
|
-
] = {
|
|
287
|
-
schema: schema as any,
|
|
288
|
-
example: internal.outputExample,
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
description: 'OK',
|
|
294
|
-
content,
|
|
295
|
-
}
|
|
296
|
-
})()
|
|
297
|
-
|
|
298
|
-
if (throwOnMissingTagDefinition && internal.tags) {
|
|
299
|
-
const missingTag = internal.tags.find(tag => !rootTags.includes(tag))
|
|
300
|
-
|
|
301
|
-
if (missingTag !== undefined) {
|
|
302
|
-
throw new Error(
|
|
303
|
-
`Tag "${missingTag}" is missing definition. Please define it in OpenAPI root tags object. [${path_.join('.')}]`,
|
|
304
|
-
)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const operation: OperationObject = {
|
|
309
|
-
summary: internal.summary,
|
|
310
|
-
description: internal.description,
|
|
311
|
-
deprecated: internal.deprecated,
|
|
312
|
-
tags: internal.tags,
|
|
313
|
-
operationId: path_.join('.'),
|
|
314
|
-
parameters: parameters.length ? parameters : undefined,
|
|
315
|
-
requestBody,
|
|
316
|
-
responses: {
|
|
317
|
-
200: successResponse,
|
|
318
|
-
},
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
builder.addPath(path, {
|
|
322
|
-
[method.toLocaleLowerCase()]: operation,
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
return preSerialize(builder.getSpec()) as OpenAPIObject
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function isFileSchema(schema: unknown) {
|
|
330
|
-
if (typeof schema !== 'object' || schema === null)
|
|
331
|
-
return false
|
|
332
|
-
return (
|
|
333
|
-
'type' in schema
|
|
334
|
-
&& 'contentMediaType' in schema
|
|
335
|
-
&& typeof schema.type === 'string'
|
|
336
|
-
&& typeof schema.contentMediaType === 'string'
|
|
337
|
-
)
|
|
338
|
-
}
|
package/src/index.ts
DELETED