@orpc/openapi 0.10.0 → 0.12.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/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
@@ -1,3 +0,0 @@
1
- /** unnoq */
2
-
3
- export * from './generator'