@orpc/openapi 0.0.3 → 0.0.4
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/LICENSE +21 -0
- package/dist/index.js +59 -14
- package/dist/index.js.map +1 -1
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/generator.test.ts +178 -0
- package/src/generator.ts +80 -11
package/src/generator.test.ts
CHANGED
|
@@ -463,3 +463,181 @@ it('work with example', () => {
|
|
|
463
463
|
},
|
|
464
464
|
})
|
|
465
465
|
})
|
|
466
|
+
|
|
467
|
+
it('should remove params on body', () => {
|
|
468
|
+
const router = oc.router({
|
|
469
|
+
upload: oc.route({ method: 'POST', path: '/upload/{id}' }).input(
|
|
470
|
+
oz.openapi(
|
|
471
|
+
z.object({
|
|
472
|
+
id: z.number(),
|
|
473
|
+
file: z.string().url(),
|
|
474
|
+
}),
|
|
475
|
+
{
|
|
476
|
+
examples: [
|
|
477
|
+
{
|
|
478
|
+
id: 123,
|
|
479
|
+
file: 'https://example.com/file.png',
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
),
|
|
484
|
+
),
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const spec = generateOpenAPI({
|
|
488
|
+
router,
|
|
489
|
+
info: {
|
|
490
|
+
title: 'test',
|
|
491
|
+
version: '1.0.0',
|
|
492
|
+
},
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
expect(spec).toEqual({
|
|
496
|
+
info: { title: 'test', version: '1.0.0' },
|
|
497
|
+
openapi: '3.1.0',
|
|
498
|
+
paths: {
|
|
499
|
+
'/upload/{id}': {
|
|
500
|
+
post: {
|
|
501
|
+
summary: undefined,
|
|
502
|
+
description: undefined,
|
|
503
|
+
deprecated: undefined,
|
|
504
|
+
tags: undefined,
|
|
505
|
+
operationId: 'upload',
|
|
506
|
+
parameters: [
|
|
507
|
+
{
|
|
508
|
+
name: 'id',
|
|
509
|
+
in: 'path',
|
|
510
|
+
required: true,
|
|
511
|
+
schema: { examples: [123], type: 'number' },
|
|
512
|
+
example: undefined,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
requestBody: {
|
|
516
|
+
required: false,
|
|
517
|
+
content: {
|
|
518
|
+
'application/json': {
|
|
519
|
+
schema: {
|
|
520
|
+
type: 'object',
|
|
521
|
+
properties: { file: { type: 'string', format: 'uri' } },
|
|
522
|
+
required: ['file'],
|
|
523
|
+
examples: [{ file: 'https://example.com/file.png' }],
|
|
524
|
+
},
|
|
525
|
+
example: undefined,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
responses: {
|
|
530
|
+
'200': {
|
|
531
|
+
description: 'OK',
|
|
532
|
+
content: {
|
|
533
|
+
'application/json': { schema: {}, example: undefined },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('should remove params on query', () => {
|
|
544
|
+
const router = oc.router({
|
|
545
|
+
upload: oc.route({ method: 'GET', path: '/upload/{id}' }).input(
|
|
546
|
+
oz.openapi(
|
|
547
|
+
z.object({
|
|
548
|
+
id: z.number(),
|
|
549
|
+
file: z.string().url(),
|
|
550
|
+
object: z
|
|
551
|
+
.object({
|
|
552
|
+
name: z.string(),
|
|
553
|
+
})
|
|
554
|
+
.optional(),
|
|
555
|
+
}),
|
|
556
|
+
{
|
|
557
|
+
examples: [
|
|
558
|
+
{
|
|
559
|
+
id: 123,
|
|
560
|
+
file: 'https://example.com/file.png',
|
|
561
|
+
object: { name: 'test' },
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 456,
|
|
565
|
+
file: 'https://example.com/file2.png',
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
),
|
|
570
|
+
),
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const spec = generateOpenAPI({
|
|
574
|
+
router,
|
|
575
|
+
info: {
|
|
576
|
+
title: 'test',
|
|
577
|
+
version: '1.0.0',
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
expect(spec).toEqual({
|
|
582
|
+
info: { title: 'test', version: '1.0.0' },
|
|
583
|
+
openapi: '3.1.0',
|
|
584
|
+
paths: {
|
|
585
|
+
'/upload/{id}': {
|
|
586
|
+
get: {
|
|
587
|
+
summary: undefined,
|
|
588
|
+
description: undefined,
|
|
589
|
+
deprecated: undefined,
|
|
590
|
+
tags: undefined,
|
|
591
|
+
operationId: 'upload',
|
|
592
|
+
parameters: [
|
|
593
|
+
{
|
|
594
|
+
name: 'id',
|
|
595
|
+
in: 'path',
|
|
596
|
+
required: true,
|
|
597
|
+
schema: { examples: [123, 456], type: 'number' },
|
|
598
|
+
example: undefined,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'file',
|
|
602
|
+
in: 'query',
|
|
603
|
+
style: 'deepObject',
|
|
604
|
+
required: true,
|
|
605
|
+
schema: {
|
|
606
|
+
examples: [
|
|
607
|
+
'https://example.com/file.png',
|
|
608
|
+
'https://example.com/file2.png',
|
|
609
|
+
],
|
|
610
|
+
type: 'string',
|
|
611
|
+
format: 'uri',
|
|
612
|
+
},
|
|
613
|
+
example: undefined,
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'object',
|
|
617
|
+
in: 'query',
|
|
618
|
+
style: 'deepObject',
|
|
619
|
+
required: false,
|
|
620
|
+
schema: {
|
|
621
|
+
examples: [{ name: 'test' }],
|
|
622
|
+
anyOf: undefined,
|
|
623
|
+
type: 'object',
|
|
624
|
+
properties: { name: { type: 'string' } },
|
|
625
|
+
required: ['name'],
|
|
626
|
+
},
|
|
627
|
+
example: undefined,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
requestBody: undefined,
|
|
631
|
+
responses: {
|
|
632
|
+
'200': {
|
|
633
|
+
description: 'OK',
|
|
634
|
+
content: {
|
|
635
|
+
'application/json': { schema: {}, example: undefined },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
})
|
package/src/generator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ContractRouter, eachContractRouterLeaf } from '@orpc/contract'
|
|
2
2
|
import { type Router, toContractRouter } from '@orpc/server'
|
|
3
|
-
import { findDeepMatches, omit } from '@orpc/shared'
|
|
3
|
+
import { findDeepMatches, isPlainObject, omit } from '@orpc/shared'
|
|
4
4
|
import { preSerialize } from '@orpc/transformer'
|
|
5
5
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
6
6
|
import {
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
type RequestBodyObject,
|
|
13
13
|
type ResponseObject,
|
|
14
14
|
} from 'openapi3-ts/oas31'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
UNSUPPORTED_JSON_SCHEMA,
|
|
17
|
+
extractJSONSchema,
|
|
18
|
+
zodToJsonSchema,
|
|
19
|
+
} from './zod-to-json-schema'
|
|
16
20
|
|
|
17
21
|
// Reference: https://spec.openapis.org/oas/v3.1.0.html#style-values
|
|
18
22
|
|
|
@@ -64,7 +68,7 @@ export function generateOpenAPI(
|
|
|
64
68
|
const path = internal.path ?? `/${path_.map(encodeURIComponent).join('/')}`
|
|
65
69
|
const method = internal.method ?? 'POST'
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
let inputSchema = internal.InputSchema
|
|
68
72
|
? zodToJsonSchema(internal.InputSchema, { mode: 'input' })
|
|
69
73
|
: {}
|
|
70
74
|
const outputSchema = internal.OutputSchema
|
|
@@ -87,10 +91,10 @@ export function generateOpenAPI(
|
|
|
87
91
|
return names
|
|
88
92
|
.map((raw) => raw.slice(1, -1))
|
|
89
93
|
.map((name) => {
|
|
90
|
-
|
|
94
|
+
let schema = inputSchema.properties?.[name]
|
|
91
95
|
const required = inputSchema.required?.includes(name)
|
|
92
96
|
|
|
93
|
-
if (
|
|
97
|
+
if (schema === undefined) {
|
|
94
98
|
throw new Error(
|
|
95
99
|
`Parameter ${name} is missing in input schema [${path_.join('.')}]`,
|
|
96
100
|
)
|
|
@@ -102,6 +106,54 @@ export function generateOpenAPI(
|
|
|
102
106
|
)
|
|
103
107
|
}
|
|
104
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)) return example
|
|
143
|
+
|
|
144
|
+
return Object.entries(example).reduce(
|
|
145
|
+
(acc, [key, value]) => {
|
|
146
|
+
if (key !== name) {
|
|
147
|
+
acc[key] = value
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return acc
|
|
151
|
+
},
|
|
152
|
+
{} as Record<string, unknown>,
|
|
153
|
+
)
|
|
154
|
+
}),
|
|
155
|
+
}
|
|
156
|
+
|
|
105
157
|
return {
|
|
106
158
|
name,
|
|
107
159
|
in: 'path',
|
|
@@ -123,18 +175,35 @@ export function generateOpenAPI(
|
|
|
123
175
|
)
|
|
124
176
|
}
|
|
125
177
|
|
|
126
|
-
return Object.entries(inputSchema.properties ?? {})
|
|
127
|
-
|
|
128
|
-
|
|
178
|
+
return Object.entries(inputSchema.properties ?? {}).map(
|
|
179
|
+
([name, schema]) => {
|
|
180
|
+
const examples = inputSchema.examples
|
|
181
|
+
?.filter((example) => {
|
|
182
|
+
return isPlainObject(example) && name in example
|
|
183
|
+
})
|
|
184
|
+
.map((example) => {
|
|
185
|
+
return example[name]
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const schema_ = {
|
|
189
|
+
examples: examples?.length ? examples : undefined,
|
|
190
|
+
...(schema === true
|
|
191
|
+
? {}
|
|
192
|
+
: schema === false
|
|
193
|
+
? UNSUPPORTED_JSON_SCHEMA
|
|
194
|
+
: schema),
|
|
195
|
+
}
|
|
196
|
+
|
|
129
197
|
return {
|
|
130
198
|
name,
|
|
131
199
|
in: 'query',
|
|
132
200
|
style: 'deepObject',
|
|
133
|
-
required:
|
|
134
|
-
schema:
|
|
201
|
+
required: inputSchema?.required?.includes(name) ?? false,
|
|
202
|
+
schema: schema_ as any,
|
|
135
203
|
example: internal.inputExample?.[name],
|
|
136
204
|
}
|
|
137
|
-
}
|
|
205
|
+
},
|
|
206
|
+
)
|
|
138
207
|
})()
|
|
139
208
|
|
|
140
209
|
const parameters = [...(params ?? []), ...(query ?? [])]
|