typed-openapi 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- const oneOf = t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
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<Endpoint["response"]>;
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])${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"]>`)};
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, QueryClient } from "@tanstack/react-query"
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: {} as TEndpoint,
892
- res: {} as TEndpoint["response"],
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, }) => {
@@ -897,18 +924,50 @@ var generateTanstackQueryFile = async (ctx) => {
897
924
  ...params,
898
925
  ...queryKey[0],
899
926
  signal,
900
- throwOnError: true
901
927
  });
902
928
  return res as TEndpoint["response"];
903
929
  },
904
930
  queryKey: queryKey
905
931
  }),
932
+ mutationOptions: {
933
+ mutationKey: queryKey,
934
+ mutationFn: async (localOptions) => {
935
+ const res = await this.client.${method}(path, {
936
+ ...params,
937
+ ...queryKey[0],
938
+ ...localOptions,
939
+ });
940
+ return res as TEndpoint["response"];
941
+ }
942
+ }
906
943
  };
907
944
 
908
945
  return query
909
946
  }
910
- // </ApiClient.get>
947
+ // </ApiClient.${method}>
911
948
  `).join("\n")}
949
+
950
+ // <ApiClient.request>
951
+ /**
952
+ * Generic mutation method with full type-safety for any endpoint that doesnt require parameters to be passed initially
953
+ */
954
+ mutation<
955
+ TMethod extends keyof EndpointByMethod,
956
+ TPath extends keyof EndpointByMethod[TMethod],
957
+ TEndpoint extends EndpointByMethod[TMethod][TPath]
958
+ >(
959
+ method: TMethod,
960
+ path: TPath) {
961
+ const mutationKey = [{ method, path }] as const;
962
+ return {
963
+ mutationKey: mutationKey,
964
+ mutationOptions: {
965
+ mutationKey: mutationKey,
966
+ mutationFn: async (params: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => this.client.request(method, path, params)
967
+ }
968
+ }
969
+ }
970
+ // </ApiClient.request>
912
971
  }
913
972
  `;
914
973
  return prettify(file);
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  generateTanstackQueryFile,
5
5
  mapOpenApiEndpoints,
6
6
  prettify
7
- } from "./chunk-STLPDNLW.js";
7
+ } from "./chunk-YSJGYFYM.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(cwd, typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
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
@@ -8,7 +8,7 @@ import {
8
8
  openApiSchemaToTs,
9
9
  tsFactory,
10
10
  unwrap
11
- } from "./chunk-STLPDNLW.js";
11
+ } from "./chunk-YSJGYFYM.js";
12
12
  export {
13
13
  createBoxFactory,
14
14
  createFactory,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "1.0.1",
4
+ "version": "1.1.1",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "exports": {
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(cwd, typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
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<Endpoint["response"]>;
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])${match(ctx.runtime)
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 but with 1 or more = `T extends oneOf ? T | T[] : never`
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
- const oneOf = t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
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) {
@@ -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, QueryClient } from "@tanstack/react-query"
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: {} as TEndpoint,
79
- res: {} as TEndpoint["response"],
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, }) => {
@@ -84,18 +84,50 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
84
84
  ...params,
85
85
  ...queryKey[0],
86
86
  signal,
87
- throwOnError: true
88
87
  });
89
88
  return res as TEndpoint["response"];
90
89
  },
91
90
  queryKey: queryKey
92
91
  }),
92
+ mutationOptions: {
93
+ mutationKey: queryKey,
94
+ mutationFn: async (localOptions) => {
95
+ const res = await this.client.${method}(path, {
96
+ ...params,
97
+ ...queryKey[0],
98
+ ...localOptions,
99
+ });
100
+ return res as TEndpoint["response"];
101
+ }
102
+ }
93
103
  };
94
104
 
95
105
  return query
96
106
  }
97
- // </ApiClient.get>
107
+ // </ApiClient.${method}>
98
108
  `).join("\n")}
109
+
110
+ // <ApiClient.request>
111
+ /**
112
+ * Generic mutation method with full type-safety for any endpoint that doesnt require parameters to be passed initially
113
+ */
114
+ mutation<
115
+ TMethod extends keyof EndpointByMethod,
116
+ TPath extends keyof EndpointByMethod[TMethod],
117
+ TEndpoint extends EndpointByMethod[TMethod][TPath]
118
+ >(
119
+ method: TMethod,
120
+ path: TPath) {
121
+ const mutationKey = [{ method, path }] as const;
122
+ return {
123
+ mutationKey: mutationKey,
124
+ mutationOptions: {
125
+ mutationKey: mutationKey,
126
+ mutationFn: async (params: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => this.client.request(method, path, params)
127
+ }
128
+ }
129
+ }
130
+ // </ApiClient.request>
99
131
  }
100
132
  `;
101
133