@orpc/openapi 0.0.4 → 0.0.5
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 +6 -8
- package/dist/index.js.map +1 -1
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/zod-to-json-schema.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/generator.test.ts +10 -10
- package/src/generator.ts +34 -32
- package/src/zod-to-json-schema.test.ts +1 -1
- package/src/zod-to-json-schema.ts +22 -33
package/src/generator.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
1
2
|
import { type ContractRouter, eachContractRouterLeaf } from '@orpc/contract'
|
|
2
3
|
import { type Router, toContractRouter } from '@orpc/server'
|
|
3
4
|
import { findDeepMatches, isPlainObject, omit } from '@orpc/shared'
|
|
4
5
|
import { preSerialize } from '@orpc/transformer'
|
|
5
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
6
6
|
import {
|
|
7
7
|
type MediaTypeObject,
|
|
8
|
-
type OpenAPIObject,
|
|
9
8
|
OpenApiBuilder,
|
|
9
|
+
type OpenAPIObject,
|
|
10
10
|
type OperationObject,
|
|
11
11
|
type ParameterObject,
|
|
12
12
|
type RequestBodyObject,
|
|
13
13
|
type ResponseObject,
|
|
14
14
|
} from 'openapi3-ts/oas31'
|
|
15
15
|
import {
|
|
16
|
-
UNSUPPORTED_JSON_SCHEMA,
|
|
17
16
|
extractJSONSchema,
|
|
17
|
+
UNSUPPORTED_JSON_SCHEMA,
|
|
18
18
|
zodToJsonSchema,
|
|
19
19
|
} from './zod-to-json-schema'
|
|
20
20
|
|
|
@@ -45,17 +45,17 @@ export function generateOpenAPI(
|
|
|
45
45
|
} & Omit<OpenAPIObject, 'openapi'>,
|
|
46
46
|
options?: GenerateOpenAPIOptions,
|
|
47
47
|
): OpenAPIObject {
|
|
48
|
-
const throwOnMissingTagDefinition
|
|
49
|
-
options?.throwOnMissingTagDefinition ?? false
|
|
50
|
-
const ignoreUndefinedPathProcedures
|
|
51
|
-
options?.ignoreUndefinedPathProcedures ?? false
|
|
48
|
+
const throwOnMissingTagDefinition
|
|
49
|
+
= options?.throwOnMissingTagDefinition ?? false
|
|
50
|
+
const ignoreUndefinedPathProcedures
|
|
51
|
+
= options?.ignoreUndefinedPathProcedures ?? false
|
|
52
52
|
|
|
53
53
|
const builder = new OpenApiBuilder({
|
|
54
54
|
...omit(opts, ['router']),
|
|
55
55
|
openapi: '3.1.0',
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
const rootTags = opts.tags?.map(
|
|
58
|
+
const rootTags = opts.tags?.map(tag => tag.name) ?? []
|
|
59
59
|
const router = toContractRouter(opts.router)
|
|
60
60
|
|
|
61
61
|
eachContractRouterLeaf(router, (procedure, path_) => {
|
|
@@ -76,7 +76,7 @@ export function generateOpenAPI(
|
|
|
76
76
|
: {}
|
|
77
77
|
|
|
78
78
|
const params: ParameterObject[] | undefined = (() => {
|
|
79
|
-
const names = path.match(
|
|
79
|
+
const names = path.match(/\{([^}]+)\}/g)
|
|
80
80
|
|
|
81
81
|
if (!names || !names.length) {
|
|
82
82
|
return undefined
|
|
@@ -89,7 +89,7 @@ export function generateOpenAPI(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
return names
|
|
92
|
-
.map(
|
|
92
|
+
.map(raw => raw.slice(1, -1))
|
|
93
93
|
.map((name) => {
|
|
94
94
|
let schema = inputSchema.properties?.[name]
|
|
95
95
|
const required = inputSchema.required?.includes(name)
|
|
@@ -127,19 +127,20 @@ export function generateOpenAPI(
|
|
|
127
127
|
...inputSchema,
|
|
128
128
|
properties: inputSchema.properties
|
|
129
129
|
? Object.entries(inputSchema.properties).reduce(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
139
|
: undefined,
|
|
140
|
-
required: inputSchema.required?.filter(
|
|
140
|
+
required: inputSchema.required?.filter(v => v !== name),
|
|
141
141
|
examples: inputSchema.examples?.map((example) => {
|
|
142
|
-
if (!isPlainObject(example))
|
|
142
|
+
if (!isPlainObject(example))
|
|
143
|
+
return example
|
|
143
144
|
|
|
144
145
|
return Object.entries(example).reduce(
|
|
145
146
|
(acc, [key, value]) => {
|
|
@@ -220,8 +221,8 @@ export function generateOpenAPI(
|
|
|
220
221
|
contentMediaType: string
|
|
221
222
|
})[]
|
|
222
223
|
|
|
223
|
-
const isStillHasFileSchema
|
|
224
|
-
findDeepMatches(isFileSchema, schema).values.length > 0
|
|
224
|
+
const isStillHasFileSchema
|
|
225
|
+
= findDeepMatches(isFileSchema, schema).values.length > 0
|
|
225
226
|
|
|
226
227
|
if (files.length) {
|
|
227
228
|
parameters.push({
|
|
@@ -268,8 +269,8 @@ export function generateOpenAPI(
|
|
|
268
269
|
contentMediaType: string
|
|
269
270
|
})[]
|
|
270
271
|
|
|
271
|
-
const isStillHasFileSchema
|
|
272
|
-
findDeepMatches(isFileSchema, schema).values.length > 0
|
|
272
|
+
const isStillHasFileSchema
|
|
273
|
+
= findDeepMatches(isFileSchema, schema).values.length > 0
|
|
273
274
|
|
|
274
275
|
const content: Record<string, MediaTypeObject> = {}
|
|
275
276
|
|
|
@@ -295,7 +296,7 @@ export function generateOpenAPI(
|
|
|
295
296
|
})()
|
|
296
297
|
|
|
297
298
|
if (throwOnMissingTagDefinition && internal.tags) {
|
|
298
|
-
const missingTag = internal.tags.find(
|
|
299
|
+
const missingTag = internal.tags.find(tag => !rootTags.includes(tag))
|
|
299
300
|
|
|
300
301
|
if (missingTag !== undefined) {
|
|
301
302
|
throw new Error(
|
|
@@ -313,7 +314,7 @@ export function generateOpenAPI(
|
|
|
313
314
|
parameters: parameters.length ? parameters : undefined,
|
|
314
315
|
requestBody,
|
|
315
316
|
responses: {
|
|
316
|
-
|
|
317
|
+
200: successResponse,
|
|
317
318
|
},
|
|
318
319
|
}
|
|
319
320
|
|
|
@@ -326,11 +327,12 @@ export function generateOpenAPI(
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
function isFileSchema(schema: unknown) {
|
|
329
|
-
if (typeof schema !== 'object' || schema === null)
|
|
330
|
+
if (typeof schema !== 'object' || schema === null)
|
|
331
|
+
return false
|
|
330
332
|
return (
|
|
331
|
-
'type' in schema
|
|
332
|
-
'contentMediaType' in schema
|
|
333
|
-
typeof schema.type === 'string'
|
|
334
|
-
typeof schema.contentMediaType === 'string'
|
|
333
|
+
'type' in schema
|
|
334
|
+
&& 'contentMediaType' in schema
|
|
335
|
+
&& typeof schema.type === 'string'
|
|
336
|
+
&& typeof schema.contentMediaType === 'string'
|
|
335
337
|
)
|
|
336
338
|
}
|
|
@@ -244,7 +244,7 @@ describe('special types', () => {
|
|
|
244
244
|
|
|
245
245
|
describe('transform and effects', () => {
|
|
246
246
|
it('should handle transform effects based on mode', () => {
|
|
247
|
-
const schema = z.string().transform(
|
|
247
|
+
const schema = z.string().transform(val => val.length)
|
|
248
248
|
|
|
249
249
|
expect(zodToJsonSchema(schema, { mode: 'input' })).toEqual({
|
|
250
250
|
type: 'string',
|
|
@@ -14,11 +14,8 @@ import {
|
|
|
14
14
|
type KeySchema,
|
|
15
15
|
type ZodAny,
|
|
16
16
|
type ZodArray,
|
|
17
|
-
type ZodBigInt,
|
|
18
|
-
type ZodBoolean,
|
|
19
17
|
type ZodBranded,
|
|
20
18
|
type ZodCatch,
|
|
21
|
-
type ZodDate,
|
|
22
19
|
type ZodDefault,
|
|
23
20
|
type ZodDiscriminatedUnion,
|
|
24
21
|
type ZodEffects,
|
|
@@ -28,7 +25,6 @@ import {
|
|
|
28
25
|
type ZodLazy,
|
|
29
26
|
type ZodLiteral,
|
|
30
27
|
type ZodMap,
|
|
31
|
-
type ZodNaN,
|
|
32
28
|
type ZodNativeEnum,
|
|
33
29
|
type ZodNullable,
|
|
34
30
|
type ZodNumber,
|
|
@@ -213,8 +209,8 @@ export function zodToJsonSchema(
|
|
|
213
209
|
json.pattern = `${escapeStringRegexp(check.value)}$`
|
|
214
210
|
break
|
|
215
211
|
case 'emoji':
|
|
216
|
-
json.pattern
|
|
217
|
-
'^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$'
|
|
212
|
+
json.pattern
|
|
213
|
+
= '^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$'
|
|
218
214
|
break
|
|
219
215
|
case 'nanoid':
|
|
220
216
|
json.pattern = '^[a-zA-Z0-9_-]{21}$'
|
|
@@ -278,14 +274,10 @@ export function zodToJsonSchema(
|
|
|
278
274
|
}
|
|
279
275
|
|
|
280
276
|
case ZodFirstPartyTypeKind.ZodNaN: {
|
|
281
|
-
const schema_ = schema as ZodNaN
|
|
282
|
-
|
|
283
277
|
return { const: 'NaN' }
|
|
284
278
|
}
|
|
285
279
|
|
|
286
280
|
case ZodFirstPartyTypeKind.ZodBigInt: {
|
|
287
|
-
const schema_ = schema as ZodBigInt
|
|
288
|
-
|
|
289
281
|
const json: JSONSchema = { type: 'string', pattern: '^-?[0-9]+$' }
|
|
290
282
|
|
|
291
283
|
// WARN: ignore checks
|
|
@@ -294,14 +286,10 @@ export function zodToJsonSchema(
|
|
|
294
286
|
}
|
|
295
287
|
|
|
296
288
|
case ZodFirstPartyTypeKind.ZodBoolean: {
|
|
297
|
-
const schema_ = schema as ZodBoolean
|
|
298
|
-
|
|
299
289
|
return { type: 'boolean' }
|
|
300
290
|
}
|
|
301
291
|
|
|
302
292
|
case ZodFirstPartyTypeKind.ZodDate: {
|
|
303
|
-
const schema_ = schema as ZodDate
|
|
304
|
-
|
|
305
293
|
const jsonSchema: JSONSchema = { type: 'string', format: Format.Date }
|
|
306
294
|
|
|
307
295
|
// WARN: ignore checks
|
|
@@ -398,7 +386,7 @@ export function zodToJsonSchema(
|
|
|
398
386
|
for (const [key, value] of Object.entries(schema_.shape)) {
|
|
399
387
|
const { schema, matches } = extractJSONSchema(
|
|
400
388
|
zodToJsonSchema(value, childOptions),
|
|
401
|
-
|
|
389
|
+
schema => schema === UNDEFINED_JSON_SCHEMA,
|
|
402
390
|
)
|
|
403
391
|
|
|
404
392
|
if (schema) {
|
|
@@ -423,14 +411,15 @@ export function zodToJsonSchema(
|
|
|
423
411
|
childOptions,
|
|
424
412
|
)
|
|
425
413
|
if (schema_._def.unknownKeys === 'strict') {
|
|
426
|
-
json.additionalProperties
|
|
427
|
-
additionalProperties === UNSUPPORTED_JSON_SCHEMA
|
|
414
|
+
json.additionalProperties
|
|
415
|
+
= additionalProperties === UNSUPPORTED_JSON_SCHEMA
|
|
428
416
|
? false
|
|
429
417
|
: additionalProperties
|
|
430
|
-
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
431
420
|
if (
|
|
432
|
-
additionalProperties
|
|
433
|
-
additionalProperties !== UNSUPPORTED_JSON_SCHEMA
|
|
421
|
+
additionalProperties
|
|
422
|
+
&& additionalProperties !== UNSUPPORTED_JSON_SCHEMA
|
|
434
423
|
) {
|
|
435
424
|
json.additionalProperties = additionalProperties
|
|
436
425
|
}
|
|
@@ -551,8 +540,8 @@ export function zodToJsonSchema(
|
|
|
551
540
|
const schema_ = schema as ZodEffects<ZodTypeAny>
|
|
552
541
|
|
|
553
542
|
if (
|
|
554
|
-
schema_._def.effect.type === 'transform'
|
|
555
|
-
childOptions?.mode === 'output'
|
|
543
|
+
schema_._def.effect.type === 'transform'
|
|
544
|
+
&& childOptions?.mode === 'output'
|
|
556
545
|
) {
|
|
557
546
|
return {}
|
|
558
547
|
}
|
|
@@ -602,7 +591,7 @@ export function extractJSONSchema(
|
|
|
602
591
|
schema: JSONSchema,
|
|
603
592
|
check: (schema: JSONSchema) => boolean,
|
|
604
593
|
matches: JSONSchema[] = [],
|
|
605
|
-
): { schema: JSONSchema | undefined
|
|
594
|
+
): { schema: JSONSchema | undefined, matches: JSONSchema[] } {
|
|
606
595
|
if (check(schema)) {
|
|
607
596
|
matches.push(schema)
|
|
608
597
|
return { schema: undefined, matches }
|
|
@@ -615,14 +604,14 @@ export function extractJSONSchema(
|
|
|
615
604
|
// TODO: $ref
|
|
616
605
|
|
|
617
606
|
if (
|
|
618
|
-
schema.anyOf
|
|
619
|
-
Object.keys(schema).every(
|
|
620
|
-
|
|
607
|
+
schema.anyOf
|
|
608
|
+
&& Object.keys(schema).every(
|
|
609
|
+
k => k === 'anyOf' || NON_LOGIC_KEYWORDS.includes(k as any),
|
|
621
610
|
)
|
|
622
611
|
) {
|
|
623
612
|
const anyOf = schema.anyOf
|
|
624
|
-
.map(
|
|
625
|
-
.filter(
|
|
613
|
+
.map(s => extractJSONSchema(s, check, matches).schema)
|
|
614
|
+
.filter(v => !!v)
|
|
626
615
|
|
|
627
616
|
if (anyOf.length === 1 && typeof anyOf[0] === 'object') {
|
|
628
617
|
return { schema: { ...schema, anyOf: undefined, ...anyOf[0] }, matches }
|
|
@@ -640,14 +629,14 @@ export function extractJSONSchema(
|
|
|
640
629
|
// TODO: $ref
|
|
641
630
|
|
|
642
631
|
if (
|
|
643
|
-
schema.oneOf
|
|
644
|
-
Object.keys(schema).every(
|
|
645
|
-
|
|
632
|
+
schema.oneOf
|
|
633
|
+
&& Object.keys(schema).every(
|
|
634
|
+
k => k === 'oneOf' || NON_LOGIC_KEYWORDS.includes(k as any),
|
|
646
635
|
)
|
|
647
636
|
) {
|
|
648
637
|
const oneOf = schema.oneOf
|
|
649
|
-
.map(
|
|
650
|
-
.filter(
|
|
638
|
+
.map(s => extractJSONSchema(s, check, matches).schema)
|
|
639
|
+
.filter(v => !!v)
|
|
651
640
|
|
|
652
641
|
if (oneOf.length === 1 && typeof oneOf[0] === 'object') {
|
|
653
642
|
return { schema: { ...schema, oneOf: undefined, ...oneOf[0] }, matches }
|