typed-openapi 0.10.1 → 1.0.0
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 +7 -0
- package/dist/{chunk-WT2PCM73.js → chunk-STLPDNLW.js} +146 -62
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +23 -16
- package/dist/index.d.ts +21 -13
- package/dist/index.js +3 -1
- package/package.json +15 -16
- package/src/asserts.ts +2 -2
- package/src/box-factory.ts +4 -4
- package/src/box.ts +4 -4
- package/src/cli.ts +25 -10
- package/src/format.ts +1 -1
- package/src/generator.ts +71 -75
- package/src/index.ts +8 -7
- package/src/map-openapi-endpoints.ts +10 -10
- package/src/openapi-schema-to-ts.ts +17 -13
- package/src/ref-resolver.ts +11 -11
- package/src/tanstack-query.generator.ts +103 -0
- package/src/ts-factory.ts +3 -3
- package/src/types.ts +6 -3
- package/dist/cli.cjs +0 -897
- package/dist/cli.d.cts +0 -2
- package/dist/index.cjs +0 -889
- package/dist/index.d.cts +0 -268
package/src/cli.ts
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
2
2
|
import { cac } from "cac";
|
|
3
3
|
import type { OpenAPIObject } from "openapi3-ts/oas31";
|
|
4
|
-
import { join } from "pathe";
|
|
4
|
+
import { basename, join } from "pathe";
|
|
5
5
|
import { type } from "arktype";
|
|
6
6
|
|
|
7
7
|
import { writeFile } from "fs/promises";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { allowedRuntimes, generateFile } from "./generator.ts";
|
|
9
|
+
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
10
|
+
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { prettify } from "./format.ts";
|
|
11
13
|
|
|
14
|
+
const { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
12
15
|
const cwd = process.cwd();
|
|
13
16
|
const cli = cac(name);
|
|
14
17
|
const now = new Date();
|
|
15
18
|
|
|
16
|
-
const optionsSchema = type({ "output?": "string", runtime: allowedRuntimes });
|
|
19
|
+
const optionsSchema = type({ "output?": "string", runtime: allowedRuntimes, tanstack: "boolean | string" });
|
|
17
20
|
|
|
18
21
|
cli
|
|
19
22
|
.command("<input>", "Generate")
|
|
20
23
|
.option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)")
|
|
21
24
|
.option(
|
|
22
25
|
"-r, --runtime <name>",
|
|
23
|
-
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.
|
|
26
|
+
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
24
27
|
{ default: "none" },
|
|
25
28
|
)
|
|
29
|
+
.option("--tanstack [name]", "Generate tanstack client, defaults to false, can optionally specify a name for the generated file")
|
|
26
30
|
.action(async (input, _options) => {
|
|
27
31
|
const options = optionsSchema.assert(_options);
|
|
28
32
|
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
|
|
@@ -30,14 +34,25 @@ cli
|
|
|
30
34
|
const ctx = mapOpenApiEndpoints(openApiDoc);
|
|
31
35
|
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
32
36
|
|
|
33
|
-
const content = generateFile({ ...ctx, runtime: options.runtime });
|
|
34
|
-
const
|
|
37
|
+
const content = await prettify(generateFile({ ...ctx, runtime: options.runtime }));
|
|
38
|
+
const outputPath = join(
|
|
35
39
|
cwd,
|
|
36
40
|
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
|
|
37
41
|
);
|
|
38
42
|
|
|
39
|
-
console.log("Generating...",
|
|
40
|
-
await writeFile(
|
|
43
|
+
console.log("Generating client...", outputPath);
|
|
44
|
+
await writeFile(outputPath, content);
|
|
45
|
+
|
|
46
|
+
if (options.tanstack) {
|
|
47
|
+
const tanstackContent = await generateTanstackQueryFile({
|
|
48
|
+
...ctx,
|
|
49
|
+
relativeApiClientPath: './' + basename(outputPath),
|
|
50
|
+
});
|
|
51
|
+
const tanstackOutputPath = join(cwd, typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
52
|
+
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
53
|
+
await writeFile(tanstackOutputPath, tanstackContent);
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
|
|
42
57
|
});
|
|
43
58
|
|
package/src/format.ts
CHANGED
|
@@ -2,7 +2,7 @@ import prettier, { type Options } from "prettier";
|
|
|
2
2
|
import parserTypescript from "prettier/parser-typescript";
|
|
3
3
|
|
|
4
4
|
/** @see https://github.dev/stephenh/ts-poet/blob/5ea0dbb3c9f1f4b0ee51a54abb2d758102eda4a2/src/Code.ts#L231 */
|
|
5
|
-
function maybePretty(input: string, options?: Options | null)
|
|
5
|
+
function maybePretty(input: string, options?: Options | null) {
|
|
6
6
|
try {
|
|
7
7
|
return prettier.format(input, {
|
|
8
8
|
parser: "typescript",
|
package/src/generator.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { capitalize, groupBy } from "pastable/server";
|
|
2
|
-
import { Box } from "./box";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { AnyBox, AnyBoxDef } from "./types";
|
|
2
|
+
import { Box } from "./box.ts";
|
|
3
|
+
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
4
|
+
import { AnyBox, AnyBoxDef } from "./types.ts";
|
|
6
5
|
import * as Codegen from "@sinclair/typebox-codegen";
|
|
7
6
|
import { match } from "ts-pattern";
|
|
8
7
|
import { type } from "arktype";
|
|
9
|
-
import { wrapWithQuotesIfNeeded } from "./string-utils";
|
|
8
|
+
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
10
9
|
|
|
11
10
|
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
12
11
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
@@ -64,36 +63,36 @@ export const generateFile = (options: GeneratorOptions) => {
|
|
|
64
63
|
ctx.runtime === "none"
|
|
65
64
|
? (file: string) => file
|
|
66
65
|
: (file: string) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
const model = Codegen.TypeScriptToModel.Generate(file);
|
|
67
|
+
const transformer = runtimeValidationGenerator[ctx.runtime as Exclude<typeof ctx.runtime, "none">];
|
|
68
|
+
// tmp fix for typebox, there's currently a "// todo" only with Codegen.ModelToTypeBox.Generate
|
|
69
|
+
// https://github.com/sinclairzx81/typebox-codegen/blob/44d44d55932371b69f349331b1c8a60f5d760d9e/src/model/model-to-typebox.ts#L31
|
|
70
|
+
const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file) : transformer(model);
|
|
71
|
+
|
|
72
|
+
let converted = "";
|
|
73
|
+
const match = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/);
|
|
74
|
+
const content = match?.[2];
|
|
75
|
+
|
|
76
|
+
if (content && ctx.runtime in replacerByRuntime) {
|
|
77
|
+
const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START"));
|
|
78
|
+
converted =
|
|
79
|
+
before +
|
|
80
|
+
replacerByRuntime[ctx.runtime as keyof typeof replacerByRuntime](
|
|
81
|
+
content.slice(content.indexOf("export")),
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
converted = generated;
|
|
85
|
+
}
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
return converted;
|
|
88
|
+
};
|
|
90
89
|
|
|
91
90
|
const file = `
|
|
92
91
|
${transform(schemaList + endpointSchemaList)}
|
|
93
92
|
${apiClient}
|
|
94
93
|
`;
|
|
95
94
|
|
|
96
|
-
return
|
|
95
|
+
return (file);
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
|
|
@@ -138,39 +137,36 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
138
137
|
method: "${endpoint.method.toUpperCase()}",
|
|
139
138
|
path: "${endpoint.path}",
|
|
140
139
|
requestFormat: "${endpoint.requestFormat}",
|
|
141
|
-
${
|
|
142
|
-
|
|
143
|
-
? `parameters: {
|
|
140
|
+
${endpoint.meta.hasParameters
|
|
141
|
+
? `parameters: {
|
|
144
142
|
${parameters.query ? `query: ${parameterObjectToString(parameters.query)},` : ""}
|
|
145
143
|
${parameters.path ? `path: ${parameterObjectToString(parameters.path)},` : ""}
|
|
146
144
|
${parameters.header ? `header: ${parameterObjectToString(parameters.header)},` : ""}
|
|
147
|
-
${
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: ""
|
|
145
|
+
${parameters.body
|
|
146
|
+
? `body: ${parameterObjectToString(
|
|
147
|
+
ctx.runtime === "none"
|
|
148
|
+
? parameters.body.recompute((box) => {
|
|
149
|
+
if (Box.isReference(box) && !box.params.generics) {
|
|
150
|
+
box.value = `Schemas.${box.value}`;
|
|
151
|
+
}
|
|
152
|
+
return box;
|
|
153
|
+
})
|
|
154
|
+
: parameters.body,
|
|
155
|
+
)},`
|
|
156
|
+
: ""
|
|
160
157
|
}
|
|
161
158
|
}`
|
|
162
|
-
|
|
159
|
+
: "parameters: never,"
|
|
163
160
|
}
|
|
164
|
-
response: ${
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
: endpoint.response.value
|
|
161
|
+
response: ${ctx.runtime === "none"
|
|
162
|
+
? endpoint.response.recompute((box) => {
|
|
163
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
164
|
+
box.value = `Schemas.${box.value}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return box;
|
|
168
|
+
}).value
|
|
169
|
+
: endpoint.response.value
|
|
174
170
|
},
|
|
175
171
|
}\n`;
|
|
176
172
|
});
|
|
@@ -193,14 +189,14 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
193
189
|
// <EndpointByMethod>
|
|
194
190
|
export ${ctx.runtime === "none" ? "type" : "const"} EndpointByMethod = {
|
|
195
191
|
${Object.entries(byMethods)
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
.map(([method, list]) => {
|
|
193
|
+
return `${method}: {
|
|
198
194
|
${list.map(
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
|
|
196
|
+
)}
|
|
201
197
|
}`;
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
})
|
|
199
|
+
.join(",\n")}
|
|
204
200
|
}
|
|
205
201
|
${ctx.runtime === "none" ? "" : "export type EndpointByMethod = typeof EndpointByMethod;"}
|
|
206
202
|
// </EndpointByMethod>
|
|
@@ -281,22 +277,22 @@ export class ApiClient {
|
|
|
281
277
|
}
|
|
282
278
|
|
|
283
279
|
${Object.entries(byMethods)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
280
|
+
.map(([method, endpointByMethod]) => {
|
|
281
|
+
const capitalizedMethod = capitalize(method);
|
|
282
|
+
const infer = inferByRuntime[ctx.runtime];
|
|
287
283
|
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
return endpointByMethod.length
|
|
285
|
+
? `// <ApiClient.${method}>
|
|
290
286
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
291
287
|
path: Path,
|
|
292
288
|
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
.with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
|
|
290
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
|
|
291
|
+
.otherwise(() => `TEndpoint["parameters"]`)}>
|
|
296
292
|
): Promise<${match(ctx.runtime)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
293
|
+
.with("zod", "yup", () => infer(`TEndpoint["response"]`))
|
|
294
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
|
|
295
|
+
.otherwise(() => `TEndpoint["response"]`)}> {
|
|
300
296
|
return this.fetcher("${method}", this.baseUrl + path, params[0])${match(ctx.runtime)
|
|
301
297
|
.with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
|
|
302
298
|
.with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`)
|
|
@@ -304,9 +300,9 @@ export class ApiClient {
|
|
|
304
300
|
}
|
|
305
301
|
// </ApiClient.${method}>
|
|
306
302
|
`
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
: "";
|
|
304
|
+
})
|
|
305
|
+
.join("\n")}
|
|
310
306
|
}
|
|
311
307
|
|
|
312
308
|
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export * from "./box-factory";
|
|
2
|
-
export { generateFile, type OutputRuntime } from "./generator";
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./openapi-
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./ts
|
|
7
|
-
export * from "./
|
|
1
|
+
export * from "./box-factory.ts";
|
|
2
|
+
export { generateFile, type OutputRuntime } from "./generator.ts";
|
|
3
|
+
export * from "./tanstack-query.generator.ts"
|
|
4
|
+
export * from "./map-openapi-endpoints.ts";
|
|
5
|
+
export * from "./openapi-schema-to-ts.ts";
|
|
6
|
+
export * from "./ref-resolver.ts";
|
|
7
|
+
export * from "./ts-factory.ts";
|
|
8
|
+
export * from "./types.ts";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { OpenAPIObject, ResponseObject } from "openapi3-ts/oas31";
|
|
2
2
|
import { OperationObject, ParameterObject } from "openapi3-ts/oas31";
|
|
3
3
|
import { capitalize, pick } from "pastable/server";
|
|
4
|
-
import { Box } from "./box";
|
|
5
|
-
import { createBoxFactory } from "./box-factory";
|
|
6
|
-
import { openApiSchemaToTs } from "./openapi-schema-to-ts";
|
|
7
|
-
import { createRefResolver } from "./ref-resolver";
|
|
8
|
-
import { tsFactory } from "./ts-factory";
|
|
9
|
-
import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types";
|
|
10
|
-
import { pathToVariableName } from "./string-utils";
|
|
4
|
+
import { Box } from "./box.ts";
|
|
5
|
+
import { createBoxFactory } from "./box-factory.ts";
|
|
6
|
+
import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
|
|
7
|
+
import { createRefResolver } from "./ref-resolver.ts";
|
|
8
|
+
import { tsFactory } from "./ts-factory.ts";
|
|
9
|
+
import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types.ts";
|
|
10
|
+
import { pathToVariableName } from "./string-utils.ts";
|
|
11
11
|
import { match, P } from "ts-pattern";
|
|
12
12
|
|
|
13
13
|
const factory = tsFactory;
|
|
@@ -112,7 +112,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
}
|
|
115
|
+
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// No need to pass empty objects, it's confusing
|
|
@@ -121,13 +121,13 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
121
121
|
|
|
122
122
|
// Match the first 2xx-3xx response found, or fallback to default one otherwise
|
|
123
123
|
let responseObject: ResponseObject | undefined;
|
|
124
|
-
Object.entries(operation.responses).map(([status, responseOrRef]) => {
|
|
124
|
+
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
|
|
125
125
|
const statusCode = Number(status);
|
|
126
126
|
if (statusCode >= 200 && statusCode < 300) {
|
|
127
127
|
responseObject = refs.unwrap<ResponseObject>(responseOrRef);
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
|
-
if (!responseObject && operation.responses
|
|
130
|
+
if (!responseObject && operation.responses?.default) {
|
|
131
131
|
responseObject = refs.unwrap(operation.responses.default);
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { isPrimitiveType } from "./asserts";
|
|
2
|
-
import { Box } from "./box";
|
|
3
|
-
import { createBoxFactory } from "./box-factory";
|
|
4
|
-
import { isReferenceObject } from "./is-reference-object";
|
|
5
|
-
import { AnyBoxDef, OpenapiSchemaConvertArgs } from "./types";
|
|
6
|
-
import { wrapWithQuotesIfNeeded } from "./string-utils";
|
|
7
|
-
import type { SchemaObject } from "openapi3-ts/oas30";
|
|
1
|
+
import { isPrimitiveType } from "./asserts.ts";
|
|
2
|
+
import { Box } from "./box.ts";
|
|
3
|
+
import { createBoxFactory } from "./box-factory.ts";
|
|
4
|
+
import { isReferenceObject } from "./is-reference-object.ts";
|
|
5
|
+
import { AnyBoxDef, OpenapiSchemaConvertArgs, type LibSchemaObject } from "./types.ts";
|
|
6
|
+
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
8
7
|
|
|
9
8
|
export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: OpenapiSchemaConvertArgs): Box<AnyBoxDef> => {
|
|
10
9
|
const meta = {} as OpenapiSchemaConvertArgs["meta"];
|
|
@@ -13,7 +12,7 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
13
12
|
throw new Error("Schema is required");
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
const t = createBoxFactory(schema, ctx);
|
|
15
|
+
const t = createBoxFactory(schema as LibSchemaObject, ctx);
|
|
17
16
|
const getTs = () => {
|
|
18
17
|
if (isReferenceObject(schema)) {
|
|
19
18
|
const refInfo = ctx.refs.getInfosByRef(schema.$ref);
|
|
@@ -30,7 +29,7 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
if (schema.type === "null") {
|
|
33
|
-
return t.
|
|
32
|
+
return t.literal("null");
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
if (schema.oneOf) {
|
|
@@ -82,7 +81,7 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
82
81
|
if (schemaType === "string") return t.string();
|
|
83
82
|
if (schemaType === "boolean") return t.boolean();
|
|
84
83
|
if (schemaType === "number" || schemaType === "integer") return t.number();
|
|
85
|
-
if (schemaType === "null") return t.
|
|
84
|
+
if (schemaType === "null") return t.literal("null");
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
if (schemaType === "array") {
|
|
@@ -100,7 +99,12 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
100
99
|
|
|
101
100
|
if (schemaType === "object" || schema.properties || schema.additionalProperties) {
|
|
102
101
|
if (!schema.properties) {
|
|
103
|
-
|
|
102
|
+
if (schema.additionalProperties && !isReferenceObject(schema.additionalProperties) && typeof schema.additionalProperties !== "boolean" && schema.additionalProperties.type) {
|
|
103
|
+
const valueSchema = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta });
|
|
104
|
+
return t.literal(`Record<string, ${valueSchema.value}>`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return t.literal("Record<string, unknown>");
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
let additionalProperties;
|
|
@@ -154,8 +158,8 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
154
158
|
let output = getTs();
|
|
155
159
|
if (!isReferenceObject(schema)) {
|
|
156
160
|
// OpenAPI 3.1 does not have nullable, but OpenAPI 3.0 does
|
|
157
|
-
if ((schema as
|
|
158
|
-
output = t.union([output, t.
|
|
161
|
+
if ((schema as LibSchemaObject).nullable) {
|
|
162
|
+
output = t.union([output, t.literal("null")]);
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
165
|
|
package/src/ref-resolver.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { OpenAPIObject, ReferenceObject
|
|
1
|
+
import type { OpenAPIObject, ReferenceObject } from "openapi3-ts/oas31";
|
|
2
2
|
import { get } from "pastable/server";
|
|
3
3
|
|
|
4
|
-
import { Box } from "./box";
|
|
5
|
-
import { isReferenceObject } from "./is-reference-object";
|
|
6
|
-
import { openApiSchemaToTs } from "./openapi-schema-to-ts";
|
|
7
|
-
import { normalizeString } from "./string-utils";
|
|
8
|
-
import { AnyBoxDef, GenericFactory } from "./types";
|
|
9
|
-
import { topologicalSort } from "./topological-sort";
|
|
4
|
+
import { Box } from "./box.ts";
|
|
5
|
+
import { isReferenceObject } from "./is-reference-object.ts";
|
|
6
|
+
import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
|
|
7
|
+
import { normalizeString } from "./string-utils.ts";
|
|
8
|
+
import { AnyBoxDef, GenericFactory, type LibSchemaObject } from "./types.ts";
|
|
9
|
+
import { topologicalSort } from "./topological-sort.ts";
|
|
10
10
|
|
|
11
11
|
const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
|
|
12
12
|
const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
|
|
@@ -36,7 +36,7 @@ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) =
|
|
|
36
36
|
|
|
37
37
|
const boxByRef = new Map<string, Box<AnyBoxDef>>();
|
|
38
38
|
|
|
39
|
-
const getSchemaByRef = <T =
|
|
39
|
+
const getSchemaByRef = <T = LibSchemaObject>(ref: string) => {
|
|
40
40
|
// #components -> #/components
|
|
41
41
|
const correctRef = autocorrectRef(ref);
|
|
42
42
|
const split = correctRef.split("/");
|
|
@@ -119,10 +119,10 @@ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) =
|
|
|
119
119
|
};
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
export interface RefResolver extends ReturnType<typeof createRefResolver> {}
|
|
122
|
+
export interface RefResolver extends ReturnType<typeof createRefResolver> { }
|
|
123
123
|
|
|
124
|
-
const setSchemaDependencies = (schema:
|
|
125
|
-
const visit = (schema:
|
|
124
|
+
const setSchemaDependencies = (schema: LibSchemaObject, deps: Set<string>) => {
|
|
125
|
+
const visit = (schema: LibSchemaObject | ReferenceObject): void => {
|
|
126
126
|
if (!schema) return;
|
|
127
127
|
|
|
128
128
|
if (isReferenceObject(schema)) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { capitalize } from "pastable/server";
|
|
2
|
+
import { prettify } from "./format.ts";
|
|
3
|
+
import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
4
|
+
|
|
5
|
+
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>
|
|
6
|
+
type GeneratorContext = Required<GeneratorOptions>;
|
|
7
|
+
|
|
8
|
+
export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => {
|
|
9
|
+
const endpointMethods = (new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase())));
|
|
10
|
+
|
|
11
|
+
const file = `
|
|
12
|
+
import { queryOptions, QueryClient } from "@tanstack/react-query"
|
|
13
|
+
import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
|
|
14
|
+
|
|
15
|
+
type EndpointQueryKey<TOptions extends EndpointParameters> = [
|
|
16
|
+
TOptions & {
|
|
17
|
+
_id: string;
|
|
18
|
+
_infinite?: boolean;
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const createQueryKey = <TOptions extends EndpointParameters>(id: string, options?: TOptions, infinite?: boolean): [
|
|
23
|
+
EndpointQueryKey<TOptions>[0]
|
|
24
|
+
] => {
|
|
25
|
+
const params: EndpointQueryKey<TOptions>[0] = { _id: id, } as EndpointQueryKey<TOptions>[0];
|
|
26
|
+
if (infinite) {
|
|
27
|
+
params._infinite = infinite;
|
|
28
|
+
}
|
|
29
|
+
if (options?.body) {
|
|
30
|
+
params.body = options.body;
|
|
31
|
+
}
|
|
32
|
+
if (options?.header) {
|
|
33
|
+
params.header = options.header;
|
|
34
|
+
}
|
|
35
|
+
if (options?.path) {
|
|
36
|
+
params.path = options.path;
|
|
37
|
+
}
|
|
38
|
+
if (options?.query) {
|
|
39
|
+
params.query = options.query;
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
params
|
|
43
|
+
];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// <EndpointByMethod.Shorthands>
|
|
47
|
+
${Array.from(endpointMethods).map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"];`).join("\n")}
|
|
48
|
+
// </EndpointByMethod.Shorthands>
|
|
49
|
+
|
|
50
|
+
// <ApiClientTypes>
|
|
51
|
+
export type EndpointParameters = {
|
|
52
|
+
body?: unknown;
|
|
53
|
+
query?: Record<string, unknown>;
|
|
54
|
+
header?: Record<string, unknown>;
|
|
55
|
+
path?: Record<string, unknown>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type RequiredKeys<T> = {
|
|
59
|
+
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
60
|
+
}[keyof T];
|
|
61
|
+
|
|
62
|
+
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
|
|
63
|
+
|
|
64
|
+
// </ApiClientTypes>
|
|
65
|
+
|
|
66
|
+
// <ApiClient>
|
|
67
|
+
export class TanstackQueryApiClient {
|
|
68
|
+
constructor(public client: ApiClient) { }
|
|
69
|
+
|
|
70
|
+
${Array.from(endpointMethods).map((method) => `
|
|
71
|
+
// <ApiClient.${method}>
|
|
72
|
+
${method}<Path extends keyof ${capitalize(method)}Endpoints, TEndpoint extends ${capitalize(method)}Endpoints[Path]>(
|
|
73
|
+
path: Path,
|
|
74
|
+
...params: MaybeOptionalArg<TEndpoint["parameters"]>
|
|
75
|
+
) {
|
|
76
|
+
const queryKey = createQueryKey(path, params[0]);
|
|
77
|
+
const query = {
|
|
78
|
+
endpoint: {} as TEndpoint,
|
|
79
|
+
res: {} as TEndpoint["response"],
|
|
80
|
+
queryKey,
|
|
81
|
+
options: queryOptions({
|
|
82
|
+
queryFn: async ({ queryKey, signal, }) => {
|
|
83
|
+
const res = await this.client.${method}(path, {
|
|
84
|
+
...params,
|
|
85
|
+
...queryKey[0],
|
|
86
|
+
signal,
|
|
87
|
+
throwOnError: true
|
|
88
|
+
});
|
|
89
|
+
return res as TEndpoint["response"];
|
|
90
|
+
},
|
|
91
|
+
queryKey: queryKey
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return query
|
|
96
|
+
}
|
|
97
|
+
// </ApiClient.get>
|
|
98
|
+
`).join("\n")}
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
return prettify(file);
|
|
103
|
+
};
|
package/src/ts-factory.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Box } from "./box";
|
|
2
|
-
import { createFactory, unwrap } from "./box-factory";
|
|
3
|
-
import { wrapWithQuotesIfNeeded } from "./string-utils";
|
|
1
|
+
import { Box } from "./box.ts";
|
|
2
|
+
import { createFactory, unwrap } from "./box-factory.ts";
|
|
3
|
+
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
4
4
|
|
|
5
5
|
export const tsFactory = createFactory({
|
|
6
6
|
union: (types) => types.map(unwrap).join(" | "),
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ReferenceObject, SchemaObject } from "openapi3-ts/oas31";
|
|
2
|
+
import type { SchemaObject as SchemaObject3 } from "openapi3-ts/oas30";
|
|
2
3
|
|
|
3
|
-
import type { RefResolver } from "./ref-resolver";
|
|
4
|
-
import { Box } from "./box";
|
|
4
|
+
import type { RefResolver } from "./ref-resolver.ts";
|
|
5
|
+
import { Box } from "./box.ts";
|
|
6
|
+
|
|
7
|
+
export type LibSchemaObject = SchemaObject & SchemaObject3
|
|
5
8
|
|
|
6
9
|
export type BoxDefinition = {
|
|
7
10
|
type: string;
|
|
@@ -10,7 +13,7 @@ export type BoxDefinition = {
|
|
|
10
13
|
};
|
|
11
14
|
export type BoxParams = string | BoxDefinition;
|
|
12
15
|
export type WithSchema = {
|
|
13
|
-
schema:
|
|
16
|
+
schema: LibSchemaObject | ReferenceObject | undefined;
|
|
14
17
|
ctx: OpenapiSchemaConvertContext;
|
|
15
18
|
};
|
|
16
19
|
|