typed-openapi 1.4.5 → 1.5.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/dist/chunk-KAEXXJ7X.js +21 -0
- package/dist/{chunk-F4UA5MLD.js → chunk-O7DZWQK4.js} +17 -9
- package/dist/{chunk-52X6V4N5.js → chunk-OVT6OLBK.js} +141 -35
- package/dist/cli.js +4 -7
- package/dist/index.d.ts +6 -227
- package/dist/index.js +2 -1
- package/dist/node.export.d.ts +8 -5
- package/dist/node.export.js +4 -6
- package/dist/pretty.export.d.ts +5 -0
- package/dist/pretty.export.js +6 -0
- package/dist/types-DLE5RaXi.d.ts +242 -0
- package/package.json +3 -2
- package/src/cli.ts +1 -5
- package/src/generate-client-files.ts +20 -9
- package/src/generator.ts +28 -0
- package/src/map-openapi-endpoints.ts +30 -6
- package/src/node.export.ts +0 -1
- package/src/openapi-schema-to-ts.ts +15 -13
- package/src/pretty.export.ts +1 -0
- package/src/ref-resolver.ts +12 -2
- package/src/sanitize-name.ts +79 -0
- package/src/types.ts +14 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as openapi3_ts_oas31 from 'openapi3-ts/oas31';
|
|
2
|
+
import { OpenAPIObject, ReferenceObject, OperationObject, SchemaObject } from 'openapi3-ts/oas31';
|
|
3
|
+
import { SchemaObject as SchemaObject$1 } from 'openapi3-ts/oas30';
|
|
4
|
+
|
|
5
|
+
declare class Box<T extends AnyBoxDef = AnyBoxDef> {
|
|
6
|
+
definition: T;
|
|
7
|
+
type: T["type"];
|
|
8
|
+
value: T["value"];
|
|
9
|
+
params: T["params"];
|
|
10
|
+
schema: T["schema"];
|
|
11
|
+
ctx: T["ctx"];
|
|
12
|
+
constructor(definition: T);
|
|
13
|
+
toJSON(): {
|
|
14
|
+
type: T["type"];
|
|
15
|
+
value: T["value"];
|
|
16
|
+
};
|
|
17
|
+
toString(): string;
|
|
18
|
+
recompute(callback: OpenapiSchemaConvertContext["onBox"]): Box<AnyBoxDef>;
|
|
19
|
+
static fromJSON(json: string): Box<any>;
|
|
20
|
+
static isBox(box: unknown): box is Box<AnyBoxDef>;
|
|
21
|
+
static isUnion(box: Box<AnyBoxDef>): box is Box<BoxUnion>;
|
|
22
|
+
static isIntersection(box: Box<AnyBoxDef>): box is Box<BoxIntersection>;
|
|
23
|
+
static isArray(box: Box<AnyBoxDef>): box is Box<BoxArray>;
|
|
24
|
+
static isOptional(box: Box<AnyBoxDef>): box is Box<BoxOptional>;
|
|
25
|
+
static isReference(box: Box<AnyBoxDef>): box is Box<BoxRef>;
|
|
26
|
+
static isKeyword(box: Box<AnyBoxDef>): box is Box<BoxKeyword>;
|
|
27
|
+
static isObject(box: Box<AnyBoxDef>): box is Box<BoxObject>;
|
|
28
|
+
static isLiteral(box: Box<AnyBoxDef>): box is Box<BoxLiteral>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type RefInfo = {
|
|
32
|
+
/**
|
|
33
|
+
* The (potentially autocorrected) ref
|
|
34
|
+
* @example "#/components/schemas/MySchema"
|
|
35
|
+
*/
|
|
36
|
+
ref: string;
|
|
37
|
+
/**
|
|
38
|
+
* The name of the ref
|
|
39
|
+
* @example "MySchema"
|
|
40
|
+
* */
|
|
41
|
+
name: string;
|
|
42
|
+
normalized: string;
|
|
43
|
+
kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
|
|
44
|
+
};
|
|
45
|
+
declare const createRefResolver: (doc: OpenAPIObject, factory: GenericFactory, nameTransform?: NameTransformOptions) => {
|
|
46
|
+
get: <T = LibSchemaObject>(ref: string) => NonNullable<T>;
|
|
47
|
+
unwrap: <T extends ReferenceObject | {}>(component: T) => Exclude<T, ReferenceObject>;
|
|
48
|
+
getInfosByRef: (ref: string) => RefInfo;
|
|
49
|
+
infos: Map<string, RefInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* Get the schemas in the order they should be generated, depending on their dependencies
|
|
52
|
+
* so that a schema is generated before the ones that depend on it
|
|
53
|
+
*/
|
|
54
|
+
getOrderedSchemas: () => [schema: Box<AnyBoxDef>, infos: RefInfo][];
|
|
55
|
+
directDependencies: Map<string, Set<string>>;
|
|
56
|
+
transitiveDependencies: Map<string, Set<string>>;
|
|
57
|
+
};
|
|
58
|
+
interface RefResolver extends ReturnType<typeof createRefResolver> {
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare const mapOpenApiEndpoints: (doc: OpenAPIObject, options?: {
|
|
62
|
+
nameTransform?: NameTransformOptions;
|
|
63
|
+
}) => {
|
|
64
|
+
doc: OpenAPIObject;
|
|
65
|
+
refs: {
|
|
66
|
+
get: <T = LibSchemaObject>(ref: string) => NonNullable<T>;
|
|
67
|
+
unwrap: <T extends openapi3_ts_oas31.ReferenceObject | {}>(component: T) => Exclude<T, openapi3_ts_oas31.ReferenceObject>;
|
|
68
|
+
getInfosByRef: (ref: string) => RefInfo;
|
|
69
|
+
infos: Map<string, RefInfo>;
|
|
70
|
+
getOrderedSchemas: () => [schema: Box<AnyBoxDef>, infos: RefInfo][];
|
|
71
|
+
directDependencies: Map<string, Set<string>>;
|
|
72
|
+
transitiveDependencies: Map<string, Set<string>>;
|
|
73
|
+
};
|
|
74
|
+
endpointList: Endpoint<DefaultEndpoint>[];
|
|
75
|
+
factory: {
|
|
76
|
+
union: (types: StringOrBox[]) => string;
|
|
77
|
+
intersection: (types: StringOrBox[]) => string;
|
|
78
|
+
array: (type: StringOrBox) => string;
|
|
79
|
+
optional: (type: StringOrBox) => string;
|
|
80
|
+
reference: (name: string, typeArgs: StringOrBox[] | undefined) => string;
|
|
81
|
+
literal: (value: StringOrBox) => string;
|
|
82
|
+
string: () => "string";
|
|
83
|
+
number: () => "number";
|
|
84
|
+
boolean: () => "boolean";
|
|
85
|
+
unknown: () => "unknown";
|
|
86
|
+
any: () => "any";
|
|
87
|
+
never: () => "never";
|
|
88
|
+
object: (props: Record<string, StringOrBox>) => string;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type MutationMethod = "post" | "put" | "patch" | "delete";
|
|
92
|
+
type Method = "get" | "head" | "options" | MutationMethod;
|
|
93
|
+
type EndpointParameters = {
|
|
94
|
+
body?: Box<BoxRef>;
|
|
95
|
+
query?: Box<BoxRef> | Record<string, AnyBox>;
|
|
96
|
+
header?: Box<BoxRef> | Record<string, AnyBox>;
|
|
97
|
+
path?: Box<BoxRef> | Record<string, AnyBox>;
|
|
98
|
+
};
|
|
99
|
+
type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
100
|
+
type DefaultEndpoint = {
|
|
101
|
+
parameters?: EndpointParameters | undefined;
|
|
102
|
+
response: AnyBox;
|
|
103
|
+
responseHeaders?: Record<string, AnyBox>;
|
|
104
|
+
};
|
|
105
|
+
type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
106
|
+
operation: OperationObject;
|
|
107
|
+
method: Method;
|
|
108
|
+
path: string;
|
|
109
|
+
parameters?: TConfig["parameters"];
|
|
110
|
+
requestFormat: RequestFormat;
|
|
111
|
+
meta: {
|
|
112
|
+
alias: string;
|
|
113
|
+
hasParameters: boolean;
|
|
114
|
+
areParametersRequired: boolean;
|
|
115
|
+
};
|
|
116
|
+
response: TConfig["response"];
|
|
117
|
+
responseHeaders?: TConfig["responseHeaders"];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type LibSchemaObject = SchemaObject & SchemaObject$1;
|
|
121
|
+
type BoxDefinition = {
|
|
122
|
+
type: string;
|
|
123
|
+
params: unknown;
|
|
124
|
+
value: string;
|
|
125
|
+
};
|
|
126
|
+
type BoxParams = string | BoxDefinition;
|
|
127
|
+
type WithSchema = {
|
|
128
|
+
schema: LibSchemaObject | ReferenceObject | undefined;
|
|
129
|
+
ctx: OpenapiSchemaConvertContext;
|
|
130
|
+
};
|
|
131
|
+
type BoxUnion = WithSchema & {
|
|
132
|
+
type: "union";
|
|
133
|
+
params: {
|
|
134
|
+
types: Array<BoxParams>;
|
|
135
|
+
};
|
|
136
|
+
value: string;
|
|
137
|
+
};
|
|
138
|
+
type BoxIntersection = WithSchema & {
|
|
139
|
+
type: "intersection";
|
|
140
|
+
params: {
|
|
141
|
+
types: Array<BoxParams>;
|
|
142
|
+
};
|
|
143
|
+
value: string;
|
|
144
|
+
};
|
|
145
|
+
type BoxArray = WithSchema & {
|
|
146
|
+
type: "array";
|
|
147
|
+
params: {
|
|
148
|
+
type: BoxParams;
|
|
149
|
+
};
|
|
150
|
+
value: string;
|
|
151
|
+
};
|
|
152
|
+
type BoxOptional = WithSchema & {
|
|
153
|
+
type: "optional";
|
|
154
|
+
params: {
|
|
155
|
+
type: BoxParams;
|
|
156
|
+
};
|
|
157
|
+
value: string;
|
|
158
|
+
};
|
|
159
|
+
type BoxRef = WithSchema & {
|
|
160
|
+
type: "ref";
|
|
161
|
+
params: {
|
|
162
|
+
name: string;
|
|
163
|
+
generics?: BoxParams[] | undefined;
|
|
164
|
+
};
|
|
165
|
+
value: string;
|
|
166
|
+
};
|
|
167
|
+
type BoxLiteral = WithSchema & {
|
|
168
|
+
type: "literal";
|
|
169
|
+
params: {};
|
|
170
|
+
value: string;
|
|
171
|
+
};
|
|
172
|
+
type BoxKeyword = WithSchema & {
|
|
173
|
+
type: "keyword";
|
|
174
|
+
params: {
|
|
175
|
+
name: string;
|
|
176
|
+
};
|
|
177
|
+
value: string;
|
|
178
|
+
};
|
|
179
|
+
type BoxObject = WithSchema & {
|
|
180
|
+
type: "object";
|
|
181
|
+
params: {
|
|
182
|
+
props: Record<string, BoxParams>;
|
|
183
|
+
};
|
|
184
|
+
value: string;
|
|
185
|
+
};
|
|
186
|
+
type AnyBoxDef = BoxUnion | BoxIntersection | BoxArray | BoxOptional | BoxRef | BoxLiteral | BoxKeyword | BoxObject;
|
|
187
|
+
type AnyBox = Box<AnyBoxDef>;
|
|
188
|
+
type OpenapiSchemaConvertArgs = {
|
|
189
|
+
schema: SchemaObject | ReferenceObject;
|
|
190
|
+
ctx: OpenapiSchemaConvertContext;
|
|
191
|
+
meta?: {} | undefined;
|
|
192
|
+
};
|
|
193
|
+
type FactoryCreator = (schema: SchemaObject | ReferenceObject, ctx: OpenapiSchemaConvertContext) => GenericFactory;
|
|
194
|
+
type NameTransformOptions = {
|
|
195
|
+
transformSchemaName?: (name: string) => string;
|
|
196
|
+
transformEndpointName?: (endpoint: {
|
|
197
|
+
alias: string;
|
|
198
|
+
operation: OperationObject;
|
|
199
|
+
method: Method;
|
|
200
|
+
path: string;
|
|
201
|
+
}) => string;
|
|
202
|
+
};
|
|
203
|
+
type OpenapiSchemaConvertContext = {
|
|
204
|
+
factory: FactoryCreator | GenericFactory;
|
|
205
|
+
refs: RefResolver;
|
|
206
|
+
onBox?: (box: Box<AnyBoxDef>) => Box<AnyBoxDef>;
|
|
207
|
+
nameTransform?: NameTransformOptions;
|
|
208
|
+
};
|
|
209
|
+
type StringOrBox = string | Box<AnyBoxDef>;
|
|
210
|
+
type BoxFactory = {
|
|
211
|
+
union: (types: Array<StringOrBox>) => Box<BoxUnion>;
|
|
212
|
+
intersection: (types: Array<StringOrBox>) => Box<BoxIntersection>;
|
|
213
|
+
array: (type: StringOrBox) => Box<BoxArray>;
|
|
214
|
+
object: (props: Record<string, StringOrBox>) => Box<BoxObject>;
|
|
215
|
+
optional: (type: StringOrBox) => Box<BoxOptional>;
|
|
216
|
+
reference: (name: string, generics?: Array<StringOrBox> | undefined) => Box<BoxRef>;
|
|
217
|
+
literal: (value: StringOrBox) => Box<BoxLiteral>;
|
|
218
|
+
string: () => Box<BoxKeyword>;
|
|
219
|
+
number: () => Box<BoxKeyword>;
|
|
220
|
+
boolean: () => Box<BoxKeyword>;
|
|
221
|
+
unknown: () => Box<BoxKeyword>;
|
|
222
|
+
any: () => Box<BoxKeyword>;
|
|
223
|
+
never: () => Box<BoxKeyword>;
|
|
224
|
+
};
|
|
225
|
+
type GenericFactory = {
|
|
226
|
+
callback?: OpenapiSchemaConvertContext["onBox"];
|
|
227
|
+
union: (types: Array<StringOrBox>) => string;
|
|
228
|
+
intersection: (types: Array<StringOrBox>) => string;
|
|
229
|
+
array: (type: StringOrBox) => string;
|
|
230
|
+
object: (props: Record<string, StringOrBox>) => string;
|
|
231
|
+
optional: (type: StringOrBox) => string;
|
|
232
|
+
reference: (name: string, generics?: Array<StringOrBox> | undefined) => string;
|
|
233
|
+
literal: (value: StringOrBox) => string;
|
|
234
|
+
string: () => string;
|
|
235
|
+
number: () => string;
|
|
236
|
+
boolean: () => string;
|
|
237
|
+
unknown: () => string;
|
|
238
|
+
any: () => string;
|
|
239
|
+
never: () => string;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export { type AnyBoxDef as A, type BoxFactory as B, type EndpointParameters as E, type FactoryCreator as F, type GenericFactory as G, type LibSchemaObject as L, type Method as M, type NameTransformOptions as N, type OpenapiSchemaConvertContext as O, type RefInfo as R, type StringOrBox as S, type WithSchema as W, type OpenapiSchemaConvertArgs as a, Box as b, type Endpoint as c, createRefResolver as d, type RefResolver as e, type BoxDefinition as f, type BoxParams as g, type BoxUnion as h, type BoxIntersection as i, type BoxArray as j, type BoxOptional as k, type BoxRef as l, mapOpenApiEndpoints as m, type BoxLiteral as n, type BoxKeyword as o, type BoxObject as p, type AnyBox as q };
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/index.js",
|
|
9
|
-
"./node": "./dist/node.export.js"
|
|
9
|
+
"./node": "./dist/node.export.js",
|
|
10
|
+
"./pretty": "./dist/pretty.export.js"
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|
|
12
13
|
"typed-openapi": "bin.js"
|
package/src/cli.ts
CHANGED
|
@@ -15,11 +15,7 @@ cli
|
|
|
15
15
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
16
16
|
{ default: "none" },
|
|
17
17
|
)
|
|
18
|
-
.option(
|
|
19
|
-
"--schemas-only",
|
|
20
|
-
"Only generate schemas, skipping client generation (defaults to false)",
|
|
21
|
-
{ default: false },
|
|
22
|
-
)
|
|
18
|
+
.option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false })
|
|
23
19
|
.option(
|
|
24
20
|
"--tanstack [name]",
|
|
25
21
|
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file",
|
|
@@ -7,6 +7,7 @@ import { allowedRuntimes, generateFile } from "./generator.ts";
|
|
|
7
7
|
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
8
8
|
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
9
9
|
import { prettify } from "./format.ts";
|
|
10
|
+
import type { NameTransformOptions } from "./types.ts";
|
|
10
11
|
|
|
11
12
|
const cwd = process.cwd();
|
|
12
13
|
const now = new Date();
|
|
@@ -26,17 +27,24 @@ export const optionsSchema = type({
|
|
|
26
27
|
schemasOnly: "boolean",
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
31
|
+
nameTransform?: NameTransformOptions;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function generateClientFiles(input: string, options: GenerateClientFilesOptions) {
|
|
30
35
|
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
|
|
31
36
|
|
|
32
|
-
const ctx = mapOpenApiEndpoints(openApiDoc);
|
|
37
|
+
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
33
38
|
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
34
39
|
|
|
35
|
-
const content = await prettify(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const content = await prettify(
|
|
41
|
+
generateFile({
|
|
42
|
+
...ctx,
|
|
43
|
+
runtime: options.runtime,
|
|
44
|
+
schemasOnly: options.schemasOnly,
|
|
45
|
+
nameTransform: options.nameTransform,
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
40
48
|
const outputPath = join(
|
|
41
49
|
cwd,
|
|
42
50
|
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
|
|
@@ -49,9 +57,12 @@ export async function generateClientFiles(input: string, options: typeof options
|
|
|
49
57
|
if (options.tanstack) {
|
|
50
58
|
const tanstackContent = await generateTanstackQueryFile({
|
|
51
59
|
...ctx,
|
|
52
|
-
relativeApiClientPath:
|
|
60
|
+
relativeApiClientPath: "./" + basename(outputPath),
|
|
53
61
|
});
|
|
54
|
-
const tanstackOutputPath = join(
|
|
62
|
+
const tanstackOutputPath = join(
|
|
63
|
+
dirname(outputPath),
|
|
64
|
+
typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`,
|
|
65
|
+
);
|
|
55
66
|
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
56
67
|
await ensureDir(dirname(tanstackOutputPath));
|
|
57
68
|
await writeFile(tanstackOutputPath, tanstackContent);
|
package/src/generator.ts
CHANGED
|
@@ -6,10 +6,12 @@ import * as Codegen from "@sinclair/typebox-codegen";
|
|
|
6
6
|
import { match } from "ts-pattern";
|
|
7
7
|
import { type } from "arktype";
|
|
8
8
|
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
9
|
+
import type { NameTransformOptions } from "./types.ts";
|
|
9
10
|
|
|
10
11
|
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
11
12
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
12
13
|
schemasOnly?: boolean;
|
|
14
|
+
nameTransform?: NameTransformOptions | undefined;
|
|
13
15
|
};
|
|
14
16
|
type GeneratorContext = Required<GeneratorOptions>;
|
|
15
17
|
|
|
@@ -132,6 +134,25 @@ const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, Any
|
|
|
132
134
|
}
|
|
133
135
|
return str + "}";
|
|
134
136
|
};
|
|
137
|
+
|
|
138
|
+
const responseHeadersObjectToString = (responseHeaders: Record<string, AnyBox>, ctx: GeneratorContext) => {
|
|
139
|
+
let str = "{";
|
|
140
|
+
for (const [key, responseHeader] of Object.entries(responseHeaders)) {
|
|
141
|
+
const value =
|
|
142
|
+
ctx.runtime === "none"
|
|
143
|
+
? responseHeader.recompute((box) => {
|
|
144
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
145
|
+
box.value = `Schemas.${box.value}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return box;
|
|
149
|
+
}).value
|
|
150
|
+
: responseHeader.value;
|
|
151
|
+
str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${value},\n`;
|
|
152
|
+
}
|
|
153
|
+
return str + "}";
|
|
154
|
+
};
|
|
155
|
+
|
|
135
156
|
const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
136
157
|
let file = `
|
|
137
158
|
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
|
|
@@ -178,6 +199,11 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
178
199
|
}).value
|
|
179
200
|
: endpoint.response.value
|
|
180
201
|
},
|
|
202
|
+
${
|
|
203
|
+
endpoint.responseHeaders
|
|
204
|
+
? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},`
|
|
205
|
+
: ""
|
|
206
|
+
}
|
|
181
207
|
}\n`;
|
|
182
208
|
});
|
|
183
209
|
|
|
@@ -246,6 +272,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
246
272
|
export type DefaultEndpoint = {
|
|
247
273
|
parameters?: EndpointParameters | undefined;
|
|
248
274
|
response: unknown;
|
|
275
|
+
responseHeaders?: Record<string, unknown>;
|
|
249
276
|
};
|
|
250
277
|
|
|
251
278
|
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
@@ -260,6 +287,7 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
260
287
|
areParametersRequired: boolean;
|
|
261
288
|
};
|
|
262
289
|
response: TConfig["response"];
|
|
290
|
+
responseHeaders?: TConfig["responseHeaders"]
|
|
263
291
|
};
|
|
264
292
|
|
|
265
293
|
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
|
|
@@ -8,11 +8,13 @@ import { createRefResolver } from "./ref-resolver.ts";
|
|
|
8
8
|
import { tsFactory } from "./ts-factory.ts";
|
|
9
9
|
import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types.ts";
|
|
10
10
|
import { pathToVariableName } from "./string-utils.ts";
|
|
11
|
+
import { NameTransformOptions } from "./types.ts";
|
|
11
12
|
import { match, P } from "ts-pattern";
|
|
13
|
+
import { sanitizeName } from "./sanitize-name.ts";
|
|
12
14
|
|
|
13
15
|
const factory = tsFactory;
|
|
14
16
|
|
|
15
|
-
export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
17
|
+
export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransform?: NameTransformOptions }) => {
|
|
16
18
|
const refs = createRefResolver(doc, factory);
|
|
17
19
|
const ctx: OpenapiSchemaConvertContext = { refs, factory };
|
|
18
20
|
const endpointList = [] as Array<Endpoint>;
|
|
@@ -22,6 +24,10 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
22
24
|
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
23
25
|
if (operation.deprecated) return;
|
|
24
26
|
|
|
27
|
+
let alias = getAlias({ path, method, operation } as Endpoint);
|
|
28
|
+
if (options?.nameTransform?.transformEndpointName) {
|
|
29
|
+
alias = options.nameTransform.transformEndpointName({ alias, path, method: method as Method, operation });
|
|
30
|
+
}
|
|
25
31
|
const endpoint = {
|
|
26
32
|
operation,
|
|
27
33
|
method: method as Method,
|
|
@@ -29,7 +35,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
29
35
|
requestFormat: "json",
|
|
30
36
|
response: openApiSchemaToTs({ schema: {}, ctx }),
|
|
31
37
|
meta: {
|
|
32
|
-
alias
|
|
38
|
+
alias,
|
|
33
39
|
areParametersRequired: false,
|
|
34
40
|
hasParameters: false,
|
|
35
41
|
},
|
|
@@ -84,7 +90,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
84
90
|
|
|
85
91
|
if (matchingMediaType && content[matchingMediaType]) {
|
|
86
92
|
params.body = openApiSchemaToTs({
|
|
87
|
-
schema: content[matchingMediaType]?.schema ?? {}
|
|
93
|
+
schema: content[matchingMediaType]?.schema ?? {},
|
|
88
94
|
ctx,
|
|
89
95
|
});
|
|
90
96
|
}
|
|
@@ -139,12 +145,25 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
139
145
|
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
|
|
140
146
|
if (matchingMediaType && content[matchingMediaType]) {
|
|
141
147
|
endpoint.response = openApiSchemaToTs({
|
|
142
|
-
schema: content[matchingMediaType]?.schema ?? {}
|
|
148
|
+
schema: content[matchingMediaType]?.schema ?? {},
|
|
143
149
|
ctx,
|
|
144
150
|
});
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
// Map response headers
|
|
155
|
+
const headers = responseObject?.headers;
|
|
156
|
+
if (headers) {
|
|
157
|
+
endpoint.responseHeaders = Object.entries(headers).reduce(
|
|
158
|
+
(acc, [name, headerOrRef]) => {
|
|
159
|
+
const header = refs.unwrap(headerOrRef);
|
|
160
|
+
acc[name] = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
|
|
161
|
+
return acc;
|
|
162
|
+
},
|
|
163
|
+
{} as Record<string, Box>,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
endpointList.push(endpoint);
|
|
149
168
|
});
|
|
150
169
|
});
|
|
@@ -167,10 +186,13 @@ const isAllowedParamMediaTypes = (
|
|
|
167
186
|
|
|
168
187
|
const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
|
|
169
188
|
const getAlias = ({ path, method, operation }: Endpoint) =>
|
|
170
|
-
(
|
|
189
|
+
sanitizeName(
|
|
190
|
+
(method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
|
|
191
|
+
"endpoint",
|
|
192
|
+
);
|
|
171
193
|
|
|
172
194
|
type MutationMethod = "post" | "put" | "patch" | "delete";
|
|
173
|
-
type Method = "get" | "head" | "options" | MutationMethod;
|
|
195
|
+
export type Method = "get" | "head" | "options" | MutationMethod;
|
|
174
196
|
|
|
175
197
|
export type EndpointParameters = {
|
|
176
198
|
body?: Box<BoxRef>;
|
|
@@ -184,6 +206,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
184
206
|
type DefaultEndpoint = {
|
|
185
207
|
parameters?: EndpointParameters | undefined;
|
|
186
208
|
response: AnyBox;
|
|
209
|
+
responseHeaders?: Record<string, AnyBox>;
|
|
187
210
|
};
|
|
188
211
|
|
|
189
212
|
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
@@ -198,4 +221,5 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
198
221
|
areParametersRequired: boolean;
|
|
199
222
|
};
|
|
200
223
|
response: TConfig["response"];
|
|
224
|
+
responseHeaders?: TConfig["responseHeaders"];
|
|
201
225
|
};
|
package/src/node.export.ts
CHANGED
|
@@ -53,9 +53,9 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
53
53
|
|
|
54
54
|
if (schema.allOf) {
|
|
55
55
|
const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }));
|
|
56
|
-
const {allOf, externalDocs,
|
|
56
|
+
const { allOf, externalDocs, example, examples, description, title, ...rest } = schema;
|
|
57
57
|
if (Object.keys(rest).length > 0) {
|
|
58
|
-
types.push(openApiSchemaToTs({schema: rest, ctx, meta}))
|
|
58
|
+
types.push(openApiSchemaToTs({ schema: rest, ctx, meta }));
|
|
59
59
|
}
|
|
60
60
|
return t.intersection(types);
|
|
61
61
|
}
|
|
@@ -72,7 +72,7 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
72
72
|
} else if (value === false) {
|
|
73
73
|
return t.literal("false");
|
|
74
74
|
} else if (typeof value === "number") {
|
|
75
|
-
return t.literal(`${value}`)
|
|
75
|
+
return t.literal(`${value}`);
|
|
76
76
|
} else {
|
|
77
77
|
return t.literal(`"${value}"`);
|
|
78
78
|
}
|
|
@@ -95,16 +95,18 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
95
95
|
if (schemaType === "null") return t.literal("null");
|
|
96
96
|
}
|
|
97
97
|
if (!schemaType && schema.enum) {
|
|
98
|
-
return t.union(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
return t.union(
|
|
99
|
+
schema.enum.map((value) => {
|
|
100
|
+
if (typeof value === "string") {
|
|
101
|
+
return t.literal(`"${value}"`);
|
|
102
|
+
}
|
|
103
|
+
if (value === null) {
|
|
104
|
+
return t.literal("null");
|
|
105
|
+
}
|
|
106
|
+
// handle boolean and number literals
|
|
107
|
+
return t.literal(value);
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
if (schemaType === "array") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { prettify } from "./format.ts";
|
package/src/ref-resolver.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { Box } from "./box.ts";
|
|
|
5
5
|
import { isReferenceObject } from "./is-reference-object.ts";
|
|
6
6
|
import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
|
|
7
7
|
import { normalizeString } from "./string-utils.ts";
|
|
8
|
+
import { NameTransformOptions } from "./types.ts";
|
|
8
9
|
import { AnyBoxDef, GenericFactory, type LibSchemaObject } from "./types.ts";
|
|
9
10
|
import { topologicalSort } from "./topological-sort.ts";
|
|
11
|
+
import { sanitizeName } from "./sanitize-name.ts";
|
|
10
12
|
|
|
11
13
|
const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
|
|
12
14
|
const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
|
|
@@ -26,7 +28,11 @@ export type RefInfo = {
|
|
|
26
28
|
kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
export const createRefResolver = (
|
|
31
|
+
export const createRefResolver = (
|
|
32
|
+
doc: OpenAPIObject,
|
|
33
|
+
factory: GenericFactory,
|
|
34
|
+
nameTransform?: NameTransformOptions,
|
|
35
|
+
) => {
|
|
30
36
|
// both used for debugging purpose
|
|
31
37
|
const nameByRef = new Map<string, string>();
|
|
32
38
|
const refByName = new Map<string, string>();
|
|
@@ -48,7 +54,11 @@ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) =
|
|
|
48
54
|
|
|
49
55
|
// "#/components/schemas/Something.jsonld" -> "Something.jsonld"
|
|
50
56
|
const name = split[split.length - 1]!;
|
|
51
|
-
|
|
57
|
+
let normalized = normalizeString(name);
|
|
58
|
+
if (nameTransform?.transformSchemaName) {
|
|
59
|
+
normalized = nameTransform.transformSchemaName(normalized);
|
|
60
|
+
}
|
|
61
|
+
normalized = sanitizeName(normalized, "schema");
|
|
52
62
|
|
|
53
63
|
nameByRef.set(correctRef, normalized);
|
|
54
64
|
refByName.set(normalized, correctRef);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const reservedWords = new Set([
|
|
2
|
+
// TS keywords and built-ins
|
|
3
|
+
"import",
|
|
4
|
+
"package",
|
|
5
|
+
"namespace",
|
|
6
|
+
"Record",
|
|
7
|
+
"Partial",
|
|
8
|
+
"Required",
|
|
9
|
+
"Readonly",
|
|
10
|
+
"Pick",
|
|
11
|
+
"Omit",
|
|
12
|
+
"String",
|
|
13
|
+
"Number",
|
|
14
|
+
"Boolean",
|
|
15
|
+
"Object",
|
|
16
|
+
"Array",
|
|
17
|
+
"Function",
|
|
18
|
+
"any",
|
|
19
|
+
"unknown",
|
|
20
|
+
"never",
|
|
21
|
+
"void",
|
|
22
|
+
"extends",
|
|
23
|
+
"super",
|
|
24
|
+
"class",
|
|
25
|
+
"interface",
|
|
26
|
+
"type",
|
|
27
|
+
"enum",
|
|
28
|
+
"const",
|
|
29
|
+
"let",
|
|
30
|
+
"var",
|
|
31
|
+
"if",
|
|
32
|
+
"else",
|
|
33
|
+
"for",
|
|
34
|
+
"while",
|
|
35
|
+
"do",
|
|
36
|
+
"switch",
|
|
37
|
+
"case",
|
|
38
|
+
"default",
|
|
39
|
+
"break",
|
|
40
|
+
"continue",
|
|
41
|
+
"return",
|
|
42
|
+
"try",
|
|
43
|
+
"catch",
|
|
44
|
+
"finally",
|
|
45
|
+
"throw",
|
|
46
|
+
"new",
|
|
47
|
+
"delete",
|
|
48
|
+
"in",
|
|
49
|
+
"instanceof",
|
|
50
|
+
"typeof",
|
|
51
|
+
"void",
|
|
52
|
+
"with",
|
|
53
|
+
"yield",
|
|
54
|
+
"await",
|
|
55
|
+
"static",
|
|
56
|
+
"public",
|
|
57
|
+
"private",
|
|
58
|
+
"protected",
|
|
59
|
+
"abstract",
|
|
60
|
+
"as",
|
|
61
|
+
"asserts",
|
|
62
|
+
"from",
|
|
63
|
+
"get",
|
|
64
|
+
"set",
|
|
65
|
+
"module",
|
|
66
|
+
"require",
|
|
67
|
+
"keyof",
|
|
68
|
+
"readonly",
|
|
69
|
+
"global",
|
|
70
|
+
"symbol",
|
|
71
|
+
"bigint",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export function sanitizeName(name: string, type: "schema" | "endpoint") {
|
|
75
|
+
let n = name.replace(/[\W/]+/g, "_");
|
|
76
|
+
if (/^\d/.test(n)) n = "_" + n;
|
|
77
|
+
if (reservedWords.has(n)) n = (type === "schema" ? "Schema_" : "Endpoint_") + n;
|
|
78
|
+
return n;
|
|
79
|
+
}
|