typed-openapi 0.10.1 → 1.0.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/src/box.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { SchemaObject } from "openapi3-ts/oas31";
2
- import { openApiSchemaToTs } from "./openapi-schema-to-ts";
1
+ import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
3
2
  import {
4
3
  AnyBoxDef,
5
4
  BoxArray,
@@ -11,7 +10,8 @@ import {
11
10
  BoxRef,
12
11
  BoxUnion,
13
12
  OpenapiSchemaConvertContext,
14
- } from "./types";
13
+ type LibSchemaObject,
14
+ } from "./types.ts";
15
15
 
16
16
  // TODO rename SchemaBox
17
17
  export class Box<T extends AnyBoxDef = AnyBoxDef> {
@@ -39,7 +39,7 @@ export class Box<T extends AnyBoxDef = AnyBoxDef> {
39
39
  }
40
40
 
41
41
  recompute(callback: OpenapiSchemaConvertContext["onBox"]) {
42
- return openApiSchemaToTs({ schema: this.schema as SchemaObject, ctx: { ...this.ctx, onBox: callback! } });
42
+ return openApiSchemaToTs({ schema: this.schema as LibSchemaObject, ctx: { ...this.ctx, onBox: callback! } });
43
43
  }
44
44
 
45
45
  static fromJSON(json: string) {
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 { name, version } from "../package.json";
9
- import { allowedRuntimes, generateFile } from "./generator";
10
- import { mapOpenApiEndpoints } from "./map-openapi-endpoints";
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.definition}`,
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 output = join(
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...", output);
40
- await writeFile(output, content);
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): string {
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 { prettify } from "./format";
4
- import { mapOpenApiEndpoints } from "./map-openapi-endpoints";
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
- const model = Codegen.TypeScriptToModel.Generate(file);
68
- const transformer = runtimeValidationGenerator[ctx.runtime as Exclude<typeof ctx.runtime, "none">];
69
- // tmp fix for typebox, there's currently a "// todo" only with Codegen.ModelToTypeBox.Generate
70
- // https://github.com/sinclairzx81/typebox-codegen/blob/44d44d55932371b69f349331b1c8a60f5d760d9e/src/model/model-to-typebox.ts#L31
71
- const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file) : transformer(model);
72
-
73
- let converted = "";
74
- const match = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/);
75
- const content = match?.[2];
76
-
77
- if (content && ctx.runtime in replacerByRuntime) {
78
- const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START"));
79
- converted =
80
- before +
81
- replacerByRuntime[ctx.runtime as keyof typeof replacerByRuntime](
82
- content.slice(content.indexOf("export")),
83
- );
84
- } else {
85
- converted = generated;
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
- return converted;
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 prettify(file);
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
- endpoint.meta.hasParameters
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
- parameters.body
149
- ? `body: ${parameterObjectToString(
150
- ctx.runtime === "none"
151
- ? parameters.body.recompute((box) => {
152
- if (Box.isReference(box) && !box.params.generics) {
153
- box.value = `Schemas.${box.value}`;
154
- }
155
- return box;
156
- })
157
- : parameters.body,
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
- : "parameters: never,"
159
+ : "parameters: never,"
163
160
  }
164
- response: ${
165
- ctx.runtime === "none"
166
- ? endpoint.response.recompute((box) => {
167
- if (Box.isReference(box) && !box.params.generics) {
168
- box.value = `Schemas.${box.value}`;
169
- }
170
-
171
- return box;
172
- }).value
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
- .map(([method, list]) => {
197
- return `${method}: {
192
+ .map(([method, list]) => {
193
+ return `${method}: {
198
194
  ${list.map(
199
- (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
200
- )}
195
+ (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
196
+ )}
201
197
  }`;
202
- })
203
- .join(",\n")}
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
- .map(([method, endpointByMethod]) => {
285
- const capitalizedMethod = capitalize(method);
286
- const infer = inferByRuntime[ctx.runtime];
280
+ .map(([method, endpointByMethod]) => {
281
+ const capitalizedMethod = capitalize(method);
282
+ const infer = inferByRuntime[ctx.runtime];
287
283
 
288
- return endpointByMethod.length
289
- ? `// <ApiClient.${method}>
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
- .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
294
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
295
- .otherwise(() => `TEndpoint["parameters"]`)}>
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
- .with("zod", "yup", () => infer(`TEndpoint["response"]`))
298
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
299
- .otherwise(() => `TEndpoint["response"]`)}> {
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
- .join("\n")}
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 "./map-openapi-endpoints";
4
- export * from "./openapi-schema-to-ts";
5
- export * from "./ref-resolver";
6
- export * from "./ts-factory";
7
- export * from "./types";
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.default) {
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.reference("null");
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.reference("null");
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
- return t.unknown();
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 any as SchemaObject).nullable) {
158
- output = t.union([output, t.reference("null")]);
161
+ if ((schema as LibSchemaObject).nullable) {
162
+ output = t.union([output, t.literal("null")]);
159
163
  }
160
164
  }
161
165
 
@@ -1,12 +1,12 @@
1
- import type { OpenAPIObject, ReferenceObject, SchemaObject } from "openapi3-ts/oas31";
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 = SchemaObject>(ref: string) => {
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: SchemaObject, deps: Set<string>) => {
125
- const visit = (schema: SchemaObject | ReferenceObject): void => {
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: SchemaObject | ReferenceObject | undefined;
16
+ schema: LibSchemaObject | ReferenceObject | undefined;
14
17
  ctx: OpenapiSchemaConvertContext;
15
18
  };
16
19