cushin-monorepo 3.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/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.claude/settings.local.json +44 -0
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +482 -0
- package/biome.json +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1552 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +84 -0
- package/dist/config/index.js +69 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +43 -0
- package/dist/config/schema.js +14 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1666 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/client.d.ts +40 -0
- package/dist/runtime/client.js +260 -0
- package/dist/runtime/client.js.map +1 -0
- package/package.json +41 -0
- package/packages/api-codegen/CHANGELOG.md +86 -0
- package/packages/api-codegen/biome.json +34 -0
- package/packages/api-codegen/dist/cli.js +1038 -0
- package/packages/api-codegen/dist/cli.js.map +1 -0
- package/packages/api-codegen/dist/index.d.ts +103 -0
- package/packages/api-codegen/dist/index.js +1026 -0
- package/packages/api-codegen/dist/index.js.map +1 -0
- package/packages/api-codegen/node_modules/.bin/acorn +21 -0
- package/packages/api-codegen/node_modules/.bin/conventional-changelog +21 -0
- package/packages/api-codegen/node_modules/.bin/conventional-commits-parser +21 -0
- package/packages/api-codegen/node_modules/.bin/esbuild +21 -0
- package/packages/api-codegen/node_modules/.bin/eslint +21 -0
- package/packages/api-codegen/node_modules/.bin/jiti +21 -0
- package/packages/api-codegen/node_modules/.bin/next +21 -0
- package/packages/api-codegen/node_modules/.bin/tsc +21 -0
- package/packages/api-codegen/node_modules/.bin/tsserver +21 -0
- package/packages/api-codegen/node_modules/.bin/tsup +21 -0
- package/packages/api-codegen/node_modules/.bin/tsup-node +21 -0
- package/packages/api-codegen/node_modules/.bin/vitest +21 -0
- package/packages/api-codegen/package.json +88 -0
- package/packages/api-runtime/CHANGELOG.md +46 -0
- package/packages/api-runtime/README.md +95 -0
- package/packages/api-runtime/dist/chunk-3FFXWCVP.js +17 -0
- package/packages/api-runtime/dist/chunk-3FFXWCVP.js.map +1 -0
- package/packages/api-runtime/dist/chunk-EZ5P7OPH.js +267 -0
- package/packages/api-runtime/dist/chunk-EZ5P7OPH.js.map +1 -0
- package/packages/api-runtime/dist/client.d.ts +40 -0
- package/packages/api-runtime/dist/client.js +13 -0
- package/packages/api-runtime/dist/client.js.map +1 -0
- package/packages/api-runtime/dist/index.d.ts +3 -0
- package/packages/api-runtime/dist/index.js +21 -0
- package/packages/api-runtime/dist/index.js.map +1 -0
- package/packages/api-runtime/dist/schema.d.ts +45 -0
- package/packages/api-runtime/dist/schema.js +11 -0
- package/packages/api-runtime/dist/schema.js.map +1 -0
- package/packages/api-runtime/node_modules/.bin/esbuild +21 -0
- package/packages/api-runtime/node_modules/.bin/jiti +21 -0
- package/packages/api-runtime/node_modules/.bin/tsc +21 -0
- package/packages/api-runtime/node_modules/.bin/tsserver +21 -0
- package/packages/api-runtime/node_modules/.bin/tsup +21 -0
- package/packages/api-runtime/node_modules/.bin/tsup-node +21 -0
- package/packages/api-runtime/package.json +54 -0
- package/packages/cli/CHANGELOG.md +34 -0
- package/packages/cli/biome.json +34 -0
- package/packages/cli/dist/index.d.ts +27 -0
- package/packages/cli/dist/index.js +183 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/node_modules/.bin/esbuild +21 -0
- package/packages/cli/node_modules/.bin/jiti +21 -0
- package/packages/cli/node_modules/.bin/tsc +21 -0
- package/packages/cli/node_modules/.bin/tsserver +21 -0
- package/packages/cli/node_modules/.bin/tsup +21 -0
- package/packages/cli/node_modules/.bin/tsup-node +21 -0
- package/packages/cli/package.json +47 -0
- package/pnpm-workspace.yaml +2 -0
- package/test-config.js +9 -0
- package/test-content-type-handling.mjs +100 -0
- package/test-endpoints-config.mjs +144 -0
- package/test-formdata-content-type-protection.mjs +127 -0
- package/test-formdata-runtime.mjs +127 -0
- package/test-full-integration.mjs +90 -0
- package/test-headers-formdata.mjs +97 -0
- package/test-headers-runtime.mjs +106 -0
- package/test-headers.mjs +79 -0
- package/test-internal-calls.mjs +57 -0
- package/test-ky-formdata.mjs +81 -0
- package/test-output/actions.ts +134 -0
- package/test-output/client.ts +81 -0
- package/test-output/hooks.ts +182 -0
- package/test-output/index.ts +9 -0
- package/test-output/prefetchs.ts +25 -0
- package/test-output/queries.ts +78 -0
- package/test-output/query-keys.ts +16 -0
- package/test-output/query-options.ts +38 -0
- package/test-output/server-client.ts +32 -0
- package/test-output/types.ts +61 -0
- package/test-real-endpoints.mjs +132 -0
- package/test-runtime-params.mjs +160 -0
- package/test-simple-config.mjs +71 -0
- package/tsconfig.base.json +29 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1552 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
6
|
+
import path6 from 'path';
|
|
7
|
+
import { createJiti } from 'jiti';
|
|
8
|
+
import fs5 from 'fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
var explorer = cosmiconfig("api-codegen", {
|
|
12
|
+
searchPlaces: [
|
|
13
|
+
"api-codegen.config.js",
|
|
14
|
+
"api-codegen.config.mjs",
|
|
15
|
+
"api-codegen.config.ts",
|
|
16
|
+
"api-codegen.config.json",
|
|
17
|
+
".api-codegenrc",
|
|
18
|
+
".api-codegenrc.json",
|
|
19
|
+
".api-codegenrc.js"
|
|
20
|
+
]
|
|
21
|
+
});
|
|
22
|
+
async function loadConfig(configPath) {
|
|
23
|
+
try {
|
|
24
|
+
const result = configPath ? await explorer.load(configPath) : await explorer.search();
|
|
25
|
+
if (!result || !result.config) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const userConfig = result.config;
|
|
29
|
+
const rootDir = path6.dirname(result.filepath);
|
|
30
|
+
const endpointsPath = path6.resolve(rootDir, userConfig.endpoints);
|
|
31
|
+
const outputDir = path6.resolve(rootDir, userConfig.output);
|
|
32
|
+
const generateHooks = userConfig.generateHooks ?? true;
|
|
33
|
+
const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs";
|
|
34
|
+
const generateServerQueries = userConfig.generateServerQueries ?? userConfig.provider === "nextjs";
|
|
35
|
+
const generateClient = userConfig.generateClient ?? true;
|
|
36
|
+
const generatePrefetch = userConfig.generatePrefetch ?? true;
|
|
37
|
+
return {
|
|
38
|
+
...userConfig,
|
|
39
|
+
rootDir,
|
|
40
|
+
endpointsPath,
|
|
41
|
+
outputDir,
|
|
42
|
+
generateHooks,
|
|
43
|
+
generateServerActions,
|
|
44
|
+
generateServerQueries,
|
|
45
|
+
generateClient,
|
|
46
|
+
generatePrefetch
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Failed to load config: ${error instanceof Error ? error.message : String(error)}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function validateConfig(config) {
|
|
55
|
+
if (!config.endpoints) {
|
|
56
|
+
throw new Error('Config error: "endpoints" path is required');
|
|
57
|
+
}
|
|
58
|
+
if (!config.provider) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'Config error: "provider" must be specified (vite or nextjs)'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!["vite", "nextjs"].includes(config.provider)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'Config error: "provider" must be either "vite" or "nextjs"'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!config.output) {
|
|
69
|
+
throw new Error('Config error: "output" directory is required');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/generators/base.ts
|
|
74
|
+
var BaseGenerator = class {
|
|
75
|
+
constructor(context) {
|
|
76
|
+
this.context = context;
|
|
77
|
+
}
|
|
78
|
+
isQueryEndpoint(endpoint) {
|
|
79
|
+
return endpoint.method === "GET";
|
|
80
|
+
}
|
|
81
|
+
isMutationEndpoint(endpoint) {
|
|
82
|
+
return !this.isQueryEndpoint(endpoint);
|
|
83
|
+
}
|
|
84
|
+
capitalize(str) {
|
|
85
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
86
|
+
}
|
|
87
|
+
getQueryTags(endpoint) {
|
|
88
|
+
return endpoint.tags || [];
|
|
89
|
+
}
|
|
90
|
+
getInvalidationTags(endpoint) {
|
|
91
|
+
const tags = endpoint.tags || [];
|
|
92
|
+
return tags.filter((tag) => tag !== "query" && tag !== "mutation");
|
|
93
|
+
}
|
|
94
|
+
hasParams(endpoint) {
|
|
95
|
+
return !!endpoint.params;
|
|
96
|
+
}
|
|
97
|
+
hasQuery(endpoint) {
|
|
98
|
+
return !!endpoint.query;
|
|
99
|
+
}
|
|
100
|
+
hasBody(endpoint) {
|
|
101
|
+
return !!endpoint.body;
|
|
102
|
+
}
|
|
103
|
+
getEndpointSignature(name, endpoint) {
|
|
104
|
+
const hasParams = this.hasParams(endpoint);
|
|
105
|
+
const hasQuery = this.hasQuery(endpoint);
|
|
106
|
+
const hasBody = this.hasBody(endpoint);
|
|
107
|
+
return {
|
|
108
|
+
hasParams,
|
|
109
|
+
hasQuery,
|
|
110
|
+
hasBody,
|
|
111
|
+
paramType: hasParams ? `ExtractParams<APIEndpoints['${name}']>` : "never",
|
|
112
|
+
queryType: hasQuery ? `ExtractQuery<APIEndpoints['${name}']>` : "never",
|
|
113
|
+
bodyType: hasBody ? `ExtractBody<APIEndpoints['${name}']>` : "never",
|
|
114
|
+
responseType: `ExtractResponse<APIEndpoints['${name}']>`
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
generateMutationCall(name, hasParams, hasBody) {
|
|
118
|
+
if (hasParams && hasBody) {
|
|
119
|
+
return `return apiClient.${name}(input.params, input.body);`;
|
|
120
|
+
} else if (hasParams) {
|
|
121
|
+
return `return apiClient.${name}(input);`;
|
|
122
|
+
} else if (hasBody) {
|
|
123
|
+
return `return apiClient.${name}(input);`;
|
|
124
|
+
} else {
|
|
125
|
+
return `return apiClient.${name}();`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
inferNonNull(expr) {
|
|
129
|
+
return `z.infer<NonNullable<${expr}>>`;
|
|
130
|
+
}
|
|
131
|
+
toCamelCase(str) {
|
|
132
|
+
return str.toLowerCase().replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
|
|
133
|
+
}
|
|
134
|
+
getResourceFromEndpoint(_name, endpoint) {
|
|
135
|
+
const tag = endpoint.tags?.find((t) => t !== "query" && t !== "mutation");
|
|
136
|
+
if (tag) return this.toCamelCase(tag);
|
|
137
|
+
const match = endpoint.path.match(/^\/([^/]+)/);
|
|
138
|
+
return match ? this.toCamelCase(match[1]) : "general";
|
|
139
|
+
}
|
|
140
|
+
groupEndpointsByResource() {
|
|
141
|
+
const groups = {};
|
|
142
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
143
|
+
([name, endpoint]) => {
|
|
144
|
+
const res = this.getResourceFromEndpoint(name, endpoint);
|
|
145
|
+
if (!groups[res]) groups[res] = [];
|
|
146
|
+
groups[res].push({ name, endpoint });
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
return groups;
|
|
150
|
+
}
|
|
151
|
+
resourceHasQueryEndpoints(resource) {
|
|
152
|
+
return this.groupEndpointsByResource()[resource]?.some(
|
|
153
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
154
|
+
) ?? false;
|
|
155
|
+
}
|
|
156
|
+
getEndpointKeyName(name) {
|
|
157
|
+
return name.startsWith("get") ? name[3].toLowerCase() + name.slice(4) : name;
|
|
158
|
+
}
|
|
159
|
+
generateQueryKeyCall(resource, name, endpoint) {
|
|
160
|
+
const key = this.getEndpointKeyName(name);
|
|
161
|
+
const args = [];
|
|
162
|
+
if (endpoint.params) args.push("params");
|
|
163
|
+
if (endpoint.query) args.push("filters");
|
|
164
|
+
return args.length ? `queryKeys.${resource}.${key}(${args.join(", ")})` : `queryKeys.${resource}.${key}()`;
|
|
165
|
+
}
|
|
166
|
+
hasQueryOptions() {
|
|
167
|
+
return Object.values(this.context.apiConfig.endpoints).some(
|
|
168
|
+
(e) => e.method === "GET"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/generators/hooks.ts
|
|
174
|
+
var HooksGenerator = class extends BaseGenerator {
|
|
175
|
+
async generate() {
|
|
176
|
+
const content = this.generateContent();
|
|
177
|
+
const outputPath = path6.join(this.context.config.outputDir, "hooks.ts");
|
|
178
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
179
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
180
|
+
}
|
|
181
|
+
generateContent() {
|
|
182
|
+
const useClientDirective = this.context.config.options?.useClientDirective ?? true;
|
|
183
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
184
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
185
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
|
|
186
|
+
const content = `${useClientDirective ? "'use client';\n" : ""}
|
|
187
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
188
|
+
import { apiClient } from "./client";
|
|
189
|
+
import { queryKeys } from "./query-keys";
|
|
190
|
+
import { apiQueryOptions } from "./query-options";
|
|
191
|
+
import { z } from "zod";
|
|
192
|
+
import { apiConfig } from "${relativePath}";
|
|
193
|
+
|
|
194
|
+
${this.generateQueryHooks()}
|
|
195
|
+
${this.generateMutationHooks()}
|
|
196
|
+
`;
|
|
197
|
+
return content;
|
|
198
|
+
}
|
|
199
|
+
generateQueryHooks() {
|
|
200
|
+
const hooks = [];
|
|
201
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
202
|
+
([name, endpoint]) => {
|
|
203
|
+
if (endpoint.method === "GET")
|
|
204
|
+
hooks.push(this.generateQueryHook(name, endpoint));
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
return hooks.join("\n\n");
|
|
208
|
+
}
|
|
209
|
+
generateQueryHook(name, endpoint) {
|
|
210
|
+
const hookName = `use${this.capitalize(name)}`;
|
|
211
|
+
const resource = this.getResourceFromEndpoint(name, endpoint);
|
|
212
|
+
const optionName = this.getEndpointKeyName(name);
|
|
213
|
+
const inferParams = this.inferNonNull(
|
|
214
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
215
|
+
);
|
|
216
|
+
const inferQuery = this.inferNonNull(
|
|
217
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
218
|
+
);
|
|
219
|
+
const inferResponse = this.inferNonNull(
|
|
220
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
221
|
+
);
|
|
222
|
+
const params = [];
|
|
223
|
+
const optionParams = [];
|
|
224
|
+
const queryTags = this.getQueryTags(endpoint);
|
|
225
|
+
if (endpoint.params) {
|
|
226
|
+
params.push(`params: ${inferParams}`);
|
|
227
|
+
optionParams.push("params");
|
|
228
|
+
}
|
|
229
|
+
if (endpoint.query) {
|
|
230
|
+
params.push(`filters?: ${inferQuery}`);
|
|
231
|
+
optionParams.push("filters");
|
|
232
|
+
}
|
|
233
|
+
params.push(`options?: {
|
|
234
|
+
enabled?: boolean;
|
|
235
|
+
select?: <TData = ${inferResponse}>(data: ${inferResponse}) => TData;
|
|
236
|
+
}`);
|
|
237
|
+
return `/**
|
|
238
|
+
* ${endpoint.description || `Query hook for ${name}`}
|
|
239
|
+
* @tags ${queryTags.join(", ") || "none"}
|
|
240
|
+
*/
|
|
241
|
+
export function ${hookName}(${params.join(",\n ")}) {
|
|
242
|
+
return useQuery({
|
|
243
|
+
...apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}),
|
|
244
|
+
...options,
|
|
245
|
+
});
|
|
246
|
+
}`;
|
|
247
|
+
}
|
|
248
|
+
generateMutationHooks() {
|
|
249
|
+
const hooks = [];
|
|
250
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
251
|
+
([name, endpoint]) => {
|
|
252
|
+
if (endpoint.method !== "GET")
|
|
253
|
+
hooks.push(this.generateMutationHook(name, endpoint));
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
return hooks.join("\n\n");
|
|
257
|
+
}
|
|
258
|
+
generateMutationHook(name, endpoint) {
|
|
259
|
+
const hookName = `use${this.capitalize(name)}`;
|
|
260
|
+
const resource = this.getResourceFromEndpoint(name, endpoint);
|
|
261
|
+
const inferParams = this.inferNonNull(
|
|
262
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
263
|
+
);
|
|
264
|
+
const inferBody = this.inferNonNull(
|
|
265
|
+
`typeof apiConfig.endpoints.${name}.body`
|
|
266
|
+
);
|
|
267
|
+
const inferResponse = this.inferNonNull(
|
|
268
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
269
|
+
);
|
|
270
|
+
const resourceHasQueries = this.resourceHasQueryEndpoints(resource);
|
|
271
|
+
let inputType;
|
|
272
|
+
let fnBody;
|
|
273
|
+
if (endpoint.params && endpoint.body) {
|
|
274
|
+
inputType = `{ params: ${inferParams}; body: ${inferBody}; }`;
|
|
275
|
+
fnBody = `({ params, body }: ${inputType}) => apiClient.${name}(params, body)`;
|
|
276
|
+
} else if (endpoint.params) {
|
|
277
|
+
inputType = `${inferParams}`;
|
|
278
|
+
fnBody = `(params: ${inputType}) => apiClient.${name}(params)`;
|
|
279
|
+
} else if (endpoint.body) {
|
|
280
|
+
inputType = `${inferBody}`;
|
|
281
|
+
fnBody = `(body: ${inputType}) => apiClient.${name}(body)`;
|
|
282
|
+
} else {
|
|
283
|
+
inputType = "void";
|
|
284
|
+
fnBody = `() => apiClient.${name}()`;
|
|
285
|
+
}
|
|
286
|
+
const invalidate = resourceHasQueries ? `queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });` : "";
|
|
287
|
+
return `/**
|
|
288
|
+
* ${endpoint.description || `Mutation hook for ${name}`}
|
|
289
|
+
* @tags ${endpoint.tags?.join(", ") || "none"}
|
|
290
|
+
*/
|
|
291
|
+
export function ${hookName}(options?: {
|
|
292
|
+
onSuccess?: (data: ${inferResponse}, variables: ${inputType}, context: unknown) => void;
|
|
293
|
+
onError?: (error: Error, variables: ${inputType}, context: unknown) => void;
|
|
294
|
+
onSettled?: (data: ${inferResponse} | undefined, error: Error | null, variables: ${inputType}, context: unknown) => void;
|
|
295
|
+
onMutate?: (variables: ${inputType}) => Promise<unknown> | unknown;
|
|
296
|
+
}) {
|
|
297
|
+
${invalidate ? "const queryClient = useQueryClient();" : ""}
|
|
298
|
+
return useMutation({
|
|
299
|
+
mutationFn: ${fnBody},
|
|
300
|
+
onSuccess: (data, variables, context) => {
|
|
301
|
+
${invalidate}
|
|
302
|
+
options?.onSuccess?.(data, variables, context);
|
|
303
|
+
},
|
|
304
|
+
onError: options?.onError,
|
|
305
|
+
onSettled: options?.onSettled,
|
|
306
|
+
onMutate: options?.onMutate,
|
|
307
|
+
});
|
|
308
|
+
}`;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var ServerActionsGenerator = class extends BaseGenerator {
|
|
312
|
+
async generate() {
|
|
313
|
+
const content = this.generateContent();
|
|
314
|
+
const outputPath = path6.join(this.context.config.outputDir, "actions.ts");
|
|
315
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
316
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
317
|
+
}
|
|
318
|
+
generateContent() {
|
|
319
|
+
const imports = `'use server';
|
|
320
|
+
|
|
321
|
+
import { revalidateTag, revalidatePath } from 'next/cache';
|
|
322
|
+
import { serverClient } from './server-client';
|
|
323
|
+
import type {
|
|
324
|
+
APIEndpoints,
|
|
325
|
+
ExtractBody,
|
|
326
|
+
ExtractParams,
|
|
327
|
+
ExtractResponse
|
|
328
|
+
} from './types';
|
|
329
|
+
|
|
330
|
+
export type ActionResult<T> =
|
|
331
|
+
| { success: true; data: T }
|
|
332
|
+
| { success: false; error: string };
|
|
333
|
+
`;
|
|
334
|
+
const actions = [];
|
|
335
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
|
|
336
|
+
if (this.isMutationEndpoint(endpoint)) {
|
|
337
|
+
actions.push(this.generateServerAction(name, endpoint));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
return imports + "\n" + actions.join("\n\n");
|
|
341
|
+
}
|
|
342
|
+
generateServerAction(name, endpoint) {
|
|
343
|
+
const actionSuffix = this.context.config.options?.actionSuffix || "Action";
|
|
344
|
+
const actionName = `${name}${actionSuffix}`;
|
|
345
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
346
|
+
const invalidationTags = this.getInvalidationTags(endpoint);
|
|
347
|
+
let inputType = "";
|
|
348
|
+
let inputParam = "";
|
|
349
|
+
if (signature.hasParams && signature.hasBody) {
|
|
350
|
+
inputType = `input: { params: ${signature.paramType}; body: ${signature.bodyType} }`;
|
|
351
|
+
inputParam = "input";
|
|
352
|
+
} else if (signature.hasParams) {
|
|
353
|
+
inputType = `params: ${signature.paramType}`;
|
|
354
|
+
inputParam = "params";
|
|
355
|
+
} else if (signature.hasBody) {
|
|
356
|
+
inputType = `body: ${signature.bodyType}`;
|
|
357
|
+
inputParam = "body";
|
|
358
|
+
}
|
|
359
|
+
const revalidateStatements = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` revalidateTag('${tag}');`).join("\n") : " // No automatic revalidations";
|
|
360
|
+
return `/**
|
|
361
|
+
* ${endpoint.description || `Server action for ${name}`}
|
|
362
|
+
* @tags ${endpoint.tags?.join(", ") || "none"}
|
|
363
|
+
*/
|
|
364
|
+
export async function ${actionName}(
|
|
365
|
+
${inputType}
|
|
366
|
+
): Promise<ActionResult<${signature.responseType}>> {
|
|
367
|
+
try {
|
|
368
|
+
const result = await serverClient.${name}(${inputParam ? inputParam : ""});
|
|
369
|
+
|
|
370
|
+
// Revalidate related data
|
|
371
|
+
${revalidateStatements}
|
|
372
|
+
|
|
373
|
+
return { success: true, data: result };
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('[Server Action Error]:', error);
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}`;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var ServerQueriesGenerator = class extends BaseGenerator {
|
|
385
|
+
async generate() {
|
|
386
|
+
const content = this.generateContent();
|
|
387
|
+
const outputPath = path6.join(this.context.config.outputDir, "queries.ts");
|
|
388
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
389
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
390
|
+
}
|
|
391
|
+
generateContent() {
|
|
392
|
+
const imports = `import { cache } from 'react';
|
|
393
|
+
import { unstable_cache } from 'next/cache';
|
|
394
|
+
import { serverClient } from './server-client';
|
|
395
|
+
import type {
|
|
396
|
+
APIEndpoints,
|
|
397
|
+
ExtractParams,
|
|
398
|
+
ExtractQuery,
|
|
399
|
+
ExtractResponse
|
|
400
|
+
} from './types';
|
|
401
|
+
`;
|
|
402
|
+
const queries = [];
|
|
403
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
404
|
+
([name, endpoint]) => {
|
|
405
|
+
if (this.isQueryEndpoint(endpoint)) {
|
|
406
|
+
queries.push(this.generateServerQuery(name, endpoint));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
return imports + "\n" + queries.join("\n\n");
|
|
411
|
+
}
|
|
412
|
+
generateServerQuery(name, endpoint) {
|
|
413
|
+
const queryName = `${name}Query`;
|
|
414
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
415
|
+
const queryTags = this.getQueryTags(endpoint);
|
|
416
|
+
const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
|
|
417
|
+
const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
|
|
418
|
+
const paramsList = [paramDef, queryDef].filter(Boolean).join(",\n ");
|
|
419
|
+
const clientCallArgs = [];
|
|
420
|
+
if (signature.hasParams) clientCallArgs.push("params");
|
|
421
|
+
if (signature.hasQuery) clientCallArgs.push("query");
|
|
422
|
+
const cacheKeyParts = [`'${name}'`];
|
|
423
|
+
if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
|
|
424
|
+
if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
|
|
425
|
+
return `/**
|
|
426
|
+
* ${endpoint.description || `Server query for ${name}`}
|
|
427
|
+
* @tags ${queryTags.join(", ") || "none"}
|
|
428
|
+
*/
|
|
429
|
+
export const ${queryName} = cache(async (
|
|
430
|
+
${paramsList}
|
|
431
|
+
): Promise<${signature.responseType}> => {
|
|
432
|
+
return unstable_cache(
|
|
433
|
+
async () => serverClient.${name}(${clientCallArgs.join(", ")}),
|
|
434
|
+
[${cacheKeyParts.join(", ")}],
|
|
435
|
+
{
|
|
436
|
+
tags: [${queryTags.map((tag) => `'${tag}'`).join(", ")}],
|
|
437
|
+
revalidate: 3600, // 1 hour default, can be overridden
|
|
438
|
+
}
|
|
439
|
+
)();
|
|
440
|
+
});`;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var TypesGenerator = class extends BaseGenerator {
|
|
444
|
+
async generate() {
|
|
445
|
+
const content = this.generateContent();
|
|
446
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
447
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
448
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
449
|
+
}
|
|
450
|
+
generateContent() {
|
|
451
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
452
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
453
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
|
|
454
|
+
return `// Auto-generated type definitions
|
|
455
|
+
// Do not edit this file manually
|
|
456
|
+
|
|
457
|
+
import type { z } from 'zod';
|
|
458
|
+
import { apiConfig } from '${relativePath}';
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
// Re-export endpoint configuration types
|
|
462
|
+
export type { APIConfig, APIEndpoint, HTTPMethod } from './schema';
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Type helper to extract params schema from an endpoint
|
|
466
|
+
*/
|
|
467
|
+
export type ExtractParams<T> = T extends { params: infer P extends z.ZodType }
|
|
468
|
+
? z.infer<P>
|
|
469
|
+
: never;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Type helper to extract query schema from an endpoint
|
|
473
|
+
*/
|
|
474
|
+
export type ExtractQuery<T> = T extends { query: infer Q extends z.ZodType }
|
|
475
|
+
? z.infer<Q>
|
|
476
|
+
: never;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Type helper to extract body schema from an endpoint
|
|
480
|
+
*/
|
|
481
|
+
export type ExtractBody<T> = T extends { body: infer B extends z.ZodType }
|
|
482
|
+
? z.infer<B>
|
|
483
|
+
: never;
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Type helper to extract response schema from an endpoint
|
|
487
|
+
*/
|
|
488
|
+
export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType }
|
|
489
|
+
? z.infer<R>
|
|
490
|
+
: never;
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Import your API config to get typed endpoints
|
|
494
|
+
*
|
|
495
|
+
*/
|
|
496
|
+
export type APIEndpoints = typeof apiConfig.endpoints;
|
|
497
|
+
|
|
498
|
+
${this.generateEndpointTypes()}
|
|
499
|
+
`;
|
|
500
|
+
}
|
|
501
|
+
generateEndpointTypes() {
|
|
502
|
+
const types = [];
|
|
503
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
504
|
+
([name, endpoint]) => {
|
|
505
|
+
const cap = this.capitalize(name);
|
|
506
|
+
if (endpoint.response)
|
|
507
|
+
types.push(
|
|
508
|
+
`export type ${cap}Response = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)};`
|
|
509
|
+
);
|
|
510
|
+
if (endpoint.body)
|
|
511
|
+
types.push(
|
|
512
|
+
`export type ${cap}Input = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`)};`
|
|
513
|
+
);
|
|
514
|
+
if (endpoint.query)
|
|
515
|
+
types.push(
|
|
516
|
+
`export type ${cap}Query = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`)};`
|
|
517
|
+
);
|
|
518
|
+
if (endpoint.params)
|
|
519
|
+
types.push(
|
|
520
|
+
`export type ${cap}Params = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`)};`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
return types.join("\n");
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
var ClientGenerator = class extends BaseGenerator {
|
|
528
|
+
async generate() {
|
|
529
|
+
await this.generateClientFile();
|
|
530
|
+
if (this.context.config.provider === "nextjs") {
|
|
531
|
+
await this.generateServerClientFile();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async generateClientFile() {
|
|
535
|
+
const content = this.generateClientContent();
|
|
536
|
+
const outputPath = path6.join(this.context.config.outputDir, "client.ts");
|
|
537
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
538
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
539
|
+
}
|
|
540
|
+
async generateServerClientFile() {
|
|
541
|
+
const content = this.generateServerClientContent();
|
|
542
|
+
const outputPath = path6.join(
|
|
543
|
+
this.context.config.outputDir,
|
|
544
|
+
"server-client.ts"
|
|
545
|
+
);
|
|
546
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
547
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
548
|
+
}
|
|
549
|
+
generateClientContent() {
|
|
550
|
+
const useClientDirective = this.context.config.options?.useClientDirective ?? true;
|
|
551
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
552
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
553
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
|
|
554
|
+
return `${useClientDirective ? "'use client';\n" : ""}
|
|
555
|
+
import { createAPIClient } from './core';
|
|
556
|
+
import type { AuthCallbacks } from './core';
|
|
557
|
+
import { apiConfig } from '${relativePath}';
|
|
558
|
+
import { z } from 'zod';
|
|
559
|
+
|
|
560
|
+
// Type the methods based on endpoints
|
|
561
|
+
type APIClientMethods = {
|
|
562
|
+
[K in keyof typeof apiConfig.endpoints]: (typeof apiConfig.endpoints)[K] extends {
|
|
563
|
+
method: infer M;
|
|
564
|
+
params?: infer P;
|
|
565
|
+
query?: infer Q;
|
|
566
|
+
body?: infer B;
|
|
567
|
+
response: infer R;
|
|
568
|
+
}
|
|
569
|
+
? M extends "GET"
|
|
570
|
+
? P extends z.ZodJSONSchema
|
|
571
|
+
? Q extends z.ZodJSONSchema
|
|
572
|
+
? (params: z.infer<P>, query?: z.infer<Q>) => Promise<z.infer<R>>
|
|
573
|
+
: (params: z.infer<P>) => Promise<z.infer<R>>
|
|
574
|
+
: Q extends z.ZodJSONSchema
|
|
575
|
+
? (query?: z.infer<Q>) => Promise<z.infer<R>>
|
|
576
|
+
: () => Promise<z.infer<R>>
|
|
577
|
+
: P extends z.ZodJSONSchema
|
|
578
|
+
? B extends z.ZodJSONSchema
|
|
579
|
+
? (params: z.infer<P>, body: z.infer<B>) => Promise<z.infer<R>>
|
|
580
|
+
: (params: z.infer<P>) => Promise<z.infer<R>>
|
|
581
|
+
: B extends z.ZodJSONSchema
|
|
582
|
+
? (body: z.infer<B>) => Promise<z.infer<R>>
|
|
583
|
+
: () => Promise<z.infer<R>>
|
|
584
|
+
: never;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
// Export singleton instance (will be initialized later)
|
|
589
|
+
export let baseClient: APIClientMethods & {
|
|
590
|
+
refreshAuth: () => Promise<void>;
|
|
591
|
+
updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
export const apiClient = {
|
|
595
|
+
${this.generateApiClientMethods()}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Initialize API client with auth callbacks
|
|
600
|
+
* Call this function in your auth provider setup
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* const authCallbacks = {
|
|
604
|
+
* getTokens: () => getStoredTokens(),
|
|
605
|
+
* onAuthError: () => router.push('/login'),
|
|
606
|
+
* onRefreshToken: async () => {
|
|
607
|
+
* await refreshAccessToken();
|
|
608
|
+
* },
|
|
609
|
+
* };
|
|
610
|
+
*
|
|
611
|
+
* initializeAPIClient(authCallbacks);
|
|
612
|
+
*/
|
|
613
|
+
export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
|
|
614
|
+
baseClient = createAPIClient(apiConfig, authCallbacks) as any;
|
|
615
|
+
return baseClient;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// Export for custom usage
|
|
619
|
+
export { createAPIClient };
|
|
620
|
+
export type { AuthCallbacks };
|
|
621
|
+
`;
|
|
622
|
+
}
|
|
623
|
+
generateServerClientContent() {
|
|
624
|
+
return `import { createAPIClient } from './core';
|
|
625
|
+
import { apiConfig } from '../config/endpoints';
|
|
626
|
+
import type { APIEndpoints } from './types';
|
|
627
|
+
|
|
628
|
+
// Type-safe API client methods for server-side
|
|
629
|
+
type APIClientMethods = {
|
|
630
|
+
[K in keyof APIEndpoints]: APIEndpoints[K] extends {
|
|
631
|
+
method: infer M;
|
|
632
|
+
params?: infer P;
|
|
633
|
+
query?: infer Q;
|
|
634
|
+
body?: infer B;
|
|
635
|
+
response: infer R;
|
|
636
|
+
}
|
|
637
|
+
? M extends 'GET'
|
|
638
|
+
? P extends { _type: any }
|
|
639
|
+
? Q extends { _type: any }
|
|
640
|
+
? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
|
|
641
|
+
: (params: P['_type']) => Promise<R['_type']>
|
|
642
|
+
: Q extends { _type: any }
|
|
643
|
+
? (query?: Q['_type']) => Promise<R['_type']>
|
|
644
|
+
: () => Promise<R['_type']>
|
|
645
|
+
: P extends { _type: any }
|
|
646
|
+
? B extends { _type: any }
|
|
647
|
+
? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
|
|
648
|
+
: (params: P['_type']) => Promise<R['_type']>
|
|
649
|
+
: B extends { _type: any }
|
|
650
|
+
? (body: B['_type']) => Promise<R['_type']>
|
|
651
|
+
: () => Promise<R['_type']>
|
|
652
|
+
: never;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Server-side API client (no auth, direct API calls)
|
|
657
|
+
* Use this in Server Components, Server Actions, and Route Handlers
|
|
658
|
+
*/
|
|
659
|
+
export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
generateApiClientMethods() {
|
|
663
|
+
const methods = [];
|
|
664
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
665
|
+
([name, endpoint]) => {
|
|
666
|
+
const inferParams = this.inferNonNull(
|
|
667
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
668
|
+
);
|
|
669
|
+
const inferQuery = this.inferNonNull(
|
|
670
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
671
|
+
);
|
|
672
|
+
const inferBody = this.inferNonNull(
|
|
673
|
+
`typeof apiConfig.endpoints.${name}.body`
|
|
674
|
+
);
|
|
675
|
+
const inferResponse = this.inferNonNull(
|
|
676
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
677
|
+
);
|
|
678
|
+
if (endpoint.method === "GET") {
|
|
679
|
+
if (endpoint.params && endpoint.query) {
|
|
680
|
+
methods.push(` ${name}: (params: ${inferParams}, query?: ${inferQuery}): Promise<${inferResponse}> =>
|
|
681
|
+
(baseClient as any).${name}(params, query),`);
|
|
682
|
+
} else if (endpoint.params) {
|
|
683
|
+
methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
|
|
684
|
+
(baseClient as any).${name}(params),`);
|
|
685
|
+
} else if (endpoint.query) {
|
|
686
|
+
methods.push(` ${name}: (query?: ${inferQuery}): Promise<${inferResponse}> =>
|
|
687
|
+
(baseClient as any).${name}(query),`);
|
|
688
|
+
} else {
|
|
689
|
+
methods.push(` ${name}: (): Promise<${inferResponse}> =>
|
|
690
|
+
(baseClient as any).${name}(),`);
|
|
691
|
+
}
|
|
692
|
+
} else {
|
|
693
|
+
if (endpoint.params && endpoint.body) {
|
|
694
|
+
methods.push(` ${name}: (params: ${inferParams}, body: ${inferBody}): Promise<${inferResponse}> =>
|
|
695
|
+
(baseClient as any).${name}(params, body),`);
|
|
696
|
+
} else if (endpoint.params) {
|
|
697
|
+
methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
|
|
698
|
+
(baseClient as any).${name}(params),`);
|
|
699
|
+
} else if (endpoint.body) {
|
|
700
|
+
methods.push(` ${name}: (body: ${inferBody}): Promise<${inferResponse}> =>
|
|
701
|
+
(baseClient as any).${name}(body),`);
|
|
702
|
+
} else {
|
|
703
|
+
methods.push(` ${name}: (): Promise<${inferResponse}> =>
|
|
704
|
+
(baseClient as any).${name}(),`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
);
|
|
709
|
+
return methods.join("\n");
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var QueryKeysGenerator = class extends BaseGenerator {
|
|
713
|
+
async generate() {
|
|
714
|
+
const content = this.generateContent();
|
|
715
|
+
const outputPath = path6.join(
|
|
716
|
+
this.context.config.outputDir,
|
|
717
|
+
"query-keys.ts"
|
|
718
|
+
);
|
|
719
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
720
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
721
|
+
}
|
|
722
|
+
generateContent() {
|
|
723
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
724
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
725
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
|
|
726
|
+
const content = `// Auto-generated query keys
|
|
727
|
+
import { z } from 'zod';
|
|
728
|
+
import { apiConfig } from '${relativePath}';
|
|
729
|
+
|
|
730
|
+
export const queryKeys = {
|
|
731
|
+
${this.generateQueryKeysContent()}
|
|
732
|
+
} as const;
|
|
733
|
+
`;
|
|
734
|
+
return content;
|
|
735
|
+
}
|
|
736
|
+
generateQueryKeysContent() {
|
|
737
|
+
const resourceGroups = this.groupEndpointsByResource();
|
|
738
|
+
const keys = [];
|
|
739
|
+
Object.entries(resourceGroups).forEach(([resource, endpoints]) => {
|
|
740
|
+
const queryEndpoints = endpoints.filter(
|
|
741
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
742
|
+
);
|
|
743
|
+
if (queryEndpoints.length === 0) return;
|
|
744
|
+
const resourceKeys = [` all: ['${resource}'] as const,`];
|
|
745
|
+
const added = /* @__PURE__ */ new Set();
|
|
746
|
+
queryEndpoints.forEach(({ name, endpoint }) => {
|
|
747
|
+
const keyName = this.getEndpointKeyName(name);
|
|
748
|
+
if (added.has(keyName)) return;
|
|
749
|
+
const inferParams = this.inferNonNull(
|
|
750
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
751
|
+
);
|
|
752
|
+
const inferQuery = this.inferNonNull(
|
|
753
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
754
|
+
);
|
|
755
|
+
if (endpoint.params || endpoint.query) {
|
|
756
|
+
const params = [];
|
|
757
|
+
if (endpoint.params) params.push(`params?: ${inferParams}`);
|
|
758
|
+
if (endpoint.query) params.push(`query?: ${inferQuery}`);
|
|
759
|
+
resourceKeys.push(` ${keyName}: (${params.join(", ")}) =>
|
|
760
|
+
['${resource}', '${keyName}', ${endpoint.params ? "params" : "undefined"}, ${endpoint.query ? "query" : "undefined"}] as const,`);
|
|
761
|
+
} else {
|
|
762
|
+
resourceKeys.push(
|
|
763
|
+
` ${keyName}: () => ['${resource}', '${keyName}'] as const,`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
added.add(keyName);
|
|
767
|
+
});
|
|
768
|
+
keys.push(` ${resource}: {
|
|
769
|
+
${resourceKeys.join("\n")}
|
|
770
|
+
},`);
|
|
771
|
+
});
|
|
772
|
+
return keys.join("\n");
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
var QueryOptionsGenerator = class extends BaseGenerator {
|
|
776
|
+
async generate() {
|
|
777
|
+
const content = this.generateContent();
|
|
778
|
+
const outputPath = path6.join(
|
|
779
|
+
this.context.config.outputDir,
|
|
780
|
+
"query-options.ts"
|
|
781
|
+
);
|
|
782
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
783
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
784
|
+
}
|
|
785
|
+
generateContent() {
|
|
786
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
787
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
788
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
|
|
789
|
+
const content = `// Auto-generated query options
|
|
790
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
791
|
+
import { apiClient } from './client';
|
|
792
|
+
import { queryKeys } from './query-keys';
|
|
793
|
+
import { z } from 'zod';
|
|
794
|
+
import { apiConfig } from '${relativePath}';
|
|
795
|
+
|
|
796
|
+
${this.generateQueryOptionsContent()}
|
|
797
|
+
|
|
798
|
+
export const apiQueryOptions = {
|
|
799
|
+
${this.generateQueryOptionsExports()}
|
|
800
|
+
} as const;
|
|
801
|
+
`;
|
|
802
|
+
return content;
|
|
803
|
+
}
|
|
804
|
+
generateQueryOptionsContent() {
|
|
805
|
+
const groups = this.groupEndpointsByResource();
|
|
806
|
+
const options = [];
|
|
807
|
+
Object.entries(groups).forEach(([resource, endpoints]) => {
|
|
808
|
+
const queries = endpoints.filter(
|
|
809
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
810
|
+
);
|
|
811
|
+
if (queries.length === 0) return;
|
|
812
|
+
const resourceOptions = [];
|
|
813
|
+
queries.forEach(({ name, endpoint }) => {
|
|
814
|
+
const optionName = this.getEndpointKeyName(name);
|
|
815
|
+
const inferParams = this.inferNonNull(
|
|
816
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
817
|
+
);
|
|
818
|
+
const inferQuery = this.inferNonNull(
|
|
819
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
820
|
+
);
|
|
821
|
+
const inferResponse = this.inferNonNull(
|
|
822
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
823
|
+
);
|
|
824
|
+
const params = [];
|
|
825
|
+
let apiCall = "";
|
|
826
|
+
if (endpoint.params && endpoint.query) {
|
|
827
|
+
params.push(`params: ${inferParams}`, `filters?: ${inferQuery}`);
|
|
828
|
+
apiCall = `apiClient.${name}(params, filters)`;
|
|
829
|
+
} else if (endpoint.params) {
|
|
830
|
+
params.push(`params: ${inferParams}`);
|
|
831
|
+
apiCall = `apiClient.${name}(params)`;
|
|
832
|
+
} else if (endpoint.query) {
|
|
833
|
+
params.push(`filters?: ${inferQuery}`);
|
|
834
|
+
apiCall = `apiClient.${name}(filters)`;
|
|
835
|
+
} else {
|
|
836
|
+
apiCall = `apiClient.${name}()`;
|
|
837
|
+
}
|
|
838
|
+
const keyCall = this.generateQueryKeyCall(resource, name, endpoint);
|
|
839
|
+
resourceOptions.push(` ${optionName}: (${params.join(", ")}) =>
|
|
840
|
+
queryOptions({
|
|
841
|
+
queryKey: ${keyCall},
|
|
842
|
+
queryFn: (): Promise<${inferResponse}> => ${apiCall},
|
|
843
|
+
staleTime: 1000 * 60 * 5,
|
|
844
|
+
}),`);
|
|
845
|
+
});
|
|
846
|
+
options.push(
|
|
847
|
+
`const ${resource}QueryOptions = {
|
|
848
|
+
${resourceOptions.join("\n")}
|
|
849
|
+
};
|
|
850
|
+
`
|
|
851
|
+
);
|
|
852
|
+
});
|
|
853
|
+
return options.join("\n");
|
|
854
|
+
}
|
|
855
|
+
generateQueryOptionsExports() {
|
|
856
|
+
const groups = this.groupEndpointsByResource();
|
|
857
|
+
const exports$1 = [];
|
|
858
|
+
Object.keys(groups).forEach((resource) => {
|
|
859
|
+
const hasQueries = groups[resource].some(
|
|
860
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
861
|
+
);
|
|
862
|
+
if (hasQueries) exports$1.push(` ${resource}: ${resource}QueryOptions,`);
|
|
863
|
+
});
|
|
864
|
+
return exports$1.join("\n");
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
var PrefetchGenerator = class extends BaseGenerator {
|
|
868
|
+
async generate() {
|
|
869
|
+
const content = this.generateContent();
|
|
870
|
+
const outputPath = path6.join(this.context.config.outputDir, "prefetchs.ts");
|
|
871
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
872
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
873
|
+
}
|
|
874
|
+
generateContent() {
|
|
875
|
+
const content = `// Auto-generated prefetch utilities
|
|
876
|
+
import { type QueryClient } from '@tanstack/react-query';
|
|
877
|
+
${this.hasQueryOptions() ? "import { apiQueryOptions } from './query-options';" : ""}
|
|
878
|
+
import { z } from 'zod';
|
|
879
|
+
import { apiConfig } from '../config/endpoints';
|
|
880
|
+
|
|
881
|
+
${this.generatePrefetchFunctions()}
|
|
882
|
+
`;
|
|
883
|
+
return content;
|
|
884
|
+
}
|
|
885
|
+
generatePrefetchFunctions() {
|
|
886
|
+
const funcs = [];
|
|
887
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
888
|
+
([name, endpoint]) => {
|
|
889
|
+
if (endpoint.method === "GET")
|
|
890
|
+
funcs.push(this.generatePrefetchFunction(name, endpoint));
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
return funcs.join("\n\n");
|
|
894
|
+
}
|
|
895
|
+
generatePrefetchFunction(name, endpoint) {
|
|
896
|
+
const prefetchName = `prefetch${this.capitalize(name)}`;
|
|
897
|
+
const resource = this.getResourceFromEndpoint(name, endpoint);
|
|
898
|
+
const optionName = this.getEndpointKeyName(name);
|
|
899
|
+
const inferParams = this.inferNonNull(
|
|
900
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
901
|
+
);
|
|
902
|
+
const inferQuery = this.inferNonNull(
|
|
903
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
904
|
+
);
|
|
905
|
+
const params = ["queryClient: QueryClient"];
|
|
906
|
+
const optionParams = [];
|
|
907
|
+
if (endpoint.params) {
|
|
908
|
+
params.push(`params: ${inferParams}`);
|
|
909
|
+
optionParams.push("params");
|
|
910
|
+
}
|
|
911
|
+
if (endpoint.query) {
|
|
912
|
+
params.push(`filters?: ${inferQuery}`);
|
|
913
|
+
optionParams.push("filters");
|
|
914
|
+
}
|
|
915
|
+
return `export const ${prefetchName} = async (${params.join(",\n ")}) => {
|
|
916
|
+
return await queryClient.ensureQueryData(apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}));
|
|
917
|
+
};`;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
var SchemaGenerator = class extends BaseGenerator {
|
|
921
|
+
async generate() {
|
|
922
|
+
const content = this.generateContent();
|
|
923
|
+
const outputPath = path6.join(this.context.config.outputDir, "schema.ts");
|
|
924
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
925
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
926
|
+
}
|
|
927
|
+
generateContent() {
|
|
928
|
+
const content = `// Auto-generated schema definitions
|
|
929
|
+
|
|
930
|
+
import type { z } from 'zod';
|
|
931
|
+
|
|
932
|
+
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
933
|
+
|
|
934
|
+
export interface APIEndpoint {
|
|
935
|
+
path: string;
|
|
936
|
+
method: HTTPMethod;
|
|
937
|
+
baseUrl?: string;
|
|
938
|
+
params?: z.ZodType<any>;
|
|
939
|
+
query?: z.ZodType<any>;
|
|
940
|
+
body?: z.ZodType<any>;
|
|
941
|
+
response: z.ZodType<any>;
|
|
942
|
+
tags?: string[];
|
|
943
|
+
description?: string;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export interface APIConfig {
|
|
947
|
+
baseUrl?: string;
|
|
948
|
+
endpoints: Record<string, APIEndpoint>;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export type EndpointConfig<
|
|
952
|
+
TPath extends string = string,
|
|
953
|
+
TMethod extends HTTPMethod = HTTPMethod,
|
|
954
|
+
TParams = undefined,
|
|
955
|
+
TQuery = undefined,
|
|
956
|
+
TBody = undefined,
|
|
957
|
+
TResponse = any,
|
|
958
|
+
> = {
|
|
959
|
+
path: TPath;
|
|
960
|
+
method: TMethod;
|
|
961
|
+
baseUrl?: string;
|
|
962
|
+
params?: z.ZodType<TParams>;
|
|
963
|
+
query?: z.ZodType<TQuery>;
|
|
964
|
+
body?: z.ZodType<TBody>;
|
|
965
|
+
response: z.ZodType<TResponse>;
|
|
966
|
+
tags?: string[];
|
|
967
|
+
description?: string;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Helper function to define API configuration with type safety
|
|
972
|
+
*/
|
|
973
|
+
export function defineConfig<T extends APIConfig>(config: T): T {
|
|
974
|
+
return config;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Helper function to define a single endpoint with type inference
|
|
979
|
+
*/
|
|
980
|
+
export function defineEndpoint<
|
|
981
|
+
TPath extends string,
|
|
982
|
+
TMethod extends HTTPMethod,
|
|
983
|
+
TParams = undefined,
|
|
984
|
+
TQuery = undefined,
|
|
985
|
+
TBody = undefined,
|
|
986
|
+
TResponse = any,
|
|
987
|
+
>(
|
|
988
|
+
config: EndpointConfig<TPath, TMethod, TParams, TQuery, TBody, TResponse>,
|
|
989
|
+
): EndpointConfig<TPath, TMethod, TParams, TQuery, TBody, TResponse> {
|
|
990
|
+
return config;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Helper to define multiple endpoints
|
|
995
|
+
*/
|
|
996
|
+
export function defineEndpoints<
|
|
997
|
+
T extends Record<string, APIEndpoint>,
|
|
998
|
+
>(endpoints: T): T {
|
|
999
|
+
return endpoints;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
`;
|
|
1003
|
+
return content;
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
var CoreGenerator = class extends BaseGenerator {
|
|
1007
|
+
async generate() {
|
|
1008
|
+
const content = this.generateContent();
|
|
1009
|
+
const outputPath = path6.join(this.context.config.outputDir, "core.ts");
|
|
1010
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
1011
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
1012
|
+
}
|
|
1013
|
+
generateContent() {
|
|
1014
|
+
const content = `// Auto-generated schema definitions
|
|
1015
|
+
|
|
1016
|
+
import ky, { HTTPError } from "ky";
|
|
1017
|
+
import type { APIConfig, APIEndpoint } from "../config/schema.js";
|
|
1018
|
+
|
|
1019
|
+
export interface AuthTokens {
|
|
1020
|
+
accessToken: string;
|
|
1021
|
+
refreshToken?: string;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
export interface AuthCallbacks {
|
|
1025
|
+
getTokens: () => Promise<AuthTokens | null>;
|
|
1026
|
+
onAuthError?: () => void;
|
|
1027
|
+
onRefreshToken?: () => Promise<void>;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export class APIError extends Error {
|
|
1031
|
+
constructor(
|
|
1032
|
+
message: string,
|
|
1033
|
+
public status: number,
|
|
1034
|
+
public response?: any,
|
|
1035
|
+
) {
|
|
1036
|
+
super(message);
|
|
1037
|
+
this.name = "APIError";
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
export class AuthError extends APIError {
|
|
1042
|
+
constructor(message: string = "Authentication failed") {
|
|
1043
|
+
super(message, 401);
|
|
1044
|
+
this.name = "AuthError";
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export class APIClient {
|
|
1049
|
+
private client: typeof ky;
|
|
1050
|
+
private isRefreshing = false;
|
|
1051
|
+
private refreshPromise: Promise<void> | null = null;
|
|
1052
|
+
private hooks: any;
|
|
1053
|
+
|
|
1054
|
+
constructor(
|
|
1055
|
+
private config: APIConfig,
|
|
1056
|
+
private authCallbacks?: AuthCallbacks,
|
|
1057
|
+
) {
|
|
1058
|
+
this.hooks = {
|
|
1059
|
+
beforeRequest: [
|
|
1060
|
+
async (request: Request) => {
|
|
1061
|
+
const tokens = await this.authCallbacks?.getTokens();
|
|
1062
|
+
if (tokens?.accessToken) {
|
|
1063
|
+
request.headers.set(
|
|
1064
|
+
"Authorization",
|
|
1065
|
+
\`Bearer \${tokens.accessToken}\`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
beforeRetry: [
|
|
1071
|
+
async ({ request, error, retryCount }: any) => {
|
|
1072
|
+
if (error instanceof HTTPError && error.response.status === 401) {
|
|
1073
|
+
if (retryCount === 1 && this.authCallbacks) {
|
|
1074
|
+
try {
|
|
1075
|
+
await this.refreshTokens();
|
|
1076
|
+
const tokens = await this.authCallbacks.getTokens();
|
|
1077
|
+
if (tokens?.accessToken) {
|
|
1078
|
+
request.headers.set(
|
|
1079
|
+
"Authorization",
|
|
1080
|
+
\`Bearer \${tokens.accessToken}\`,
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
} catch (refreshError) {
|
|
1084
|
+
this.authCallbacks.onAuthError?.();
|
|
1085
|
+
throw new AuthError();
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
this.authCallbacks?.onAuthError?.();
|
|
1089
|
+
throw new AuthError();
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
],
|
|
1094
|
+
beforeError: [
|
|
1095
|
+
async (error: any) => {
|
|
1096
|
+
const { response } = error;
|
|
1097
|
+
if (response?.body) {
|
|
1098
|
+
try {
|
|
1099
|
+
const body = await response.json();
|
|
1100
|
+
error.message =
|
|
1101
|
+
(body as Error).message || \`HTTP \${response.status}\`;
|
|
1102
|
+
} catch {
|
|
1103
|
+
// Keep original message
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return error;
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
this.client = ky.create({
|
|
1112
|
+
prefixUrl: this.config.baseUrl,
|
|
1113
|
+
headers: {
|
|
1114
|
+
"Content-Type": "application/json",
|
|
1115
|
+
},
|
|
1116
|
+
retry: {
|
|
1117
|
+
limit: 2,
|
|
1118
|
+
methods: ["get", "post", "put", "delete", "patch"],
|
|
1119
|
+
statusCodes: [401],
|
|
1120
|
+
},
|
|
1121
|
+
hooks: this.hooks,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private async refreshTokens(): Promise<void> {
|
|
1126
|
+
if (!this.authCallbacks) {
|
|
1127
|
+
throw new AuthError("No auth callbacks provided");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (this.isRefreshing && this.refreshPromise) {
|
|
1131
|
+
return this.refreshPromise;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
this.isRefreshing = true;
|
|
1135
|
+
|
|
1136
|
+
this.refreshPromise = (async () => {
|
|
1137
|
+
try {
|
|
1138
|
+
if (this.authCallbacks?.onRefreshToken) {
|
|
1139
|
+
await this.authCallbacks.onRefreshToken();
|
|
1140
|
+
} else {
|
|
1141
|
+
throw new AuthError("No refresh token handler provided");
|
|
1142
|
+
}
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
throw error;
|
|
1145
|
+
} finally {
|
|
1146
|
+
this.isRefreshing = false;
|
|
1147
|
+
this.refreshPromise = null;
|
|
1148
|
+
}
|
|
1149
|
+
})();
|
|
1150
|
+
|
|
1151
|
+
return this.refreshPromise;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private buildPath(path: string, params?: Record<string, any>): string {
|
|
1155
|
+
if (!params) return path;
|
|
1156
|
+
|
|
1157
|
+
let finalPath = path;
|
|
1158
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
1159
|
+
finalPath = finalPath.replace(
|
|
1160
|
+
\`:\${key}\`,
|
|
1161
|
+
encodeURIComponent(String(value)),
|
|
1162
|
+
);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
return finalPath;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private getEndpointBaseUrl(endpoint: APIEndpoint): string {
|
|
1169
|
+
return endpoint.baseUrl || this.config.baseUrl!;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private getClientForEndpoint(endpoint: APIEndpoint): typeof ky {
|
|
1173
|
+
const endpointBaseUrl = this.getEndpointBaseUrl(endpoint);
|
|
1174
|
+
|
|
1175
|
+
if (endpointBaseUrl === this.config.baseUrl) {
|
|
1176
|
+
return this.client;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return ky.create({
|
|
1180
|
+
prefixUrl: endpointBaseUrl,
|
|
1181
|
+
headers: {
|
|
1182
|
+
"Content-Type": "application/json",
|
|
1183
|
+
},
|
|
1184
|
+
retry: {
|
|
1185
|
+
limit: 2,
|
|
1186
|
+
methods: ["get", "post", "put", "delete", "patch"],
|
|
1187
|
+
statusCodes: [401],
|
|
1188
|
+
},
|
|
1189
|
+
hooks: this.hooks,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async request<T>(
|
|
1194
|
+
endpoint: APIEndpoint,
|
|
1195
|
+
params?: Record<string, any>,
|
|
1196
|
+
query?: Record<string, any>,
|
|
1197
|
+
body?: any,
|
|
1198
|
+
): Promise<T> {
|
|
1199
|
+
try {
|
|
1200
|
+
const path = this.buildPath(endpoint.path, params);
|
|
1201
|
+
const client = this.getClientForEndpoint(endpoint);
|
|
1202
|
+
|
|
1203
|
+
const options: Record<string, any> = {
|
|
1204
|
+
method: endpoint.method,
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
if (query && Object.keys(query).length > 0) {
|
|
1208
|
+
const searchParams = new URLSearchParams();
|
|
1209
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
1210
|
+
if (value !== undefined && value !== null) {
|
|
1211
|
+
searchParams.append(key, String(value));
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
if (searchParams.toString()) {
|
|
1215
|
+
options.searchParams = searchParams;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (body && endpoint.method !== "GET") {
|
|
1220
|
+
if (endpoint.body) {
|
|
1221
|
+
const validatedBody = endpoint.body.parse(body);
|
|
1222
|
+
options.json = validatedBody;
|
|
1223
|
+
} else {
|
|
1224
|
+
options.json = body;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const response = await client(path, options);
|
|
1229
|
+
const data = await response.json();
|
|
1230
|
+
|
|
1231
|
+
if (endpoint.response) {
|
|
1232
|
+
return endpoint.response.parse(data);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return data as T;
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (error instanceof HTTPError) {
|
|
1238
|
+
const errorData = await error.response.json().catch(() => ({}));
|
|
1239
|
+
throw new APIError(
|
|
1240
|
+
errorData.message || error.message,
|
|
1241
|
+
error.response.status,
|
|
1242
|
+
errorData,
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (error instanceof AuthError) {
|
|
1247
|
+
throw error;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
throw new APIError(
|
|
1251
|
+
error instanceof Error ? error.message : "Network error",
|
|
1252
|
+
0,
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
updateAuthCallbacks(authCallbacks: AuthCallbacks) {
|
|
1258
|
+
this.authCallbacks = authCallbacks;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async refreshAuth(): Promise<void> {
|
|
1262
|
+
if (!this.authCallbacks) {
|
|
1263
|
+
throw new AuthError("No auth callbacks provided");
|
|
1264
|
+
}
|
|
1265
|
+
await this.refreshTokens();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
generateMethods() {
|
|
1269
|
+
const methods: any = {};
|
|
1270
|
+
|
|
1271
|
+
Object.entries(this.config.endpoints).forEach(([name, endpoint]) => {
|
|
1272
|
+
if (endpoint.method === "GET") {
|
|
1273
|
+
if (endpoint.params && endpoint.query) {
|
|
1274
|
+
methods[name] = (params: any, query?: any): Promise<any> => {
|
|
1275
|
+
return this.request(endpoint, params, query);
|
|
1276
|
+
};
|
|
1277
|
+
} else if (endpoint.params) {
|
|
1278
|
+
methods[name] = (params: any): Promise<any> => {
|
|
1279
|
+
return this.request(endpoint, params);
|
|
1280
|
+
};
|
|
1281
|
+
} else if (endpoint.query) {
|
|
1282
|
+
methods[name] = (query?: any): Promise<any> => {
|
|
1283
|
+
return this.request(endpoint, undefined, query);
|
|
1284
|
+
};
|
|
1285
|
+
} else {
|
|
1286
|
+
methods[name] = (): Promise<any> => {
|
|
1287
|
+
return this.request(endpoint);
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
} else {
|
|
1291
|
+
if (endpoint.params && endpoint.body) {
|
|
1292
|
+
methods[name] = (params: any, body: any): Promise<any> => {
|
|
1293
|
+
return this.request(endpoint, params, undefined, body);
|
|
1294
|
+
};
|
|
1295
|
+
} else if (endpoint.params) {
|
|
1296
|
+
methods[name] = (params: any): Promise<any> => {
|
|
1297
|
+
return this.request(endpoint, params);
|
|
1298
|
+
};
|
|
1299
|
+
} else if (endpoint.body) {
|
|
1300
|
+
methods[name] = (body: any): Promise<any> => {
|
|
1301
|
+
return this.request(endpoint, undefined, undefined, body);
|
|
1302
|
+
};
|
|
1303
|
+
} else {
|
|
1304
|
+
methods[name] = (): Promise<any> => {
|
|
1305
|
+
return this.request(endpoint);
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
return methods;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
export function createAPIClient(
|
|
1316
|
+
config: APIConfig,
|
|
1317
|
+
authCallbacks?: AuthCallbacks,
|
|
1318
|
+
) {
|
|
1319
|
+
const instance = new APIClient(config, authCallbacks);
|
|
1320
|
+
const methods = instance.generateMethods();
|
|
1321
|
+
|
|
1322
|
+
return {
|
|
1323
|
+
...methods,
|
|
1324
|
+
refreshAuth: () => instance.refreshAuth(),
|
|
1325
|
+
updateAuthCallbacks: (newCallbacks: AuthCallbacks) =>
|
|
1326
|
+
instance.updateAuthCallbacks(newCallbacks),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
`;
|
|
1330
|
+
return content;
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
// src/generators/index.ts
|
|
1335
|
+
var CodeGenerator = class {
|
|
1336
|
+
constructor(context) {
|
|
1337
|
+
this.context = context;
|
|
1338
|
+
}
|
|
1339
|
+
async generate() {
|
|
1340
|
+
const generators = this.getGenerators();
|
|
1341
|
+
for (const generator of generators) {
|
|
1342
|
+
await generator.generate();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
getGenerators() {
|
|
1346
|
+
const generators = [];
|
|
1347
|
+
generators.push(new TypesGenerator(this.context));
|
|
1348
|
+
generators.push(new SchemaGenerator(this.context));
|
|
1349
|
+
generators.push(new CoreGenerator(this.context));
|
|
1350
|
+
if (this.context.config.generateClient) {
|
|
1351
|
+
generators.push(new ClientGenerator(this.context));
|
|
1352
|
+
}
|
|
1353
|
+
if (this.context.config.generateHooks) {
|
|
1354
|
+
generators.push(new QueryKeysGenerator(this.context));
|
|
1355
|
+
generators.push(new QueryOptionsGenerator(this.context));
|
|
1356
|
+
generators.push(new HooksGenerator(this.context));
|
|
1357
|
+
}
|
|
1358
|
+
if (this.context.config.generatePrefetch) {
|
|
1359
|
+
generators.push(new PrefetchGenerator(this.context));
|
|
1360
|
+
}
|
|
1361
|
+
if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {
|
|
1362
|
+
generators.push(new ServerActionsGenerator(this.context));
|
|
1363
|
+
}
|
|
1364
|
+
if (this.context.config.generateServerQueries && this.context.config.provider === "nextjs") {
|
|
1365
|
+
generators.push(new ServerQueriesGenerator(this.context));
|
|
1366
|
+
}
|
|
1367
|
+
return generators;
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
var CodegenCore = class {
|
|
1371
|
+
constructor(config) {
|
|
1372
|
+
this.config = config;
|
|
1373
|
+
}
|
|
1374
|
+
async execute() {
|
|
1375
|
+
const apiConfig = await this.loadAPIConfig();
|
|
1376
|
+
this.config.apiConfig = apiConfig;
|
|
1377
|
+
const generator = new CodeGenerator({
|
|
1378
|
+
config: this.config,
|
|
1379
|
+
apiConfig
|
|
1380
|
+
});
|
|
1381
|
+
await generator.generate();
|
|
1382
|
+
}
|
|
1383
|
+
async loadAPIConfig() {
|
|
1384
|
+
try {
|
|
1385
|
+
const jiti = createJiti(fileURLToPath(import.meta.url), {
|
|
1386
|
+
interopDefault: true
|
|
1387
|
+
});
|
|
1388
|
+
const module = await jiti.import(this.config.endpointsPath);
|
|
1389
|
+
const apiConfig = module.apiConfig || module.default?.apiConfig || module.default || module;
|
|
1390
|
+
if (!apiConfig || !apiConfig.endpoints) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
'Invalid API config: must export an object with "endpoints" property'
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
return apiConfig;
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
throw new Error(
|
|
1398
|
+
`Failed to load endpoints from "${this.config.endpointsPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
var program = new Command();
|
|
1404
|
+
program.name("api-codegen").description("Generate type-safe API client code from endpoint definitions").version("1.0.0");
|
|
1405
|
+
program.command("generate").alias("gen").description("Generate API client code from configuration").option("-c, --config <path>", "Path to configuration file").option("-w, --watch", "Watch for changes and regenerate").action(async (options) => {
|
|
1406
|
+
const spinner = ora("Loading configuration...").start();
|
|
1407
|
+
try {
|
|
1408
|
+
const config = await loadConfig(options.config);
|
|
1409
|
+
if (!config) {
|
|
1410
|
+
spinner.fail(
|
|
1411
|
+
chalk.red(
|
|
1412
|
+
"No configuration file found. Please create an api-codegen.config.js file."
|
|
1413
|
+
)
|
|
1414
|
+
);
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
}
|
|
1417
|
+
spinner.text = "Validating configuration...";
|
|
1418
|
+
validateConfig(config);
|
|
1419
|
+
spinner.text = "Loading API endpoints...";
|
|
1420
|
+
const codegen = new CodegenCore(config);
|
|
1421
|
+
spinner.text = "Generating code...";
|
|
1422
|
+
await codegen.execute();
|
|
1423
|
+
spinner.succeed(
|
|
1424
|
+
chalk.green(
|
|
1425
|
+
`\u2728 Code generated successfully in ${chalk.cyan(config.outputDir)}`
|
|
1426
|
+
)
|
|
1427
|
+
);
|
|
1428
|
+
console.log(chalk.dim("\nGenerated files:"));
|
|
1429
|
+
const files = await fs5.readdir(config.outputDir);
|
|
1430
|
+
files.forEach((file) => {
|
|
1431
|
+
console.log(chalk.dim(` \u2022 ${file}`));
|
|
1432
|
+
});
|
|
1433
|
+
if (options.watch) {
|
|
1434
|
+
console.log(chalk.yellow("\n\u{1F440} Watching for changes..."));
|
|
1435
|
+
spinner.info(chalk.dim("Watch mode not yet implemented"));
|
|
1436
|
+
}
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
spinner.fail(chalk.red("Failed to generate code"));
|
|
1439
|
+
console.error(
|
|
1440
|
+
chalk.red("\n" + (error instanceof Error ? error.message : String(error)))
|
|
1441
|
+
);
|
|
1442
|
+
if (error instanceof Error && error.stack) {
|
|
1443
|
+
console.error(chalk.dim(error.stack));
|
|
1444
|
+
}
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
program.command("init").description("Initialize a new api-codegen configuration").option("-p, --provider <provider>", "Provider type (vite or nextjs)", "vite").action(async (options) => {
|
|
1449
|
+
const spinner = ora("Creating configuration file...").start();
|
|
1450
|
+
try {
|
|
1451
|
+
const configContent = generateConfigTemplate(options.provider);
|
|
1452
|
+
const configPath = path6.join(process.cwd(), "api-codegen.config.js");
|
|
1453
|
+
try {
|
|
1454
|
+
await fs5.access(configPath);
|
|
1455
|
+
spinner.warn(
|
|
1456
|
+
chalk.yellow(
|
|
1457
|
+
"Configuration file already exists at api-codegen.config.js"
|
|
1458
|
+
)
|
|
1459
|
+
);
|
|
1460
|
+
return;
|
|
1461
|
+
} catch {
|
|
1462
|
+
}
|
|
1463
|
+
await fs5.writeFile(configPath, configContent, "utf-8");
|
|
1464
|
+
spinner.succeed(
|
|
1465
|
+
chalk.green("\u2728 Configuration file created: api-codegen.config.js")
|
|
1466
|
+
);
|
|
1467
|
+
console.log(chalk.dim("\nNext steps:"));
|
|
1468
|
+
console.log(chalk.dim(" 1. Update the endpoints path in the config"));
|
|
1469
|
+
console.log(chalk.dim(" 2. Run: npx @cushin/api-codegen generate"));
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
spinner.fail(chalk.red("Failed to create configuration file"));
|
|
1472
|
+
console.error(
|
|
1473
|
+
chalk.red("\n" + (error instanceof Error ? error.message : String(error)))
|
|
1474
|
+
);
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
program.command("validate").description("Validate your API endpoints configuration").option("-c, --config <path>", "Path to configuration file").action(async (options) => {
|
|
1479
|
+
const spinner = ora("Loading configuration...").start();
|
|
1480
|
+
try {
|
|
1481
|
+
const config = await loadConfig(options.config);
|
|
1482
|
+
if (!config) {
|
|
1483
|
+
spinner.fail(chalk.red("No configuration file found"));
|
|
1484
|
+
process.exit(1);
|
|
1485
|
+
}
|
|
1486
|
+
spinner.text = "Validating configuration...";
|
|
1487
|
+
validateConfig(config);
|
|
1488
|
+
spinner.text = "Loading API endpoints...";
|
|
1489
|
+
new CodegenCore(config);
|
|
1490
|
+
const apiConfigModule = await import(pathToFileURL(config.endpointsPath).href);
|
|
1491
|
+
const apiConfig = apiConfigModule.apiConfig || apiConfigModule.default?.apiConfig || apiConfigModule.default;
|
|
1492
|
+
if (!apiConfig || !apiConfig.endpoints) {
|
|
1493
|
+
throw new Error("Invalid endpoints configuration");
|
|
1494
|
+
}
|
|
1495
|
+
const endpointCount = Object.keys(apiConfig.endpoints).length;
|
|
1496
|
+
spinner.succeed(
|
|
1497
|
+
chalk.green(
|
|
1498
|
+
`\u2728 Configuration is valid! Found ${endpointCount} endpoint${endpointCount === 1 ? "" : "s"}`
|
|
1499
|
+
)
|
|
1500
|
+
);
|
|
1501
|
+
console.log(chalk.dim("\nEndpoints:"));
|
|
1502
|
+
Object.entries(apiConfig.endpoints).forEach(([name, endpoint]) => {
|
|
1503
|
+
console.log(
|
|
1504
|
+
chalk.dim(
|
|
1505
|
+
` \u2022 ${chalk.cyan(name)}: ${endpoint.method} ${endpoint.path}`
|
|
1506
|
+
)
|
|
1507
|
+
);
|
|
1508
|
+
});
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
spinner.fail(chalk.red("Validation failed"));
|
|
1511
|
+
console.error(
|
|
1512
|
+
chalk.red("\n" + (error instanceof Error ? error.message : String(error)))
|
|
1513
|
+
);
|
|
1514
|
+
process.exit(1);
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
function generateConfigTemplate(provider) {
|
|
1518
|
+
return `/** @type {import('@cushin/api-codegen').UserConfig} */
|
|
1519
|
+
export default {
|
|
1520
|
+
// Provider: 'vite' or 'nextjs'
|
|
1521
|
+
provider: '${provider}',
|
|
1522
|
+
|
|
1523
|
+
// Path to your API endpoints configuration
|
|
1524
|
+
endpoints: './lib/api/config/endpoints.ts',
|
|
1525
|
+
|
|
1526
|
+
// Output directory for generated files
|
|
1527
|
+
output: './lib/api/generated',
|
|
1528
|
+
|
|
1529
|
+
// Base URL for API requests (optional, can be set at runtime)
|
|
1530
|
+
baseUrl: process.env.VITE_API_URL || process.env.NEXT_PUBLIC_API_URL,
|
|
1531
|
+
|
|
1532
|
+
// Generation options
|
|
1533
|
+
generateHooks: true,
|
|
1534
|
+
generateClient: true,
|
|
1535
|
+
${provider === "nextjs" ? `generateServerActions: true,
|
|
1536
|
+
generateServerQueries: true,` : ""}
|
|
1537
|
+
|
|
1538
|
+
// Advanced options
|
|
1539
|
+
options: {
|
|
1540
|
+
useClientDirective: true,
|
|
1541
|
+
hookPrefix: 'use',
|
|
1542
|
+
actionSuffix: 'Action',
|
|
1543
|
+
},
|
|
1544
|
+
};
|
|
1545
|
+
`;
|
|
1546
|
+
}
|
|
1547
|
+
function pathToFileURL(filePath) {
|
|
1548
|
+
return new URL(`file://${path6.resolve(filePath)}`);
|
|
1549
|
+
}
|
|
1550
|
+
program.parse();
|
|
1551
|
+
//# sourceMappingURL=cli.js.map
|
|
1552
|
+
//# sourceMappingURL=cli.js.map
|