typed-openapi 1.0.0 → 1.1.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/bin.js +1 -1
- package/dist/{chunk-STLPDNLW.js → chunk-FTIKYHGK.js} +36 -9
- package/dist/cli.js +3 -2
- package/dist/index.js +1 -1
- package/package.json +6 -3
- package/src/cli.ts +2 -1
- package/src/generator.ts +35 -3
- package/src/openapi-schema-to-ts.ts +4 -3
- package/src/string-utils.ts +2 -1
- package/src/tanstack-query.generator.ts +3 -3
package/bin.js
CHANGED
|
@@ -30,7 +30,7 @@ var prefixStringStartingWithNumberIfNeeded = (str) => {
|
|
|
30
30
|
};
|
|
31
31
|
var pathParamWithBracketsRegex = /({\w+})/g;
|
|
32
32
|
var wordPrecededByNonWordCharacter = /[^\w\-]+/g;
|
|
33
|
-
var pathToVariableName = (path) => capitalize(kebabToCamel(path).replaceAll("/", "")).replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))).replace(wordPrecededByNonWordCharacter, "_");
|
|
33
|
+
var pathToVariableName = (path) => capitalize(kebabToCamel(path).replaceAll("/", "_")).replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))).replace(wordPrecededByNonWordCharacter, "_");
|
|
34
34
|
|
|
35
35
|
// src/openapi-schema-to-ts.ts
|
|
36
36
|
var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => {
|
|
@@ -63,8 +63,7 @@ var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => {
|
|
|
63
63
|
if (schema.anyOf.length === 1) {
|
|
64
64
|
return openApiSchemaToTs({ schema: schema.anyOf[0], ctx, meta });
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
return t.union([oneOf, t.array(oneOf)]);
|
|
66
|
+
return t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
|
|
68
67
|
}
|
|
69
68
|
if (schema.allOf) {
|
|
70
69
|
if (schema.allOf.length === 1) {
|
|
@@ -394,7 +393,6 @@ var generateEndpointByMethod = (ctx) => {
|
|
|
394
393
|
|
|
395
394
|
// <EndpointByMethod.Shorthands>
|
|
396
395
|
${Object.keys(byMethods).map((method) => `export type ${capitalize2(method)}Endpoints = EndpointByMethod["${method}"]`).join("\n")}
|
|
397
|
-
${endpointList.length ? `export type AllEndpoints = EndpointByMethod[keyof EndpointByMethod];` : ""}
|
|
398
396
|
// </EndpointByMethod.Shorthands>
|
|
399
397
|
`;
|
|
400
398
|
return endpointByMethod + shorthands;
|
|
@@ -436,7 +434,7 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
436
434
|
response: TConfig["response"];
|
|
437
435
|
};
|
|
438
436
|
|
|
439
|
-
type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<
|
|
437
|
+
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
|
|
440
438
|
|
|
441
439
|
type RequiredKeys<T> = {
|
|
442
440
|
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
@@ -458,6 +456,14 @@ export class ApiClient {
|
|
|
458
456
|
return this;
|
|
459
457
|
}
|
|
460
458
|
|
|
459
|
+
parseResponse = async <T>(response: Response): Promise<T> => {
|
|
460
|
+
const contentType = response.headers.get('content-type');
|
|
461
|
+
if (contentType?.includes('application/json')) {
|
|
462
|
+
return response.json();
|
|
463
|
+
}
|
|
464
|
+
return response.text() as unknown as T;
|
|
465
|
+
}
|
|
466
|
+
|
|
461
467
|
${Object.entries(byMethods).map(([method, endpointByMethod]) => {
|
|
462
468
|
const capitalizedMethod = capitalize2(method);
|
|
463
469
|
const infer = inferByRuntime[ctx.runtime];
|
|
@@ -466,11 +472,32 @@ export class ApiClient {
|
|
|
466
472
|
path: Path,
|
|
467
473
|
...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)}>
|
|
468
474
|
): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["response"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`).otherwise(() => `TEndpoint["response"]`)}> {
|
|
469
|
-
return this.fetcher("${method}", this.baseUrl + path, params[0])
|
|
475
|
+
return this.fetcher("${method}", this.baseUrl + path, params[0])
|
|
476
|
+
.then(response => this.parseResponse(response))${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`).otherwise(() => `as Promise<TEndpoint["response"]>`)};
|
|
470
477
|
}
|
|
471
478
|
// </ApiClient.${method}>
|
|
472
479
|
` : "";
|
|
473
480
|
}).join("\n")}
|
|
481
|
+
|
|
482
|
+
// <ApiClient.request>
|
|
483
|
+
/**
|
|
484
|
+
* Generic request method with full type-safety for any endpoint
|
|
485
|
+
*/
|
|
486
|
+
request<
|
|
487
|
+
TMethod extends keyof EndpointByMethod,
|
|
488
|
+
TPath extends keyof EndpointByMethod[TMethod],
|
|
489
|
+
TEndpoint extends EndpointByMethod[TMethod][TPath]
|
|
490
|
+
>(
|
|
491
|
+
method: TMethod,
|
|
492
|
+
path: TPath,
|
|
493
|
+
...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`)).with("arktype", "io-ts", "typebox", "valibot", () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
494
|
+
: Promise<Omit<Response, "json"> & {
|
|
495
|
+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
|
|
496
|
+
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
|
497
|
+
}> {
|
|
498
|
+
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
|
|
499
|
+
}
|
|
500
|
+
// </ApiClient.request>
|
|
474
501
|
}
|
|
475
502
|
|
|
476
503
|
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
|
@@ -822,7 +849,7 @@ import { capitalize as capitalize4 } from "pastable/server";
|
|
|
822
849
|
var generateTanstackQueryFile = async (ctx) => {
|
|
823
850
|
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
|
|
824
851
|
const file = `
|
|
825
|
-
import { queryOptions
|
|
852
|
+
import { queryOptions } from "@tanstack/react-query"
|
|
826
853
|
import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
|
|
827
854
|
|
|
828
855
|
type EndpointQueryKey<TOptions extends EndpointParameters> = [
|
|
@@ -888,8 +915,8 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
888
915
|
) {
|
|
889
916
|
const queryKey = createQueryKey(path, params[0]);
|
|
890
917
|
const query = {
|
|
891
|
-
endpoint
|
|
892
|
-
|
|
918
|
+
/** type-only property if you need easy access to the endpoint params */
|
|
919
|
+
"~endpoint": {} as TEndpoint,
|
|
893
920
|
queryKey,
|
|
894
921
|
options: queryOptions({
|
|
895
922
|
queryFn: async ({ queryKey, signal, }) => {
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
generateTanstackQueryFile,
|
|
5
5
|
mapOpenApiEndpoints,
|
|
6
6
|
prettify
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FTIKYHGK.js";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
@@ -13,6 +13,7 @@ import { basename, join } from "pathe";
|
|
|
13
13
|
import { type } from "arktype";
|
|
14
14
|
import { writeFile } from "fs/promises";
|
|
15
15
|
import { readFileSync } from "fs";
|
|
16
|
+
import { dirname } from "path";
|
|
16
17
|
var { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
17
18
|
var cwd = process.cwd();
|
|
18
19
|
var cli = cac(name);
|
|
@@ -39,7 +40,7 @@ cli.command("<input>", "Generate").option("-o, --output <path>", "Output path fo
|
|
|
39
40
|
...ctx,
|
|
40
41
|
relativeApiClientPath: "./" + basename(outputPath)
|
|
41
42
|
});
|
|
42
|
-
const tanstackOutputPath = join(
|
|
43
|
+
const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
43
44
|
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
44
45
|
await writeFile(tanstackOutputPath, tanstackContent);
|
|
45
46
|
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"main": "dist/index.
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
7
10
|
"bin": {
|
|
8
11
|
"typed-openapi": "bin.js"
|
|
9
12
|
},
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
"access": "public"
|
|
56
59
|
},
|
|
57
60
|
"scripts": {
|
|
58
|
-
"start": "node ./dist/cli.
|
|
61
|
+
"start": "node ./dist/cli.js",
|
|
59
62
|
"dev": "tsup --watch",
|
|
60
63
|
"build": "tsup",
|
|
61
64
|
"test": "vitest",
|
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
|
10
10
|
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
11
11
|
import { readFileSync } from "fs";
|
|
12
12
|
import { prettify } from "./format.ts";
|
|
13
|
+
import { dirname } from "path";
|
|
13
14
|
|
|
14
15
|
const { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
15
16
|
const cwd = process.cwd();
|
|
@@ -48,7 +49,7 @@ cli
|
|
|
48
49
|
...ctx,
|
|
49
50
|
relativeApiClientPath: './' + basename(outputPath),
|
|
50
51
|
});
|
|
51
|
-
const tanstackOutputPath = join(
|
|
52
|
+
const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
52
53
|
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
53
54
|
await writeFile(tanstackOutputPath, tanstackContent);
|
|
54
55
|
}
|
package/src/generator.ts
CHANGED
|
@@ -208,7 +208,6 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
208
208
|
${Object.keys(byMethods)
|
|
209
209
|
.map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"]`)
|
|
210
210
|
.join("\n")}
|
|
211
|
-
${endpointList.length ? `export type AllEndpoints = EndpointByMethod[keyof EndpointByMethod];` : ""}
|
|
212
211
|
// </EndpointByMethod.Shorthands>
|
|
213
212
|
`;
|
|
214
213
|
|
|
@@ -253,7 +252,7 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
253
252
|
response: TConfig["response"];
|
|
254
253
|
};
|
|
255
254
|
|
|
256
|
-
type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<
|
|
255
|
+
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
|
|
257
256
|
|
|
258
257
|
type RequiredKeys<T> = {
|
|
259
258
|
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
@@ -264,6 +263,7 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
|
|
|
264
263
|
// </ApiClientTypes>
|
|
265
264
|
`;
|
|
266
265
|
|
|
266
|
+
|
|
267
267
|
const apiClient = `
|
|
268
268
|
// <ApiClient>
|
|
269
269
|
export class ApiClient {
|
|
@@ -276,6 +276,14 @@ export class ApiClient {
|
|
|
276
276
|
return this;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
parseResponse = async <T>(response: Response): Promise<T> => {
|
|
280
|
+
const contentType = response.headers.get('content-type');
|
|
281
|
+
if (contentType?.includes('application/json')) {
|
|
282
|
+
return response.json();
|
|
283
|
+
}
|
|
284
|
+
return response.text() as unknown as T;
|
|
285
|
+
}
|
|
286
|
+
|
|
279
287
|
${Object.entries(byMethods)
|
|
280
288
|
.map(([method, endpointByMethod]) => {
|
|
281
289
|
const capitalizedMethod = capitalize(method);
|
|
@@ -293,7 +301,8 @@ export class ApiClient {
|
|
|
293
301
|
.with("zod", "yup", () => infer(`TEndpoint["response"]`))
|
|
294
302
|
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
|
|
295
303
|
.otherwise(() => `TEndpoint["response"]`)}> {
|
|
296
|
-
return this.fetcher("${method}", this.baseUrl + path, params[0])
|
|
304
|
+
return this.fetcher("${method}", this.baseUrl + path, params[0])
|
|
305
|
+
.then(response => this.parseResponse(response))${match(ctx.runtime)
|
|
297
306
|
.with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
|
|
298
307
|
.with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`)
|
|
299
308
|
.otherwise(() => `as Promise<TEndpoint["response"]>`)};
|
|
@@ -303,6 +312,29 @@ export class ApiClient {
|
|
|
303
312
|
: "";
|
|
304
313
|
})
|
|
305
314
|
.join("\n")}
|
|
315
|
+
|
|
316
|
+
// <ApiClient.request>
|
|
317
|
+
/**
|
|
318
|
+
* Generic request method with full type-safety for any endpoint
|
|
319
|
+
*/
|
|
320
|
+
request<
|
|
321
|
+
TMethod extends keyof EndpointByMethod,
|
|
322
|
+
TPath extends keyof EndpointByMethod[TMethod],
|
|
323
|
+
TEndpoint extends EndpointByMethod[TMethod][TPath]
|
|
324
|
+
>(
|
|
325
|
+
method: TMethod,
|
|
326
|
+
path: TPath,
|
|
327
|
+
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
328
|
+
.with("zod", "yup", () => inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`))
|
|
329
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`)
|
|
330
|
+
.otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
331
|
+
: Promise<Omit<Response, "json"> & {
|
|
332
|
+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
|
|
333
|
+
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
|
334
|
+
}> {
|
|
335
|
+
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
|
|
336
|
+
}
|
|
337
|
+
// </ApiClient.request>
|
|
306
338
|
}
|
|
307
339
|
|
|
308
340
|
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
|
@@ -40,14 +40,15 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
40
40
|
return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// anyOf = oneOf
|
|
43
|
+
// tl;dr: anyOf = oneOf
|
|
44
|
+
// oneOf matches exactly one subschema, and anyOf can match one or more subschemas.
|
|
45
|
+
// https://swagger.io/docs/specification/v3_0/data-models/oneof-anyof-allof-not/
|
|
44
46
|
if (schema.anyOf) {
|
|
45
47
|
if (schema.anyOf.length === 1) {
|
|
46
48
|
return openApiSchemaToTs({ schema: schema.anyOf[0]!, ctx, meta });
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
return t.union([oneOf, t.array(oneOf)]);
|
|
51
|
+
return t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
if (schema.allOf) {
|
package/src/string-utils.ts
CHANGED
|
@@ -35,8 +35,9 @@ const prefixStringStartingWithNumberIfNeeded = (str: string) => {
|
|
|
35
35
|
const pathParamWithBracketsRegex = /({\w+})/g;
|
|
36
36
|
const wordPrecededByNonWordCharacter = /[^\w\-]+/g;
|
|
37
37
|
|
|
38
|
+
|
|
38
39
|
/** @example turns `/media-objects/{id}` into `MediaObjectsId` */
|
|
39
40
|
export const pathToVariableName = (path: string) =>
|
|
40
|
-
capitalize(kebabToCamel(path).replaceAll("/", "")) // /media-objects/{id} -> MediaObjects{id}
|
|
41
|
+
capitalize(kebabToCamel((path)).replaceAll("/", "_")) // /media-objects/{id} -> MediaObjects{id}
|
|
41
42
|
.replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))) // {id} -> Id
|
|
42
43
|
.replace(wordPrecededByNonWordCharacter, "_"); // "/robots.txt" -> "/robots_txt"
|
|
@@ -9,7 +9,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
9
9
|
const endpointMethods = (new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase())));
|
|
10
10
|
|
|
11
11
|
const file = `
|
|
12
|
-
import { queryOptions
|
|
12
|
+
import { queryOptions } from "@tanstack/react-query"
|
|
13
13
|
import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
|
|
14
14
|
|
|
15
15
|
type EndpointQueryKey<TOptions extends EndpointParameters> = [
|
|
@@ -75,8 +75,8 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
75
75
|
) {
|
|
76
76
|
const queryKey = createQueryKey(path, params[0]);
|
|
77
77
|
const query = {
|
|
78
|
-
endpoint
|
|
79
|
-
|
|
78
|
+
/** type-only property if you need easy access to the endpoint params */
|
|
79
|
+
"~endpoint": {} as TEndpoint,
|
|
80
80
|
queryKey,
|
|
81
81
|
options: queryOptions({
|
|
82
82
|
queryFn: async ({ queryKey, signal, }) => {
|