@7nohe/openapi-react-query-codegen 1.2.0 → 1.2.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/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  > Node.js library that generates [React Query (also called TanStack Query)](https://tanstack.com/query) hooks based on an OpenAPI specification file.
4
4
 
5
+ [![npm version](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen.svg)](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen)
6
+
5
7
  ## Features
6
8
 
7
- - Supports generation of custom react hooks that use React Query's `useQuery` and `useMutation` hooks
8
- - Supports generation of query keys for query caching
9
- - Supports the option to use pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
9
+ - Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery` and `useMutation` hooks
10
+ - Generates query keys and functions for query caching
11
+ - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
10
12
 
11
- ## Install
13
+ ## Installation
12
14
 
13
15
  ```
14
16
  $ npm install -D @7nohe/openapi-react-query-codegen
@@ -48,7 +50,7 @@ Options:
48
50
  --format <value> Process output folder with formatter? ['biome', 'prettier']
49
51
  --lint <value> Process output folder with linter? ['eslint', 'biome']
50
52
  --operationId Use operation ID to generate operation names?
51
- --serviceResponse <value> Define shape of returned value from service calls ['body', 'generics', 'response']
53
+ --serviceResponse <value> Define shape of returned value from service calls ['body', 'response'] (default: "body")
52
54
  --base <value> Manually set base in OpenAPI config instead of inferring from server value
53
55
  --enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
54
56
  --useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
@@ -58,27 +60,32 @@ Options:
58
60
  -h, --help display help for command
59
61
  ```
60
62
 
61
- ## Example Usage
63
+ ### Example Usage
62
64
 
63
- ### Command
65
+ #### Command
64
66
 
65
67
  ```
66
68
  $ openapi-rq -i ./petstore.yaml
67
69
  ```
68
70
 
69
- ### Output directory structure
71
+ #### Output directory structure
70
72
 
71
73
  ```
72
74
  - openapi
73
75
  - queries
74
- - index.ts <- main file that exports common types, variables, and hooks
76
+ - index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
75
77
  - common.ts <- common types
76
78
  - queries.ts <- generated query hooks
77
79
  - suspenses.ts <- generated suspense hooks
80
+ - prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below
78
81
  - requests <- output code generated by @hey-api/openapi-ts
79
82
  ```
80
83
 
81
- ### In your app
84
+ - [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)
85
+
86
+ #### In your app
87
+
88
+ ##### Using the generated hooks
82
89
 
83
90
  ```tsx
84
91
  // App.tsx
@@ -97,7 +104,7 @@ function App() {
97
104
  export default App;
98
105
  ```
99
106
 
100
- You can also use pure TS clients.
107
+ ##### Using the generated typescript client
101
108
 
102
109
  ```tsx
103
110
  import { useQuery } from "@tanstack/react-query";
@@ -120,7 +127,7 @@ function App() {
120
127
  export default App;
121
128
  ```
122
129
 
123
- You can also use suspense hooks.
130
+ ##### Using Suspense Hooks
124
131
 
125
132
  ```tsx
126
133
  // App.tsx
@@ -153,6 +160,34 @@ function App() {
153
160
  export default App;
154
161
  ```
155
162
 
163
+ ##### Runtime Configuration
164
+
165
+ You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
166
+
167
+ It's default location is `openapi/requests/core/OpenAPI.ts` and it is also exported from `openapi/index.ts`
168
+
169
+ Import the constant into your runtime and modify it before setting up the react app.
170
+
171
+ ```typescript
172
+ /** main.tsx */
173
+ import { OpenAPI as OpenAPIConfig } from './openapi/requests/core/OpenAPI';
174
+ ...
175
+ OpenAPIConfig.BASE = 'www.domain.com/api';
176
+ OpenAPIConfig.HEADERS = {
177
+ 'x-header-1': 'value-1',
178
+ 'x-header-2': 'value-2',
179
+ };
180
+ ...
181
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
182
+ <React.StrictMode>
183
+ <QueryClientProvider client={queryClient}>
184
+ <App />
185
+ </QueryClientProvider>
186
+ </React.StrictMode>
187
+ );
188
+
189
+ ```
190
+
156
191
  ## License
157
192
 
158
193
  MIT
package/dist/cli.mjs CHANGED
@@ -25,7 +25,9 @@ async function setupProgram() {
25
25
  .addOption(new Option("--format <value>", "Process output folder with formatter?").choices(["biome", "prettier"]))
26
26
  .addOption(new Option("--lint <value>", "Process output folder with linter?").choices(["biome", "eslint"]))
27
27
  .option("--operationId", "Use operation ID to generate operation names?")
28
- .addOption(new Option("--serviceResponse <value>", "Define shape of returned value from service calls").choices(["body", "response"]))
28
+ .addOption(new Option("--serviceResponse <value>", "Define shape of returned value from service calls")
29
+ .choices(["body", "response"])
30
+ .default("body"))
29
31
  .option("--base <value>", "Manually set base in OpenAPI config instead of inferring from server value")
30
32
  .addOption(new Option("--enums <value>", "Generate JavaScript objects from enum definitions?").choices(["javascript", "typescript"]))
31
33
  .option("--useDateType", "Use Date type instead of string for date types for models, this will not convert the data to a Date object")
@@ -3,3 +3,10 @@ export const queriesOutputPath = "queries";
3
3
  export const requestsOutputPath = "requests";
4
4
  export const serviceFileName = "services.gen";
5
5
  export const modalsFileName = "types.gen";
6
+ export const OpenApiRqFiles = {
7
+ queries: "queries",
8
+ common: "common",
9
+ suspense: "suspense",
10
+ index: "index",
11
+ prefetch: "prefetch",
12
+ };
@@ -1,5 +1,6 @@
1
1
  import { createUseQuery } from "./createUseQuery.mjs";
2
2
  import { createUseMutation } from "./createUseMutation.mjs";
3
+ import { createPrefetch } from "./createPrefetch.mjs";
3
4
  export const createExports = (service) => {
4
5
  const { klasses } = service;
5
6
  const methods = klasses.map((k) => k.methods).flat();
@@ -9,6 +10,7 @@ export const createExports = (service) => {
9
10
  const allPatch = methods.filter((m) => m.httpMethodName.toUpperCase().includes("PATCH"));
10
11
  const allDelete = methods.filter((m) => m.httpMethodName.toUpperCase().includes("DELETE"));
11
12
  const allGetQueries = allGet.map((m) => createUseQuery(m));
13
+ const allPrefetchQueries = allGet.map((m) => createPrefetch(m));
12
14
  const allPostMutations = allPost.map((m) => createUseMutation(m));
13
15
  const allPutMutations = allPut.map((m) => createUseMutation(m));
14
16
  const allPatchMutations = allPatch.map((m) => createUseMutation(m));
@@ -36,6 +38,10 @@ export const createExports = (service) => {
36
38
  .map(({ suspenseQueryHook }) => [suspenseQueryHook])
37
39
  .flat();
38
40
  const suspenseExports = [...suspenseQueries];
41
+ const allPrefetches = allPrefetchQueries
42
+ .map(({ prefetchHook }) => [prefetchHook])
43
+ .flat();
44
+ const allPrefetchExports = [...allPrefetches];
39
45
  return {
40
46
  /**
41
47
  * Common types and variables between queries (regular and suspense) and mutations
@@ -49,5 +55,9 @@ export const createExports = (service) => {
49
55
  * Suspense exports are the hooks that are used in the suspense components
50
56
  */
51
57
  suspenseExports,
58
+ /**
59
+ * Prefetch exports are the hooks that are used in the prefetch components
60
+ */
61
+ allPrefetchExports,
52
62
  };
53
63
  };
@@ -0,0 +1,58 @@
1
+ import ts from "typescript";
2
+ import { BuildCommonTypeName, extractPropertiesFromObjectParam, getNameFromMethod, } from "./common.mjs";
3
+ import { createQueryKeyFromMethod, getRequestParamFromMethod, hookNameFromMethod, } from "./createUseQuery.mjs";
4
+ import { addJSDocToNode } from "./util.mjs";
5
+ /**
6
+ * Creates a prefetch function for a query
7
+ */
8
+ function createPrefetchHook({ requestParams, method, className, }) {
9
+ const methodName = getNameFromMethod(method);
10
+ const queryName = hookNameFromMethod({ method, className });
11
+ const customHookName = `prefetch${queryName.charAt(0).toUpperCase() + queryName.slice(1)}`;
12
+ const queryKey = createQueryKeyFromMethod({ method, className });
13
+ // const
14
+ const hookExport = ts.factory.createVariableStatement(
15
+ // export
16
+ [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
17
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(customHookName), undefined, undefined, ts.factory.createArrowFunction(undefined, undefined, [
18
+ ts.factory.createParameterDeclaration(undefined, undefined, "queryClient", undefined, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("QueryClient"))),
19
+ ...requestParams,
20
+ ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createIdentifier("queryClient.prefetchQuery"), undefined, [
21
+ ts.factory.createObjectLiteralExpression([
22
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryKey"), ts.factory.createArrayLiteralExpression([
23
+ BuildCommonTypeName(queryKey),
24
+ method.getParameters().length
25
+ ? ts.factory.createArrayLiteralExpression([
26
+ ts.factory.createObjectLiteralExpression(method
27
+ .getParameters()
28
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
29
+ .flat()),
30
+ ])
31
+ : ts.factory.createArrayLiteralExpression([]),
32
+ ], false)),
33
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryFn"), ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined, method.getParameters().length
34
+ ? [
35
+ ts.factory.createObjectLiteralExpression(method
36
+ .getParameters()
37
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
38
+ .flat()),
39
+ ]
40
+ : undefined))),
41
+ ]),
42
+ ]))),
43
+ ], ts.NodeFlags.Const));
44
+ return hookExport;
45
+ }
46
+ export const createPrefetch = ({ className, method, jsDoc, }) => {
47
+ const requestParam = getRequestParamFromMethod(method);
48
+ const requestParams = requestParam ? [requestParam] : [];
49
+ const prefetchHook = createPrefetchHook({
50
+ requestParams,
51
+ method,
52
+ className,
53
+ });
54
+ const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc);
55
+ return {
56
+ prefetchHook: hookWithJsDoc,
57
+ };
58
+ };
@@ -1,9 +1,10 @@
1
1
  import ts from "typescript";
2
+ import { Project } from "ts-morph";
3
+ import { join } from "path";
4
+ import { OpenApiRqFiles } from "./constants.mjs";
2
5
  import { createImports } from "./createImports.mjs";
3
6
  import { createExports } from "./createExports.mjs";
4
7
  import { getServices } from "./service.mjs";
5
- import { Project } from "ts-morph";
6
- import { join } from "path";
7
8
  const createSourceFile = async (outputPath, serviceEndName) => {
8
9
  const project = new Project({
9
10
  // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here.
@@ -21,29 +22,32 @@ const createSourceFile = async (outputPath, serviceEndName) => {
21
22
  });
22
23
  const exports = createExports(service);
23
24
  const commonSource = ts.factory.createSourceFile([...imports, ...exports.allCommon], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
24
- const commonImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, ts.factory.createIdentifier("* as Common"), undefined), ts.factory.createStringLiteral("./common"), undefined);
25
- const commonExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral("./common"), undefined);
26
- const queriesExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral("./queries"), undefined);
25
+ const commonImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, ts.factory.createIdentifier("* as Common"), undefined), ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), undefined);
26
+ const commonExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), undefined);
27
+ const queriesExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral(`./${OpenApiRqFiles.queries}`), undefined);
27
28
  const mainSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.mainExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
28
29
  const suspenseSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.suspenseExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
29
30
  const indexSource = ts.factory.createSourceFile([commonExport, queriesExport], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
31
+ const prefetchSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.allPrefetchExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
30
32
  return {
31
33
  commonSource,
32
34
  mainSource,
33
35
  suspenseSource,
34
36
  indexSource,
37
+ prefetchSource,
35
38
  };
36
39
  };
37
40
  export const createSource = async ({ outputPath, version, serviceEndName, }) => {
38
- const queriesFile = ts.createSourceFile("queries.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
39
- const commonFile = ts.createSourceFile("common.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
40
- const suspenseFile = ts.createSourceFile("suspense.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
41
- const indexFile = ts.createSourceFile("index.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
41
+ const queriesFile = ts.createSourceFile(`${OpenApiRqFiles.queries}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
42
+ const commonFile = ts.createSourceFile(`${OpenApiRqFiles.common}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
43
+ const suspenseFile = ts.createSourceFile(`${OpenApiRqFiles.suspense}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
44
+ const indexFile = ts.createSourceFile(`${OpenApiRqFiles.index}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
45
+ const prefetchFile = ts.createSourceFile(`${OpenApiRqFiles.prefetch}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
42
46
  const printer = ts.createPrinter({
43
47
  newLine: ts.NewLineKind.LineFeed,
44
48
  removeComments: false,
45
49
  });
46
- const { commonSource, mainSource, suspenseSource, indexSource } = await createSourceFile(outputPath, serviceEndName);
50
+ const { commonSource, mainSource, suspenseSource, indexSource, prefetchSource, } = await createSourceFile(outputPath, serviceEndName);
47
51
  const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`;
48
52
  const commonResult = comment +
49
53
  printer.printNode(ts.EmitHint.Unspecified, commonSource, commonFile);
@@ -53,22 +57,28 @@ export const createSource = async ({ outputPath, version, serviceEndName, }) =>
53
57
  printer.printNode(ts.EmitHint.Unspecified, suspenseSource, suspenseFile);
54
58
  const indexResult = comment +
55
59
  printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile);
60
+ const prefetchResult = comment +
61
+ printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile);
56
62
  return [
57
63
  {
58
- name: "index.ts",
64
+ name: `${OpenApiRqFiles.index}.ts`,
59
65
  content: indexResult,
60
66
  },
61
67
  {
62
- name: "common.ts",
68
+ name: `${OpenApiRqFiles.common}.ts`,
63
69
  content: commonResult,
64
70
  },
65
71
  {
66
- name: "queries.ts",
72
+ name: `${OpenApiRqFiles.queries}.ts`,
67
73
  content: mainResult,
68
74
  },
69
75
  {
70
- name: "suspense.ts",
76
+ name: `${OpenApiRqFiles.suspense}.ts`,
71
77
  content: suspenseResult,
72
78
  },
79
+ {
80
+ name: `${OpenApiRqFiles.prefetch}.ts`,
81
+ content: prefetchResult,
82
+ },
73
83
  ];
74
84
  };
@@ -73,11 +73,11 @@ export function createQueryKeyExport({ className, methodName, queryKey, }) {
73
73
  ts.factory.createVariableDeclaration(ts.factory.createIdentifier(queryKey), undefined, undefined, ts.factory.createStringLiteral(`${className}${capitalizeFirstLetter(methodName)}`)),
74
74
  ], ts.NodeFlags.Const));
75
75
  }
76
- function hookNameFromMethod({ method, className, }) {
76
+ export function hookNameFromMethod({ method, className, }) {
77
77
  const methodName = getNameFromMethod(method);
78
78
  return `use${className}${capitalizeFirstLetter(methodName)}`;
79
79
  }
80
- function createQueryKeyFromMethod({ method, className, }) {
80
+ export function createQueryKeyFromMethod({ method, className, }) {
81
81
  const customHookName = hookNameFromMethod({ method, className });
82
82
  const queryKey = `${customHookName}Key`;
83
83
  return queryKey;
@@ -87,7 +87,7 @@ function createQueryKeyFromMethod({ method, className, }) {
87
87
  * @param queryString The type of query to use from react-query
88
88
  * @param suffix The suffix to append to the hook name
89
89
  */
90
- function createQueryHook({ queryString, suffix, responseDataType, requestParams, method, className, }) {
90
+ export function createQueryHook({ queryString, suffix, responseDataType, requestParams, method, className, }) {
91
91
  const methodName = getNameFromMethod(method);
92
92
  const customHookName = hookNameFromMethod({ method, className });
93
93
  const queryKey = createQueryKeyFromMethod({ method, className });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nohe/openapi-react-query-codegen",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "OpenAPI React Query Codegen",
5
5
  "bin": {
6
6
  "openapi-rq": "dist/cli.mjs"