@furystack/rest 8.0.42 → 8.1.1
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/CHANGELOG.md +77 -0
- package/README.md +37 -1
- package/esm/api-endpoint-schema.d.ts +47 -2
- package/esm/api-endpoint-schema.d.ts.map +1 -1
- package/esm/index.d.ts +4 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +4 -1
- package/esm/index.js.map +1 -1
- package/esm/openapi-document.d.ts +303 -0
- package/esm/openapi-document.d.ts.map +1 -0
- package/esm/openapi-document.js +2 -0
- package/esm/openapi-document.js.map +1 -0
- package/esm/openapi-resolve-refs.d.ts +20 -0
- package/esm/openapi-resolve-refs.d.ts.map +1 -0
- package/esm/openapi-resolve-refs.js +68 -0
- package/esm/openapi-resolve-refs.js.map +1 -0
- package/esm/openapi-resolve-refs.spec.d.ts +2 -0
- package/esm/openapi-resolve-refs.spec.d.ts.map +1 -0
- package/esm/openapi-resolve-refs.spec.js +294 -0
- package/esm/openapi-resolve-refs.spec.js.map +1 -0
- package/esm/openapi-to-rest-api.d.ts +197 -0
- package/esm/openapi-to-rest-api.d.ts.map +1 -0
- package/esm/openapi-to-rest-api.js +2 -0
- package/esm/openapi-to-rest-api.js.map +1 -0
- package/esm/openapi-to-rest-api.spec.d.ts +2 -0
- package/esm/openapi-to-rest-api.spec.d.ts.map +1 -0
- package/esm/openapi-to-rest-api.spec.js +665 -0
- package/esm/openapi-to-rest-api.spec.js.map +1 -0
- package/esm/openapi-to-schema.d.ts +24 -0
- package/esm/openapi-to-schema.d.ts.map +1 -0
- package/esm/openapi-to-schema.js +145 -0
- package/esm/openapi-to-schema.js.map +1 -0
- package/esm/openapi-to-schema.spec.d.ts +2 -0
- package/esm/openapi-to-schema.spec.d.ts.map +1 -0
- package/esm/openapi-to-schema.spec.js +610 -0
- package/esm/openapi-to-schema.spec.js.map +1 -0
- package/esm/rest-api.d.ts +21 -4
- package/esm/rest-api.d.ts.map +1 -1
- package/esm/swagger-document.d.ts +2 -195
- package/esm/swagger-document.d.ts.map +1 -1
- package/esm/swagger-document.js +2 -1
- package/esm/swagger-document.js.map +1 -1
- package/package.json +3 -3
- package/src/api-endpoint-schema.ts +56 -3
- package/src/index.ts +4 -1
- package/src/openapi-document.ts +328 -0
- package/src/openapi-resolve-refs.spec.ts +324 -0
- package/src/openapi-resolve-refs.ts +71 -0
- package/src/openapi-to-rest-api.spec.ts +823 -0
- package/src/openapi-to-rest-api.ts +263 -0
- package/src/openapi-to-schema.spec.ts +707 -0
- package/src/openapi-to-schema.ts +163 -0
- package/src/rest-api.ts +26 -5
- package/src/swagger-document.ts +2 -220
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { OpenApiDocument } from './openapi-document.js'
|
|
2
|
+
import type { RestApi } from './rest-api.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts an OpenAPI `{param}` path to FuryStack `:param` format at the type level.
|
|
6
|
+
*/
|
|
7
|
+
export type ConvertOpenApiPath<P extends string> = P extends `${infer Before}{${infer Param}}${infer After}`
|
|
8
|
+
? `${Before}:${Param}${ConvertOpenApiPath<After>}`
|
|
9
|
+
: P
|
|
10
|
+
|
|
11
|
+
// ─── $ref resolution ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
type ResolveRef<Doc extends OpenApiDocument, Ref extends string> = Ref extends `#/components/schemas/${infer Name}`
|
|
14
|
+
? Doc['components'] extends { schemas: infer S }
|
|
15
|
+
? Name extends keyof S
|
|
16
|
+
? S[Name]
|
|
17
|
+
: unknown
|
|
18
|
+
: unknown
|
|
19
|
+
: Ref extends `#/components/parameters/${infer Name}`
|
|
20
|
+
? Doc['components'] extends { parameters: infer S }
|
|
21
|
+
? Name extends keyof S
|
|
22
|
+
? S[Name]
|
|
23
|
+
: unknown
|
|
24
|
+
: unknown
|
|
25
|
+
: Ref extends `#/components/responses/${infer Name}`
|
|
26
|
+
? Doc['components'] extends { responses: infer S }
|
|
27
|
+
? Name extends keyof S
|
|
28
|
+
? S[Name]
|
|
29
|
+
: unknown
|
|
30
|
+
: unknown
|
|
31
|
+
: Ref extends `#/components/requestBodies/${infer Name}`
|
|
32
|
+
? Doc['components'] extends { requestBodies: infer S }
|
|
33
|
+
? Name extends keyof S
|
|
34
|
+
? S[Name]
|
|
35
|
+
: unknown
|
|
36
|
+
: unknown
|
|
37
|
+
: unknown
|
|
38
|
+
|
|
39
|
+
type ResolveSchemaOrRef<Doc extends OpenApiDocument, S> = S extends { $ref: infer Ref extends string }
|
|
40
|
+
? ResolveRef<Doc, Ref>
|
|
41
|
+
: S
|
|
42
|
+
|
|
43
|
+
// ─── JSON Schema → TypeScript type mapping ─────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Maps a JSON Schema type keyword to its TypeScript equivalent.
|
|
49
|
+
* Supports primitives, arrays, objects, enum, const, oneOf/anyOf/allOf, nullable, and $ref.
|
|
50
|
+
*
|
|
51
|
+
* The `Doc` parameter is threaded through for `$ref` resolution within schemas.
|
|
52
|
+
*/
|
|
53
|
+
export type JsonSchemaToType<S, Doc extends OpenApiDocument = OpenApiDocument> =
|
|
54
|
+
// $ref
|
|
55
|
+
S extends { $ref: infer Ref extends string }
|
|
56
|
+
? JsonSchemaToType<ResolveRef<Doc, Ref>, Doc>
|
|
57
|
+
: // const literal
|
|
58
|
+
S extends { const: infer C }
|
|
59
|
+
? C
|
|
60
|
+
: // string enum
|
|
61
|
+
S extends { type: 'string'; enum: ReadonlyArray<infer E> }
|
|
62
|
+
? E
|
|
63
|
+
: // string
|
|
64
|
+
S extends { type: 'string' }
|
|
65
|
+
? string
|
|
66
|
+
: // number / integer
|
|
67
|
+
S extends { type: 'number' | 'integer' }
|
|
68
|
+
? number
|
|
69
|
+
: // boolean
|
|
70
|
+
S extends { type: 'boolean' }
|
|
71
|
+
? boolean
|
|
72
|
+
: // null
|
|
73
|
+
S extends { type: 'null' }
|
|
74
|
+
? null
|
|
75
|
+
: // nullable (3.1 style: type is a tuple with 'null')
|
|
76
|
+
S extends { type: readonly [infer T, 'null'] }
|
|
77
|
+
? JsonSchemaToType<{ type: T }, Doc> | null
|
|
78
|
+
: S extends { type: readonly ['null', infer T] }
|
|
79
|
+
? JsonSchemaToType<{ type: T }, Doc> | null
|
|
80
|
+
: // array
|
|
81
|
+
S extends { type: 'array'; items: infer Items }
|
|
82
|
+
? Array<JsonSchemaToType<Items, Doc>>
|
|
83
|
+
: // object with properties + required
|
|
84
|
+
S extends { type: 'object'; properties: infer Props extends Record<string, unknown> }
|
|
85
|
+
? S extends { required: ReadonlyArray<infer R extends string> }
|
|
86
|
+
? { [K in keyof Props & R]: JsonSchemaToType<Props[K], Doc> } & {
|
|
87
|
+
[K in Exclude<keyof Props, R>]?: JsonSchemaToType<Props[K], Doc>
|
|
88
|
+
}
|
|
89
|
+
: { [K in keyof Props]?: JsonSchemaToType<Props[K], Doc> }
|
|
90
|
+
: // object without properties
|
|
91
|
+
S extends { type: 'object' }
|
|
92
|
+
? Record<string, unknown>
|
|
93
|
+
: // allOf → intersection
|
|
94
|
+
S extends { allOf: ReadonlyArray<infer Items> }
|
|
95
|
+
? UnionToIntersection<JsonSchemaToType<Items, Doc>>
|
|
96
|
+
: // oneOf → union
|
|
97
|
+
S extends { oneOf: ReadonlyArray<infer Items> }
|
|
98
|
+
? JsonSchemaToType<Items, Doc>
|
|
99
|
+
: // anyOf → union
|
|
100
|
+
S extends { anyOf: ReadonlyArray<infer Items> }
|
|
101
|
+
? JsonSchemaToType<Items, Doc>
|
|
102
|
+
: unknown
|
|
103
|
+
|
|
104
|
+
// ─── HTTP method utilities ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
type LowercaseHttpMethod = 'get' | 'put' | 'post' | 'delete' | 'patch' | 'head' | 'options' | 'trace'
|
|
107
|
+
|
|
108
|
+
type MethodMap = {
|
|
109
|
+
get: 'GET'
|
|
110
|
+
put: 'PUT'
|
|
111
|
+
post: 'POST'
|
|
112
|
+
delete: 'DELETE'
|
|
113
|
+
patch: 'PATCH'
|
|
114
|
+
head: 'HEAD'
|
|
115
|
+
options: 'OPTIONS'
|
|
116
|
+
trace: 'TRACE'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type UppercaseMethod<M extends string> = M extends keyof MethodMap ? MethodMap[M] : never
|
|
120
|
+
|
|
121
|
+
// ─── Path / operation extraction ────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
type PathsWithMethod<T extends OpenApiDocument, M extends LowercaseHttpMethod> = {
|
|
124
|
+
[P in keyof NonNullable<T['paths']> & string]: NonNullable<T['paths']>[P] extends infer PathItem
|
|
125
|
+
? M extends keyof PathItem
|
|
126
|
+
? PathItem[M] extends object
|
|
127
|
+
? P
|
|
128
|
+
: never
|
|
129
|
+
: never
|
|
130
|
+
: never
|
|
131
|
+
}[keyof NonNullable<T['paths']> & string]
|
|
132
|
+
|
|
133
|
+
type GetOperation<T extends OpenApiDocument, P extends string, M extends LowercaseHttpMethod> = NonNullable<
|
|
134
|
+
T['paths']
|
|
135
|
+
>[P] extends infer PathItem
|
|
136
|
+
? M extends keyof PathItem
|
|
137
|
+
? PathItem[M]
|
|
138
|
+
: never
|
|
139
|
+
: never
|
|
140
|
+
|
|
141
|
+
// ─── Response / body / parameter extraction ─────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
type ExtractResponseSchema<Doc extends OpenApiDocument, Op> = Op extends { responses: infer R }
|
|
144
|
+
? R extends { '200': infer Resp200 }
|
|
145
|
+
? ResolveSchemaOrRef<Doc, Resp200> extends { content: { 'application/json': { schema: infer S } } }
|
|
146
|
+
? JsonSchemaToType<S, Doc>
|
|
147
|
+
: unknown
|
|
148
|
+
: R extends { '201': infer Resp201 }
|
|
149
|
+
? ResolveSchemaOrRef<Doc, Resp201> extends { content: { 'application/json': { schema: infer S } } }
|
|
150
|
+
? JsonSchemaToType<S, Doc>
|
|
151
|
+
: unknown
|
|
152
|
+
: unknown
|
|
153
|
+
: unknown
|
|
154
|
+
|
|
155
|
+
type ExtractRequestBodySchema<Doc extends OpenApiDocument, Op> = Op extends {
|
|
156
|
+
requestBody: infer RB
|
|
157
|
+
}
|
|
158
|
+
? ResolveSchemaOrRef<Doc, RB> extends { content: { 'application/json': { schema: infer S } } }
|
|
159
|
+
? JsonSchemaToType<S, Doc>
|
|
160
|
+
: never
|
|
161
|
+
: never
|
|
162
|
+
|
|
163
|
+
type ExtractPathParamsFromPath<P extends string> = P extends `${string}{${infer Param}}${infer Rest}`
|
|
164
|
+
? { [K in Param | keyof ExtractPathParamsFromPath<Rest>]: string }
|
|
165
|
+
: never
|
|
166
|
+
|
|
167
|
+
type HasPathParams<P extends string> = P extends `${string}{${string}}${string}` ? true : false
|
|
168
|
+
|
|
169
|
+
type ResolvedParam<Doc extends OpenApiDocument, P> = P extends { $ref: infer Ref extends string }
|
|
170
|
+
? ResolveRef<Doc, Ref>
|
|
171
|
+
: P
|
|
172
|
+
|
|
173
|
+
type ExtractQueryParamEntry<Doc extends OpenApiDocument, P> =
|
|
174
|
+
ResolvedParam<Doc, P> extends { in: 'query'; name: infer N extends string; schema: infer S }
|
|
175
|
+
? { [K in N]: JsonSchemaToType<S, Doc> }
|
|
176
|
+
: ResolvedParam<Doc, P> extends { in: 'query'; name: infer N extends string }
|
|
177
|
+
? { [K in N]: string }
|
|
178
|
+
: never
|
|
179
|
+
|
|
180
|
+
type BuildQueryParamsFromTuple<Doc extends OpenApiDocument, T> = T extends readonly [infer Head, ...infer Tail]
|
|
181
|
+
? [ExtractQueryParamEntry<Doc, Head>] extends [never]
|
|
182
|
+
? BuildQueryParamsFromTuple<Doc, Tail>
|
|
183
|
+
: ExtractQueryParamEntry<Doc, Head> & BuildQueryParamsFromTuple<Doc, Tail>
|
|
184
|
+
: unknown
|
|
185
|
+
|
|
186
|
+
type HasQueryParams<Doc extends OpenApiDocument, T> = T extends readonly [infer Head, ...infer Tail]
|
|
187
|
+
? [ExtractQueryParamEntry<Doc, Head>] extends [never]
|
|
188
|
+
? HasQueryParams<Doc, Tail>
|
|
189
|
+
: true
|
|
190
|
+
: false
|
|
191
|
+
|
|
192
|
+
type BuildQueryParams<Doc extends OpenApiDocument, Op> = Op extends {
|
|
193
|
+
parameters: infer Params extends readonly unknown[]
|
|
194
|
+
}
|
|
195
|
+
? HasQueryParams<Doc, Params> extends true
|
|
196
|
+
? BuildQueryParamsFromTuple<Doc, Params>
|
|
197
|
+
: never
|
|
198
|
+
: never
|
|
199
|
+
|
|
200
|
+
// ─── Metadata extraction ────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
type ExtractTags<Op> = Op extends { tags: infer T } ? T : never
|
|
203
|
+
type ExtractDeprecated<Op> = Op extends { deprecated: true } ? true : never
|
|
204
|
+
type ExtractSummary<Op> = Op extends { summary: infer S extends string } ? S : never
|
|
205
|
+
type ExtractDescription<Op> = Op extends { description: infer S extends string } ? S : never
|
|
206
|
+
|
|
207
|
+
// ─── Endpoint builder ───────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
type BuildEndpoint<T extends OpenApiDocument, P extends string, M extends LowercaseHttpMethod> = {
|
|
210
|
+
result: ExtractResponseSchema<T, GetOperation<T, P, M>>
|
|
211
|
+
} & ([ExtractRequestBodySchema<T, GetOperation<T, P, M>>] extends [never]
|
|
212
|
+
? unknown
|
|
213
|
+
: { body: ExtractRequestBodySchema<T, GetOperation<T, P, M>> }) &
|
|
214
|
+
(HasPathParams<P> extends true ? { url: ExtractPathParamsFromPath<P> } : unknown) &
|
|
215
|
+
([BuildQueryParams<T, GetOperation<T, P, M>>] extends [never]
|
|
216
|
+
? unknown
|
|
217
|
+
: { query: BuildQueryParams<T, GetOperation<T, P, M>> }) &
|
|
218
|
+
([ExtractTags<GetOperation<T, P, M>>] extends [never] ? unknown : { tags: ExtractTags<GetOperation<T, P, M>> }) &
|
|
219
|
+
([ExtractDeprecated<GetOperation<T, P, M>>] extends [never] ? unknown : { deprecated: true }) &
|
|
220
|
+
([ExtractSummary<GetOperation<T, P, M>>] extends [never]
|
|
221
|
+
? unknown
|
|
222
|
+
: { summary: ExtractSummary<GetOperation<T, P, M>> }) &
|
|
223
|
+
([ExtractDescription<GetOperation<T, P, M>>] extends [never]
|
|
224
|
+
? unknown
|
|
225
|
+
: { description: ExtractDescription<GetOperation<T, P, M>> })
|
|
226
|
+
|
|
227
|
+
type EndpointsForMethod<T extends OpenApiDocument, M extends LowercaseHttpMethod> =
|
|
228
|
+
string extends PathsWithMethod<T, M>
|
|
229
|
+
? never
|
|
230
|
+
: [PathsWithMethod<T, M>] extends [never]
|
|
231
|
+
? never
|
|
232
|
+
: {
|
|
233
|
+
[P in PathsWithMethod<T, M> as ConvertOpenApiPath<P>]: BuildEndpoint<T, P, M>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
type CleanObject<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] }
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extracts a strongly-typed `RestApi` from an OpenAPI document type.
|
|
240
|
+
*
|
|
241
|
+
* Supports `$ref` resolution, `allOf`/`oneOf`/`anyOf` composition, `nullable`, `const`,
|
|
242
|
+
* and metadata extraction (`tags`, `deprecated`, `summary`, `description`).
|
|
243
|
+
*
|
|
244
|
+
* Use with `as const satisfies OpenApiDocument` to get full type inference:
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* import type { OpenApiDocument, OpenApiToRestApi } from '@furystack/rest'
|
|
249
|
+
* import { createClient } from '@furystack/rest-client-fetch'
|
|
250
|
+
*
|
|
251
|
+
* const apiDoc = { ... } as const satisfies OpenApiDocument
|
|
252
|
+
* type MyApi = OpenApiToRestApi<typeof apiDoc>
|
|
253
|
+
* const client = createClient<MyApi>({ endpointUrl: 'https://api.example.com' })
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export type OpenApiToRestApi<T extends OpenApiDocument> =
|
|
257
|
+
CleanObject<{
|
|
258
|
+
[M in LowercaseHttpMethod as [EndpointsForMethod<T, M>] extends [never]
|
|
259
|
+
? never
|
|
260
|
+
: UppercaseMethod<M>]: EndpointsForMethod<T, M>
|
|
261
|
+
}> extends infer R extends RestApi
|
|
262
|
+
? R
|
|
263
|
+
: never
|