@7nohe/openapi-react-query-codegen 0.0.0-13d458778df8db161111e788ed4207a13a110724
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 +300 -0
- package/dist/cli.mjs +41 -0
- package/dist/common.mjs +151 -0
- package/dist/constants.mjs +12 -0
- package/dist/createExports.mjs +66 -0
- package/dist/createImports.mjs +43 -0
- package/dist/createPrefetch.mjs +48 -0
- package/dist/createSource.mjs +83 -0
- package/dist/createUseMutation.mjs +76 -0
- package/dist/createUseQuery.mjs +201 -0
- package/dist/format.mjs +67 -0
- package/dist/generate.mjs +50 -0
- package/dist/print.mjs +22 -0
- package/dist/service.mjs +79 -0
- package/dist/util.mjs +16 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# OpenAPI React Query Codegen
|
|
2
|
+
|
|
3
|
+
> Node.js library that generates [React Query (also called TanStack Query)](https://tanstack.com/query) hooks based on an OpenAPI specification file.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
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)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
$ npm install -D @7nohe/openapi-react-query-codegen
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Register the command to the `scripts` property in your package.json file.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"scripts": {
|
|
24
|
+
"codegen": "openapi-rq -i ./petstore.yaml -c @hey-api/client-fetch"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can also run the command without installing it in your project using the npx command.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ npx --package @7nohe/openapi-react-query-codegen openapi-rq -i ./petstore.yaml -c @hey-api/client-fetch
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
$ openapi-rq --help
|
|
39
|
+
|
|
40
|
+
Usage: openapi-rq [options]
|
|
41
|
+
|
|
42
|
+
Generate React Query code based on OpenAPI
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
-V, --version output the version number
|
|
46
|
+
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
|
|
47
|
+
-o, --output <value> Output directory (default: "openapi")
|
|
48
|
+
-c, --client <value> HTTP client to generate [@hey-api/client-fetch, @hey-api/client-axios] (default: "@hey-api/client-fetch")
|
|
49
|
+
--request <value> Path to custom request file
|
|
50
|
+
--format <value> Process output folder with formatter? ['biome', 'prettier']
|
|
51
|
+
--lint <value> Process output folder with linter? ['eslint', 'biome']
|
|
52
|
+
--operationId Use operation ID to generate operation names?
|
|
53
|
+
--serviceResponse <value> Define shape of returned value from service calls ['body', 'response'] (default: "body")
|
|
54
|
+
--base <value> Manually set base in OpenAPI config instead of inferring from server value
|
|
55
|
+
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
|
|
56
|
+
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
|
|
57
|
+
--debug Enable debug mode
|
|
58
|
+
--noSchemas Disable generating schemas for request and response objects
|
|
59
|
+
--schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
|
|
60
|
+
-h, --help display help for command
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Example Usage
|
|
64
|
+
|
|
65
|
+
#### Command
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
$ openapi-rq -i ./petstore.yaml
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Output directory structure
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
- openapi
|
|
75
|
+
- queries
|
|
76
|
+
- index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
|
|
77
|
+
- common.ts <- common types
|
|
78
|
+
- queries.ts <- generated query hooks
|
|
79
|
+
- suspenses.ts <- generated suspense hooks
|
|
80
|
+
- prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below
|
|
81
|
+
- requests <- output code generated by @hey-api/openapi-ts
|
|
82
|
+
```
|
|
83
|
+
|
|
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
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
// App.tsx
|
|
92
|
+
import { usePetServiceFindPetsByStatus } from "../openapi/queries";
|
|
93
|
+
function App() {
|
|
94
|
+
const { data } = usePetServiceFindPetsByStatus({ status: ["available"] });
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="App">
|
|
98
|
+
<h1>Pet List</h1>
|
|
99
|
+
<ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default App;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
##### Using the generated typescript client
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useQuery } from "@tanstack/react-query";
|
|
111
|
+
import { PetService } from "../openapi/requests/services";
|
|
112
|
+
import { usePetServiceFindPetsByStatusKey } from "../openapi/queries";
|
|
113
|
+
|
|
114
|
+
function App() {
|
|
115
|
+
// You can still use the auto-generated query key
|
|
116
|
+
const { data } = useQuery({
|
|
117
|
+
queryKey: [usePetServiceFindPetsByStatusKey],
|
|
118
|
+
queryFn: () => {
|
|
119
|
+
// Do something here
|
|
120
|
+
return PetService.findPetsByStatus(["available"]);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return <div className="App">{/* .... */}</div>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default App;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
##### Using Suspense Hooks
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// App.tsx
|
|
134
|
+
import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
|
|
135
|
+
function ChildComponent() {
|
|
136
|
+
const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
|
|
137
|
+
|
|
138
|
+
return <ul>{data?.map((pet, index) => <li key={pet.id}>{pet.name}</li>)}</ul>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ParentComponent() {
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<Suspense fallback={<>loading...</>}>
|
|
145
|
+
<ChildComponent />
|
|
146
|
+
</Suspense>
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function App() {
|
|
152
|
+
return (
|
|
153
|
+
<div className="App">
|
|
154
|
+
<h1>Pet List</h1>
|
|
155
|
+
<ParentComponent />
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default App;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
##### Using Mutation hooks
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
// App.tsx
|
|
167
|
+
import { usePetServiceAddPet } from "../openapi/queries";
|
|
168
|
+
|
|
169
|
+
function App() {
|
|
170
|
+
const { mutate } = usePetServiceAddPet();
|
|
171
|
+
|
|
172
|
+
const handleAddPet = () => {
|
|
173
|
+
mutate({ name: "Fluffy", status: "available" });
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="App">
|
|
178
|
+
<h1>Add Pet</h1>
|
|
179
|
+
<button onClick={handleAddPet}>Add Pet</button>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default App;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
##### Invalidating queries after mutation
|
|
188
|
+
|
|
189
|
+
Invalidating queries after a mutation is important to ensure the cache is updated with the new data. This is done by calling the `queryClient.invalidateQueries` function with the query key used by the query hook.
|
|
190
|
+
|
|
191
|
+
Learn more about invalidating queries [here](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation).
|
|
192
|
+
|
|
193
|
+
To ensure the query key is created the same way as the query hook, you can use the query key function exported by the generated query hooks.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import {
|
|
197
|
+
usePetServiceFindPetsByStatus,
|
|
198
|
+
usePetServiceAddPet,
|
|
199
|
+
UsePetServiceFindPetsByStatusKeyFn,
|
|
200
|
+
} from "../openapi/queries";
|
|
201
|
+
|
|
202
|
+
// App.tsx
|
|
203
|
+
function App() {
|
|
204
|
+
const [status, setStatus] = React.useState(["available"]);
|
|
205
|
+
const { data } = usePetServiceFindPetsByStatus({ status });
|
|
206
|
+
const { mutate } = usePetServiceAddPet({
|
|
207
|
+
onSuccess: () => {
|
|
208
|
+
queryClient.invalidateQueries({
|
|
209
|
+
// Call the query key function to get the query key
|
|
210
|
+
// This is important to ensure the query key is created the same way as the query hook
|
|
211
|
+
// This insures the cache is invalidated correctly and is typed correctly
|
|
212
|
+
queryKey: [UsePetServiceFindPetsByStatusKeyFn({
|
|
213
|
+
status
|
|
214
|
+
})],
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className="App">
|
|
221
|
+
<h1>Pet List</h1>
|
|
222
|
+
<ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => {
|
|
225
|
+
mutate({ name: "Fluffy", status: "available" });
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
Add Pet
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default App;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
##### Runtime Configuration
|
|
238
|
+
|
|
239
|
+
You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
|
|
240
|
+
|
|
241
|
+
It's default location is `openapi/requests/core/OpenAPI.ts` and it is also exported from `openapi/index.ts`
|
|
242
|
+
|
|
243
|
+
Import the constant into your runtime and modify it before setting up the react app.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
/** main.tsx */
|
|
247
|
+
import { OpenAPI as OpenAPIConfig } from './openapi/requests/core/OpenAPI';
|
|
248
|
+
...
|
|
249
|
+
OpenAPIConfig.BASE = 'www.domain.com/api';
|
|
250
|
+
OpenAPIConfig.HEADERS = {
|
|
251
|
+
'x-header-1': 'value-1',
|
|
252
|
+
'x-header-2': 'value-2',
|
|
253
|
+
};
|
|
254
|
+
...
|
|
255
|
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
256
|
+
<React.StrictMode>
|
|
257
|
+
<QueryClientProvider client={queryClient}>
|
|
258
|
+
<App />
|
|
259
|
+
</QueryClientProvider>
|
|
260
|
+
</React.StrictMode>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Development
|
|
266
|
+
|
|
267
|
+
### Install dependencies
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
pnpm install
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Run tests
|
|
274
|
+
```bash
|
|
275
|
+
pnpm test
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Run linter
|
|
279
|
+
```bash
|
|
280
|
+
pnpm lint
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Run linter and fix
|
|
284
|
+
```bash
|
|
285
|
+
pnpm lint:fix
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Update snapshots
|
|
289
|
+
```bash
|
|
290
|
+
pnpm snapshot
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Build example and validate generated code
|
|
294
|
+
```bash
|
|
295
|
+
npm run build && pnpm --filter @7nohe/react-app generate:api && pnpm --filter @7nohe/react-app test:generated
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import { defaultOutputPath } from "./constants.mjs";
|
|
7
|
+
import { generate } from "./generate.mjs";
|
|
8
|
+
const program = new Command();
|
|
9
|
+
async function setupProgram() {
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const file = await readFile(join(__dirname, "../package.json"), "utf-8");
|
|
13
|
+
const packageJson = JSON.parse(file);
|
|
14
|
+
const version = packageJson.version;
|
|
15
|
+
program
|
|
16
|
+
.name("openapi-rq")
|
|
17
|
+
.version(version)
|
|
18
|
+
.description("Generate React Query code based on OpenAPI")
|
|
19
|
+
.requiredOption("-i, --input <value>", "OpenAPI specification, can be a path, url or string content (required)")
|
|
20
|
+
.option("-o, --output <value>", "Output directory", defaultOutputPath)
|
|
21
|
+
.addOption(new Option("-c, --client <value>", "HTTP client to generate")
|
|
22
|
+
.choices(["@hey-api/client-fetch", "@hey-api/client-axios"])
|
|
23
|
+
.default("@hey-api/client-fetch"))
|
|
24
|
+
.option("--request <value>", "Path to custom request file")
|
|
25
|
+
.addOption(new Option("--format <value>", "Process output folder with formatter?").choices(["biome", "prettier"]))
|
|
26
|
+
.addOption(new Option("--lint <value>", "Process output folder with linter?").choices(["biome", "eslint"]))
|
|
27
|
+
.option("--operationId", "Use operation ID to generate operation names?")
|
|
28
|
+
.addOption(new Option("--serviceResponse <value>", "Define shape of returned value from service calls")
|
|
29
|
+
.choices(["body", "response"])
|
|
30
|
+
.default("body"))
|
|
31
|
+
.option("--base <value>", "Manually set base in OpenAPI config instead of inferring from server value")
|
|
32
|
+
.addOption(new Option("--enums <value>", "Generate JavaScript objects from enum definitions?").choices(["javascript", "typescript"]))
|
|
33
|
+
.option("--useDateType", "Use Date type instead of string for date types for models, this will not convert the data to a Date object")
|
|
34
|
+
.option("--debug", "Run in debug mode?")
|
|
35
|
+
.option("--noSchemas", "Disable generating JSON schemas")
|
|
36
|
+
.addOption(new Option("--schemaType <value>", "Type of JSON schema [Default: 'json']").choices(["form", "json"]))
|
|
37
|
+
.parse();
|
|
38
|
+
const options = program.opts();
|
|
39
|
+
await generate(options, version);
|
|
40
|
+
}
|
|
41
|
+
setupProgram();
|
package/dist/common.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ArrowFunction } from "ts-morph";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { queriesOutputPath, requestsOutputPath } from "./constants.mjs";
|
|
6
|
+
export const TData = ts.factory.createIdentifier("TData");
|
|
7
|
+
export const TError = ts.factory.createIdentifier("TError");
|
|
8
|
+
export const TContext = ts.factory.createIdentifier("TContext");
|
|
9
|
+
export const EqualsOrGreaterThanToken = ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken);
|
|
10
|
+
export const QuestionToken = ts.factory.createToken(ts.SyntaxKind.QuestionToken);
|
|
11
|
+
export const queryKeyGenericType = ts.factory.createTypeReferenceNode("TQueryKey");
|
|
12
|
+
export const queryKeyConstraint = ts.factory.createTypeReferenceNode("Array", [
|
|
13
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
|
|
14
|
+
]);
|
|
15
|
+
export const capitalizeFirstLetter = (str) => {
|
|
16
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
17
|
+
};
|
|
18
|
+
export const lowercaseFirstLetter = (str) => {
|
|
19
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
20
|
+
};
|
|
21
|
+
export const getVariableArrowFunctionParameters = (variable) => {
|
|
22
|
+
const initializer = variable.getInitializer();
|
|
23
|
+
if (!initializer) {
|
|
24
|
+
throw new Error("Initializer not found");
|
|
25
|
+
}
|
|
26
|
+
if (!ArrowFunction.isArrowFunction(initializer)) {
|
|
27
|
+
throw new Error("Initializer is not an arrow function");
|
|
28
|
+
}
|
|
29
|
+
return initializer.getParameters();
|
|
30
|
+
};
|
|
31
|
+
export const getNameFromVariable = (variable) => {
|
|
32
|
+
const variableName = variable.getName();
|
|
33
|
+
if (!variableName) {
|
|
34
|
+
throw new Error("Variable name not found");
|
|
35
|
+
}
|
|
36
|
+
return variableName;
|
|
37
|
+
};
|
|
38
|
+
export async function exists(f) {
|
|
39
|
+
try {
|
|
40
|
+
await stat(f);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const Common = "Common";
|
|
48
|
+
/**
|
|
49
|
+
* Build a common type name by prepending the Common namespace.
|
|
50
|
+
*/
|
|
51
|
+
export function BuildCommonTypeName(name) {
|
|
52
|
+
if (typeof name === "string") {
|
|
53
|
+
return ts.factory.createIdentifier(`${Common}.${name}`);
|
|
54
|
+
}
|
|
55
|
+
return ts.factory.createIdentifier(`${Common}.${name.text}`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Safely parse a value into a number. Checks for NaN and Infinity.
|
|
59
|
+
* Returns NaN if the string is not a valid number.
|
|
60
|
+
* @param value The value to parse.
|
|
61
|
+
* @returns The parsed number or NaN if the value is not a valid number.
|
|
62
|
+
*/
|
|
63
|
+
export function safeParseNumber(value) {
|
|
64
|
+
const parsed = Number(value);
|
|
65
|
+
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
return Number.NaN;
|
|
69
|
+
}
|
|
70
|
+
export function extractPropertiesFromObjectParam(param) {
|
|
71
|
+
const referenced = param.findReferences()[0];
|
|
72
|
+
const def = referenced.getDefinition();
|
|
73
|
+
const paramNodes = def
|
|
74
|
+
.getNode()
|
|
75
|
+
.getType()
|
|
76
|
+
.getProperties()
|
|
77
|
+
.filter((prop) => prop.getValueDeclaration()?.getType())
|
|
78
|
+
.map((prop) => {
|
|
79
|
+
return {
|
|
80
|
+
name: prop.getName(),
|
|
81
|
+
optional: prop.isOptional(),
|
|
82
|
+
type: prop.getValueDeclaration()?.getType(),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return paramNodes;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Replace the import("...") surrounding the type if there is one.
|
|
89
|
+
* This can happen when the type is imported from another file, but
|
|
90
|
+
* we are already importing all the types from that file.
|
|
91
|
+
*
|
|
92
|
+
* https://regex101.com/r/3DyHaQ/1
|
|
93
|
+
*
|
|
94
|
+
* TODO: Replace with a more robust solution.
|
|
95
|
+
*/
|
|
96
|
+
export function getShortType(type) {
|
|
97
|
+
return type.replaceAll(/import\(".*"\)\./g, "");
|
|
98
|
+
}
|
|
99
|
+
export function getClassesFromService(node) {
|
|
100
|
+
const klasses = node.getClasses();
|
|
101
|
+
if (!klasses.length) {
|
|
102
|
+
throw new Error("No classes found");
|
|
103
|
+
}
|
|
104
|
+
return klasses.map((klass) => {
|
|
105
|
+
const className = klass.getName();
|
|
106
|
+
if (!className) {
|
|
107
|
+
throw new Error("Class name not found");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
className,
|
|
111
|
+
klass,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function getClassNameFromClassNode(klass) {
|
|
116
|
+
const className = klass.getName();
|
|
117
|
+
if (!className) {
|
|
118
|
+
throw new Error("Class name not found");
|
|
119
|
+
}
|
|
120
|
+
return className;
|
|
121
|
+
}
|
|
122
|
+
export function formatOptions(options) {
|
|
123
|
+
// loop through properties on the options object
|
|
124
|
+
// if the property is a string of number then convert it to a number
|
|
125
|
+
// if the property is a string of boolean then convert it to a boolean
|
|
126
|
+
const formattedOptions = Object.entries(options).reduce((acc, [key, value]) => {
|
|
127
|
+
const typedKey = key;
|
|
128
|
+
const typedValue = value;
|
|
129
|
+
const parsedNumber = safeParseNumber(typedValue);
|
|
130
|
+
if (value === "true" || value === true) {
|
|
131
|
+
acc[typedKey] = true;
|
|
132
|
+
}
|
|
133
|
+
else if (value === "false" || value === false) {
|
|
134
|
+
acc[typedKey] = false;
|
|
135
|
+
}
|
|
136
|
+
else if (!Number.isNaN(parsedNumber)) {
|
|
137
|
+
acc[typedKey] = parsedNumber;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
acc[typedKey] = typedValue;
|
|
141
|
+
}
|
|
142
|
+
return acc;
|
|
143
|
+
}, options);
|
|
144
|
+
return formattedOptions;
|
|
145
|
+
}
|
|
146
|
+
export function buildRequestsOutputPath(outputPath) {
|
|
147
|
+
return path.join(outputPath, requestsOutputPath);
|
|
148
|
+
}
|
|
149
|
+
export function buildQueriesOutputPath(outputPath) {
|
|
150
|
+
return path.join(outputPath, queriesOutputPath);
|
|
151
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const defaultOutputPath = "openapi";
|
|
2
|
+
export const queriesOutputPath = "queries";
|
|
3
|
+
export const requestsOutputPath = "requests";
|
|
4
|
+
export const serviceFileName = "services.gen";
|
|
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
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createPrefetch } from "./createPrefetch.mjs";
|
|
2
|
+
import { createUseMutation } from "./createUseMutation.mjs";
|
|
3
|
+
import { createUseQuery } from "./createUseQuery.mjs";
|
|
4
|
+
export const createExports = (service) => {
|
|
5
|
+
const { methods } = service;
|
|
6
|
+
// const methods = klasses.flatMap((k) => k.methods);
|
|
7
|
+
const allGet = methods.filter((m) => m.httpMethodName.toUpperCase().includes("GET"));
|
|
8
|
+
const allPost = methods.filter((m) => m.httpMethodName.toUpperCase().includes("POST"));
|
|
9
|
+
const allPut = methods.filter((m) => m.httpMethodName.toUpperCase().includes("PUT"));
|
|
10
|
+
const allPatch = methods.filter((m) => m.httpMethodName.toUpperCase().includes("PATCH"));
|
|
11
|
+
const allDelete = methods.filter((m) => m.httpMethodName.toUpperCase().includes("DELETE"));
|
|
12
|
+
const allGetQueries = allGet.map((m) => createUseQuery(m));
|
|
13
|
+
const allPrefetchQueries = allGet.map((m) => createPrefetch(m));
|
|
14
|
+
const allPostMutations = allPost.map((m) => createUseMutation(m));
|
|
15
|
+
const allPutMutations = allPut.map((m) => createUseMutation(m));
|
|
16
|
+
const allPatchMutations = allPatch.map((m) => createUseMutation(m));
|
|
17
|
+
const allDeleteMutations = allDelete.map((m) => createUseMutation(m));
|
|
18
|
+
const allQueries = [...allGetQueries];
|
|
19
|
+
const allMutations = [
|
|
20
|
+
...allPostMutations,
|
|
21
|
+
...allPutMutations,
|
|
22
|
+
...allPatchMutations,
|
|
23
|
+
...allDeleteMutations,
|
|
24
|
+
];
|
|
25
|
+
const commonInQueries = allQueries.flatMap(({ apiResponse, returnType, key, queryKeyFn }) => [
|
|
26
|
+
apiResponse,
|
|
27
|
+
returnType,
|
|
28
|
+
key,
|
|
29
|
+
queryKeyFn,
|
|
30
|
+
]);
|
|
31
|
+
const commonInMutations = allMutations.flatMap(({ mutationResult }) => [
|
|
32
|
+
mutationResult,
|
|
33
|
+
]);
|
|
34
|
+
const allCommon = [...commonInQueries, ...commonInMutations];
|
|
35
|
+
const mainQueries = allQueries.flatMap(({ queryHook }) => [queryHook]);
|
|
36
|
+
const mainMutations = allMutations.flatMap(({ mutationHook }) => [
|
|
37
|
+
mutationHook,
|
|
38
|
+
]);
|
|
39
|
+
const mainExports = [...mainQueries, ...mainMutations];
|
|
40
|
+
const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [
|
|
41
|
+
suspenseQueryHook,
|
|
42
|
+
]);
|
|
43
|
+
const suspenseExports = [...suspenseQueries];
|
|
44
|
+
const allPrefetches = allPrefetchQueries.flatMap(({ prefetchHook }) => [
|
|
45
|
+
prefetchHook,
|
|
46
|
+
]);
|
|
47
|
+
const allPrefetchExports = [...allPrefetches];
|
|
48
|
+
return {
|
|
49
|
+
/**
|
|
50
|
+
* Common types and variables between queries (regular and suspense) and mutations
|
|
51
|
+
*/
|
|
52
|
+
allCommon,
|
|
53
|
+
/**
|
|
54
|
+
* Main exports are the hooks that are used in the components
|
|
55
|
+
*/
|
|
56
|
+
mainExports,
|
|
57
|
+
/**
|
|
58
|
+
* Suspense exports are the hooks that are used in the suspense components
|
|
59
|
+
*/
|
|
60
|
+
suspenseExports,
|
|
61
|
+
/**
|
|
62
|
+
* Prefetch exports are the hooks that are used in the prefetch components
|
|
63
|
+
*/
|
|
64
|
+
allPrefetchExports,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { modalsFileName, serviceFileName } from "./constants.mjs";
|
|
4
|
+
const { join } = posix;
|
|
5
|
+
export const createImports = ({ project, }) => {
|
|
6
|
+
const modelsFile = project
|
|
7
|
+
.getSourceFiles()
|
|
8
|
+
.find((sourceFile) => sourceFile.getFilePath().includes(modalsFileName));
|
|
9
|
+
const serviceFile = project.getSourceFileOrThrow(`${serviceFileName}.ts`);
|
|
10
|
+
if (!modelsFile) {
|
|
11
|
+
console.warn(`
|
|
12
|
+
⚠️ WARNING: No models file found.
|
|
13
|
+
This may be an error if \`.components.schemas\` or \`.components.parameters\` is defined in your OpenAPI input.`);
|
|
14
|
+
}
|
|
15
|
+
const modelNames = modelsFile
|
|
16
|
+
? Array.from(modelsFile.getExportedDeclarations().keys())
|
|
17
|
+
: [];
|
|
18
|
+
const serviceExports = Array.from(serviceFile.getExportedDeclarations().keys());
|
|
19
|
+
const serviceNames = serviceExports;
|
|
20
|
+
const imports = [
|
|
21
|
+
ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
|
|
22
|
+
ts.factory.createImportSpecifier(true, undefined, ts.factory.createIdentifier("QueryClient")),
|
|
23
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useQuery")),
|
|
24
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useSuspenseQuery")),
|
|
25
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useMutation")),
|
|
26
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseQueryResult")),
|
|
27
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseQueryOptions")),
|
|
28
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseMutationOptions")),
|
|
29
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseMutationResult")),
|
|
30
|
+
])), ts.factory.createStringLiteral("@tanstack/react-query"), undefined),
|
|
31
|
+
ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
|
|
32
|
+
// import all class names from service file
|
|
33
|
+
...serviceNames.map((serviceName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(serviceName))),
|
|
34
|
+
])), ts.factory.createStringLiteral(join("../requests", serviceFileName)), undefined),
|
|
35
|
+
];
|
|
36
|
+
if (modelsFile) {
|
|
37
|
+
// import all the models by name
|
|
38
|
+
imports.push(ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
|
|
39
|
+
...modelNames.map((modelName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(modelName))),
|
|
40
|
+
])), ts.factory.createStringLiteral(join("../requests/", modalsFileName)), undefined));
|
|
41
|
+
}
|
|
42
|
+
return imports;
|
|
43
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { BuildCommonTypeName, extractPropertiesFromObjectParam, getNameFromVariable, getVariableArrowFunctionParameters, } from "./common.mjs";
|
|
3
|
+
import { createQueryKeyFromMethod, getQueryKeyFnName, 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, }) {
|
|
9
|
+
const methodName = getNameFromVariable(method);
|
|
10
|
+
const queryName = hookNameFromMethod({ method });
|
|
11
|
+
const customHookName = `prefetch${queryName.charAt(0).toUpperCase() + queryName.slice(1)}`;
|
|
12
|
+
const queryKey = createQueryKeyFromMethod({ method });
|
|
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.createCallExpression(BuildCommonTypeName(getQueryKeyFnName(queryKey)), undefined, getVariableArrowFunctionParameters(method).length
|
|
23
|
+
? [
|
|
24
|
+
ts.factory.createObjectLiteralExpression(getVariableArrowFunctionParameters(method).flatMap((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))),
|
|
25
|
+
]
|
|
26
|
+
: [])),
|
|
27
|
+
ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryFn"), ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createIdentifier(methodName), undefined, getVariableArrowFunctionParameters(method).length
|
|
28
|
+
? [
|
|
29
|
+
ts.factory.createObjectLiteralExpression(getVariableArrowFunctionParameters(method).flatMap((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))),
|
|
30
|
+
]
|
|
31
|
+
: undefined))),
|
|
32
|
+
]),
|
|
33
|
+
]))),
|
|
34
|
+
], ts.NodeFlags.Const));
|
|
35
|
+
return hookExport;
|
|
36
|
+
}
|
|
37
|
+
export const createPrefetch = ({ method, jsDoc }) => {
|
|
38
|
+
const requestParam = getRequestParamFromMethod(method);
|
|
39
|
+
const requestParams = requestParam ? [requestParam] : [];
|
|
40
|
+
const prefetchHook = createPrefetchHook({
|
|
41
|
+
requestParams,
|
|
42
|
+
method,
|
|
43
|
+
});
|
|
44
|
+
const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc);
|
|
45
|
+
return {
|
|
46
|
+
prefetchHook: hookWithJsDoc,
|
|
47
|
+
};
|
|
48
|
+
};
|