@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.
@@ -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 { extractJSONSchema, zodToJsonSchema } from './zod-to-json-schema'
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
- const inputSchema = internal.InputSchema
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
- const schema = inputSchema.properties?.[name]
94
+ let schema = inputSchema.properties?.[name]
91
95
  const required = inputSchema.required?.includes(name)
92
96
 
93
- if (!schema) {
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
- .filter(([name]) => !params?.find((param) => param.name === name))
128
- .map(([name, schema]) => {
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: true,
134
- schema: schema as any,
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 ?? [])]