@cushin/api-codegen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +435 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +756 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +79 -0
- package/dist/config/index.js +63 -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 +875 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/client.d.ts +42 -0
- package/dist/runtime/client.js +265 -0
- package/dist/runtime/client.js.map +1 -0
- package/package.json +100 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
2
|
+
import path6 from 'path';
|
|
3
|
+
import ky, { HTTPError } from 'ky';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import fs5 from 'fs/promises';
|
|
6
|
+
|
|
7
|
+
// src/config/schema.ts
|
|
8
|
+
function defineConfig(config) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
function defineEndpoint(config) {
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
function defineEndpoints(endpoints) {
|
|
15
|
+
return endpoints;
|
|
16
|
+
}
|
|
17
|
+
var explorer = cosmiconfig("api-codegen", {
|
|
18
|
+
searchPlaces: [
|
|
19
|
+
"api-codegen.config.js",
|
|
20
|
+
"api-codegen.config.mjs",
|
|
21
|
+
"api-codegen.config.ts",
|
|
22
|
+
"api-codegen.config.json",
|
|
23
|
+
".api-codegenrc",
|
|
24
|
+
".api-codegenrc.json",
|
|
25
|
+
".api-codegenrc.js"
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
async function loadConfig(configPath) {
|
|
29
|
+
try {
|
|
30
|
+
const result = configPath ? await explorer.load(configPath) : await explorer.search();
|
|
31
|
+
if (!result || !result.config) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const userConfig = result.config;
|
|
35
|
+
const rootDir = path6.dirname(result.filepath);
|
|
36
|
+
const endpointsPath = path6.resolve(rootDir, userConfig.endpoints);
|
|
37
|
+
const outputDir = path6.resolve(rootDir, userConfig.output);
|
|
38
|
+
const generateHooks = userConfig.generateHooks ?? true;
|
|
39
|
+
const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs";
|
|
40
|
+
const generateServerQueries = userConfig.generateServerQueries ?? userConfig.provider === "nextjs";
|
|
41
|
+
const generateClient = userConfig.generateClient ?? true;
|
|
42
|
+
return {
|
|
43
|
+
...userConfig,
|
|
44
|
+
rootDir,
|
|
45
|
+
endpointsPath,
|
|
46
|
+
outputDir,
|
|
47
|
+
generateHooks,
|
|
48
|
+
generateServerActions,
|
|
49
|
+
generateServerQueries,
|
|
50
|
+
generateClient
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Failed to load config: ${error instanceof Error ? error.message : String(error)}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function validateConfig(config) {
|
|
59
|
+
if (!config.endpoints) {
|
|
60
|
+
throw new Error('Config error: "endpoints" path is required');
|
|
61
|
+
}
|
|
62
|
+
if (!config.provider) {
|
|
63
|
+
throw new Error('Config error: "provider" must be specified (vite or nextjs)');
|
|
64
|
+
}
|
|
65
|
+
if (!["vite", "nextjs"].includes(config.provider)) {
|
|
66
|
+
throw new Error('Config error: "provider" must be either "vite" or "nextjs"');
|
|
67
|
+
}
|
|
68
|
+
if (!config.output) {
|
|
69
|
+
throw new Error('Config error: "output" directory is required');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var APIError = class extends Error {
|
|
73
|
+
constructor(message, status, response) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.status = status;
|
|
76
|
+
this.response = response;
|
|
77
|
+
this.name = "APIError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var AuthError = class extends APIError {
|
|
81
|
+
constructor(message = "Authentication failed") {
|
|
82
|
+
super(message, 401);
|
|
83
|
+
this.name = "AuthError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var APIClient = class {
|
|
87
|
+
constructor(config, authCallbacks) {
|
|
88
|
+
this.config = config;
|
|
89
|
+
this.authCallbacks = authCallbacks;
|
|
90
|
+
this.hooks = {
|
|
91
|
+
beforeRequest: [
|
|
92
|
+
(request) => {
|
|
93
|
+
const tokens = this.authCallbacks?.getTokens();
|
|
94
|
+
if (tokens?.accessToken) {
|
|
95
|
+
request.headers.set(
|
|
96
|
+
"Authorization",
|
|
97
|
+
`Bearer ${tokens.accessToken}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
beforeRetry: [
|
|
103
|
+
async ({ request, error, retryCount }) => {
|
|
104
|
+
if (error instanceof HTTPError && error.response.status === 401) {
|
|
105
|
+
if (retryCount === 1 && this.authCallbacks) {
|
|
106
|
+
try {
|
|
107
|
+
await this.refreshTokens();
|
|
108
|
+
const tokens = this.authCallbacks.getTokens();
|
|
109
|
+
if (tokens?.accessToken) {
|
|
110
|
+
request.headers.set(
|
|
111
|
+
"Authorization",
|
|
112
|
+
`Bearer ${tokens.accessToken}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} catch (refreshError) {
|
|
116
|
+
this.authCallbacks.clearTokens();
|
|
117
|
+
this.authCallbacks.onAuthError?.();
|
|
118
|
+
throw new AuthError();
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
this.authCallbacks?.clearTokens();
|
|
122
|
+
this.authCallbacks?.onAuthError?.();
|
|
123
|
+
throw new AuthError();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
beforeError: [
|
|
129
|
+
async (error) => {
|
|
130
|
+
const { response } = error;
|
|
131
|
+
if (response?.body) {
|
|
132
|
+
try {
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
error.message = body.message || `HTTP ${response.status}`;
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
};
|
|
142
|
+
this.client = ky.create({
|
|
143
|
+
prefixUrl: this.config.baseUrl,
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json"
|
|
146
|
+
},
|
|
147
|
+
retry: {
|
|
148
|
+
limit: 2,
|
|
149
|
+
methods: ["get", "post", "put", "delete", "patch"],
|
|
150
|
+
statusCodes: [401]
|
|
151
|
+
},
|
|
152
|
+
hooks: this.hooks
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
client;
|
|
156
|
+
isRefreshing = false;
|
|
157
|
+
refreshPromise = null;
|
|
158
|
+
hooks;
|
|
159
|
+
async refreshTokens() {
|
|
160
|
+
if (!this.authCallbacks) {
|
|
161
|
+
throw new AuthError("No auth callbacks provided");
|
|
162
|
+
}
|
|
163
|
+
if (this.isRefreshing && this.refreshPromise) {
|
|
164
|
+
return this.refreshPromise;
|
|
165
|
+
}
|
|
166
|
+
this.isRefreshing = true;
|
|
167
|
+
this.refreshPromise = (async () => {
|
|
168
|
+
try {
|
|
169
|
+
if (this.authCallbacks?.onRefreshToken) {
|
|
170
|
+
const newAccessToken = await this.authCallbacks.onRefreshToken();
|
|
171
|
+
this.authCallbacks.setTokens({
|
|
172
|
+
accessToken: newAccessToken
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
throw new AuthError("No refresh token handler provided");
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
180
|
+
this.isRefreshing = false;
|
|
181
|
+
this.refreshPromise = null;
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
return this.refreshPromise;
|
|
185
|
+
}
|
|
186
|
+
buildPath(path7, params) {
|
|
187
|
+
if (!params) return path7;
|
|
188
|
+
let finalPath = path7;
|
|
189
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
190
|
+
finalPath = finalPath.replace(
|
|
191
|
+
`:${key}`,
|
|
192
|
+
encodeURIComponent(String(value))
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
return finalPath;
|
|
196
|
+
}
|
|
197
|
+
getEndpointBaseUrl(endpoint) {
|
|
198
|
+
return endpoint.baseUrl || this.config.baseUrl;
|
|
199
|
+
}
|
|
200
|
+
getClientForEndpoint(endpoint) {
|
|
201
|
+
const endpointBaseUrl = this.getEndpointBaseUrl(endpoint);
|
|
202
|
+
if (endpointBaseUrl === this.config.baseUrl) {
|
|
203
|
+
return this.client;
|
|
204
|
+
}
|
|
205
|
+
return ky.create({
|
|
206
|
+
prefixUrl: endpointBaseUrl,
|
|
207
|
+
headers: {
|
|
208
|
+
"Content-Type": "application/json"
|
|
209
|
+
},
|
|
210
|
+
retry: {
|
|
211
|
+
limit: 2,
|
|
212
|
+
methods: ["get", "post", "put", "delete", "patch"],
|
|
213
|
+
statusCodes: [401]
|
|
214
|
+
},
|
|
215
|
+
hooks: this.hooks
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async request(endpoint, params, query, body) {
|
|
219
|
+
try {
|
|
220
|
+
const path7 = this.buildPath(endpoint.path, params);
|
|
221
|
+
const client = this.getClientForEndpoint(endpoint);
|
|
222
|
+
const options = {
|
|
223
|
+
method: endpoint.method
|
|
224
|
+
};
|
|
225
|
+
if (query && Object.keys(query).length > 0) {
|
|
226
|
+
const searchParams = new URLSearchParams();
|
|
227
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
228
|
+
if (value !== void 0 && value !== null) {
|
|
229
|
+
searchParams.append(key, String(value));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
if (searchParams.toString()) {
|
|
233
|
+
options.searchParams = searchParams;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (body && endpoint.method !== "GET") {
|
|
237
|
+
if (endpoint.body) {
|
|
238
|
+
const validatedBody = endpoint.body.parse(body);
|
|
239
|
+
options.json = validatedBody;
|
|
240
|
+
} else {
|
|
241
|
+
options.json = body;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const response = await client(path7, options);
|
|
245
|
+
const data = await response.json();
|
|
246
|
+
if (endpoint.response) {
|
|
247
|
+
return endpoint.response.parse(data);
|
|
248
|
+
}
|
|
249
|
+
return data;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof HTTPError) {
|
|
252
|
+
const errorData = await error.response.json().catch(() => ({}));
|
|
253
|
+
throw new APIError(
|
|
254
|
+
errorData.message || error.message,
|
|
255
|
+
error.response.status,
|
|
256
|
+
errorData
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (error instanceof AuthError) {
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
throw new APIError(
|
|
263
|
+
error instanceof Error ? error.message : "Network error",
|
|
264
|
+
0
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
updateAuthCallbacks(authCallbacks) {
|
|
269
|
+
this.authCallbacks = authCallbacks;
|
|
270
|
+
}
|
|
271
|
+
async refreshAuth() {
|
|
272
|
+
if (!this.authCallbacks) {
|
|
273
|
+
throw new AuthError("No auth callbacks provided");
|
|
274
|
+
}
|
|
275
|
+
await this.refreshTokens();
|
|
276
|
+
}
|
|
277
|
+
generateMethods() {
|
|
278
|
+
const methods = {};
|
|
279
|
+
Object.entries(this.config.endpoints).forEach(([name, endpoint]) => {
|
|
280
|
+
if (endpoint.method === "GET") {
|
|
281
|
+
if (endpoint.params && endpoint.query) {
|
|
282
|
+
methods[name] = (params, query) => {
|
|
283
|
+
return this.request(endpoint, params, query);
|
|
284
|
+
};
|
|
285
|
+
} else if (endpoint.params) {
|
|
286
|
+
methods[name] = (params) => {
|
|
287
|
+
return this.request(endpoint, params);
|
|
288
|
+
};
|
|
289
|
+
} else if (endpoint.query) {
|
|
290
|
+
methods[name] = (query) => {
|
|
291
|
+
return this.request(endpoint, void 0, query);
|
|
292
|
+
};
|
|
293
|
+
} else {
|
|
294
|
+
methods[name] = () => {
|
|
295
|
+
return this.request(endpoint);
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
if (endpoint.params && endpoint.body) {
|
|
300
|
+
methods[name] = (params, body) => {
|
|
301
|
+
return this.request(endpoint, params, void 0, body);
|
|
302
|
+
};
|
|
303
|
+
} else if (endpoint.params) {
|
|
304
|
+
methods[name] = (params) => {
|
|
305
|
+
return this.request(endpoint, params);
|
|
306
|
+
};
|
|
307
|
+
} else if (endpoint.body) {
|
|
308
|
+
methods[name] = (body) => {
|
|
309
|
+
return this.request(endpoint, void 0, void 0, body);
|
|
310
|
+
};
|
|
311
|
+
} else {
|
|
312
|
+
methods[name] = () => {
|
|
313
|
+
return this.request(endpoint);
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return methods;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
function createAPIClient(config, authCallbacks) {
|
|
322
|
+
const instance = new APIClient(config, authCallbacks);
|
|
323
|
+
const methods = instance.generateMethods();
|
|
324
|
+
return {
|
|
325
|
+
...methods,
|
|
326
|
+
refreshAuth: () => instance.refreshAuth(),
|
|
327
|
+
updateAuthCallbacks: (newCallbacks) => instance.updateAuthCallbacks(newCallbacks)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/generators/base.ts
|
|
332
|
+
var BaseGenerator = class {
|
|
333
|
+
constructor(context) {
|
|
334
|
+
this.context = context;
|
|
335
|
+
}
|
|
336
|
+
isQueryEndpoint(endpoint) {
|
|
337
|
+
return endpoint.method === "GET";
|
|
338
|
+
}
|
|
339
|
+
isMutationEndpoint(endpoint) {
|
|
340
|
+
return !this.isQueryEndpoint(endpoint);
|
|
341
|
+
}
|
|
342
|
+
capitalize(str) {
|
|
343
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
344
|
+
}
|
|
345
|
+
getQueryTags(endpoint) {
|
|
346
|
+
return endpoint.tags || [];
|
|
347
|
+
}
|
|
348
|
+
getInvalidationTags(endpoint) {
|
|
349
|
+
const tags = endpoint.tags || [];
|
|
350
|
+
return tags.filter((tag) => tag !== "query" && tag !== "mutation");
|
|
351
|
+
}
|
|
352
|
+
hasParams(endpoint) {
|
|
353
|
+
return !!endpoint.params;
|
|
354
|
+
}
|
|
355
|
+
hasQuery(endpoint) {
|
|
356
|
+
return !!endpoint.query;
|
|
357
|
+
}
|
|
358
|
+
hasBody(endpoint) {
|
|
359
|
+
return !!endpoint.body;
|
|
360
|
+
}
|
|
361
|
+
getEndpointSignature(name, endpoint) {
|
|
362
|
+
const hasParams = this.hasParams(endpoint);
|
|
363
|
+
const hasQuery = this.hasQuery(endpoint);
|
|
364
|
+
const hasBody = this.hasBody(endpoint);
|
|
365
|
+
return {
|
|
366
|
+
hasParams,
|
|
367
|
+
hasQuery,
|
|
368
|
+
hasBody,
|
|
369
|
+
paramType: hasParams ? `ExtractParams<APIEndpoints['${name}']>` : "never",
|
|
370
|
+
queryType: hasQuery ? `ExtractQuery<APIEndpoints['${name}']>` : "never",
|
|
371
|
+
bodyType: hasBody ? `ExtractBody<APIEndpoints['${name}']>` : "never",
|
|
372
|
+
responseType: `ExtractResponse<APIEndpoints['${name}']>`
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
generateMutationCall(name, hasParams, hasBody) {
|
|
376
|
+
if (hasParams && hasBody) {
|
|
377
|
+
return `return apiClient.${name}(input.params, input.body);`;
|
|
378
|
+
} else if (hasParams) {
|
|
379
|
+
return `return apiClient.${name}(input);`;
|
|
380
|
+
} else if (hasBody) {
|
|
381
|
+
return `return apiClient.${name}(input);`;
|
|
382
|
+
} else {
|
|
383
|
+
return `return apiClient.${name}();`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/generators/hooks.ts
|
|
389
|
+
var HooksGenerator = class extends BaseGenerator {
|
|
390
|
+
async generate() {
|
|
391
|
+
const content = this.generateContent();
|
|
392
|
+
const outputPath = path6.join(this.context.config.outputDir, "hooks.ts");
|
|
393
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
394
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
395
|
+
}
|
|
396
|
+
generateContent() {
|
|
397
|
+
const useClientDirective = this.context.config.options?.useClientDirective ?? true;
|
|
398
|
+
const imports = `${useClientDirective ? "'use client';\n" : ""}
|
|
399
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
400
|
+
import type {
|
|
401
|
+
UseQueryOptions,
|
|
402
|
+
UseMutationOptions,
|
|
403
|
+
QueryKey
|
|
404
|
+
} from '@tanstack/react-query';
|
|
405
|
+
import { apiClient } from './client';
|
|
406
|
+
import type {
|
|
407
|
+
APIEndpoints,
|
|
408
|
+
ExtractBody,
|
|
409
|
+
ExtractParams,
|
|
410
|
+
ExtractQuery,
|
|
411
|
+
ExtractResponse
|
|
412
|
+
} from './types';
|
|
413
|
+
`;
|
|
414
|
+
const hooks = [];
|
|
415
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
|
|
416
|
+
if (this.isQueryEndpoint(endpoint)) {
|
|
417
|
+
hooks.push(this.generateQueryHook(name, endpoint));
|
|
418
|
+
} else {
|
|
419
|
+
hooks.push(this.generateMutationHook(name, endpoint));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
return imports + "\n" + hooks.join("\n\n");
|
|
423
|
+
}
|
|
424
|
+
generateQueryHook(name, endpoint) {
|
|
425
|
+
const hookPrefix = this.context.config.options?.hookPrefix || "use";
|
|
426
|
+
const hookName = `${hookPrefix}${this.capitalize(name)}`;
|
|
427
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
428
|
+
const queryTags = this.getQueryTags(endpoint);
|
|
429
|
+
const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
|
|
430
|
+
const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
|
|
431
|
+
const optionsDef = `options?: Omit<UseQueryOptions<${signature.responseType}, Error, ${signature.responseType}, QueryKey>, 'queryKey' | 'queryFn'>`;
|
|
432
|
+
const paramsList = [paramDef, queryDef, optionsDef].filter(Boolean).join(",\n ");
|
|
433
|
+
const queryKeyParts = [
|
|
434
|
+
...queryTags.map((tag) => `'${tag}'`),
|
|
435
|
+
signature.hasParams ? "params" : "undefined",
|
|
436
|
+
signature.hasQuery ? "query" : "undefined"
|
|
437
|
+
];
|
|
438
|
+
const clientCallArgs = [];
|
|
439
|
+
if (signature.hasParams) clientCallArgs.push("params");
|
|
440
|
+
if (signature.hasQuery) clientCallArgs.push("query");
|
|
441
|
+
return `/**
|
|
442
|
+
* ${endpoint.description || `Query hook for ${name}`}
|
|
443
|
+
* @tags ${queryTags.join(", ") || "none"}
|
|
444
|
+
*/
|
|
445
|
+
export function ${hookName}(
|
|
446
|
+
${paramsList}
|
|
447
|
+
) {
|
|
448
|
+
return useQuery({
|
|
449
|
+
queryKey: [${queryKeyParts.join(", ")}] as const,
|
|
450
|
+
queryFn: () => apiClient.${name}(${clientCallArgs.join(", ")}),
|
|
451
|
+
...options,
|
|
452
|
+
});
|
|
453
|
+
}`;
|
|
454
|
+
}
|
|
455
|
+
generateMutationHook(name, endpoint) {
|
|
456
|
+
const hookPrefix = this.context.config.options?.hookPrefix || "use";
|
|
457
|
+
const hookName = `${hookPrefix}${this.capitalize(name)}`;
|
|
458
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
459
|
+
const invalidationTags = this.getInvalidationTags(endpoint);
|
|
460
|
+
let inputType = "void";
|
|
461
|
+
if (signature.hasParams && signature.hasBody) {
|
|
462
|
+
inputType = `{ params: ${signature.paramType}; body: ${signature.bodyType} }`;
|
|
463
|
+
} else if (signature.hasParams) {
|
|
464
|
+
inputType = signature.paramType;
|
|
465
|
+
} else if (signature.hasBody) {
|
|
466
|
+
inputType = signature.bodyType;
|
|
467
|
+
}
|
|
468
|
+
const invalidationQueries = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` queryClient.invalidateQueries({ queryKey: ['${tag}'] });`).join("\n") : " // No automatic invalidations";
|
|
469
|
+
return `/**
|
|
470
|
+
* ${endpoint.description || `Mutation hook for ${name}`}
|
|
471
|
+
* @tags ${endpoint.tags?.join(", ") || "none"}
|
|
472
|
+
*/
|
|
473
|
+
export function ${hookName}(
|
|
474
|
+
options?: Omit<UseMutationOptions<${signature.responseType}, Error, ${inputType}>, 'mutationFn'>
|
|
475
|
+
) {
|
|
476
|
+
const queryClient = useQueryClient();
|
|
477
|
+
|
|
478
|
+
return useMutation({
|
|
479
|
+
mutationFn: ${inputType === "void" ? "() => {" : "(input) => {"}
|
|
480
|
+
${this.generateMutationCall(name, signature.hasParams, signature.hasBody)}
|
|
481
|
+
},
|
|
482
|
+
onSuccess: (data, variables, context) => {
|
|
483
|
+
// Invalidate related queries
|
|
484
|
+
${invalidationQueries}
|
|
485
|
+
|
|
486
|
+
// Call user's onSuccess if provided
|
|
487
|
+
options?.onSuccess?.(data, variables, context);
|
|
488
|
+
},
|
|
489
|
+
...options,
|
|
490
|
+
});
|
|
491
|
+
}`;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
var ServerActionsGenerator = class extends BaseGenerator {
|
|
495
|
+
async generate() {
|
|
496
|
+
const content = this.generateContent();
|
|
497
|
+
const outputPath = path6.join(this.context.config.outputDir, "actions.ts");
|
|
498
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
499
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
500
|
+
}
|
|
501
|
+
generateContent() {
|
|
502
|
+
const imports = `'use server';
|
|
503
|
+
|
|
504
|
+
import { revalidateTag, revalidatePath } from 'next/cache';
|
|
505
|
+
import { serverClient } from './server-client';
|
|
506
|
+
import type {
|
|
507
|
+
APIEndpoints,
|
|
508
|
+
ExtractBody,
|
|
509
|
+
ExtractParams,
|
|
510
|
+
ExtractResponse
|
|
511
|
+
} from './types';
|
|
512
|
+
|
|
513
|
+
export type ActionResult<T> =
|
|
514
|
+
| { success: true; data: T }
|
|
515
|
+
| { success: false; error: string };
|
|
516
|
+
`;
|
|
517
|
+
const actions = [];
|
|
518
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
|
|
519
|
+
if (this.isMutationEndpoint(endpoint)) {
|
|
520
|
+
actions.push(this.generateServerAction(name, endpoint));
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
return imports + "\n" + actions.join("\n\n");
|
|
524
|
+
}
|
|
525
|
+
generateServerAction(name, endpoint) {
|
|
526
|
+
const actionSuffix = this.context.config.options?.actionSuffix || "Action";
|
|
527
|
+
const actionName = `${name}${actionSuffix}`;
|
|
528
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
529
|
+
const invalidationTags = this.getInvalidationTags(endpoint);
|
|
530
|
+
let inputType = "";
|
|
531
|
+
let inputParam = "";
|
|
532
|
+
if (signature.hasParams && signature.hasBody) {
|
|
533
|
+
inputType = `input: { params: ${signature.paramType}; body: ${signature.bodyType} }`;
|
|
534
|
+
inputParam = "input";
|
|
535
|
+
} else if (signature.hasParams) {
|
|
536
|
+
inputType = `params: ${signature.paramType}`;
|
|
537
|
+
inputParam = "params";
|
|
538
|
+
} else if (signature.hasBody) {
|
|
539
|
+
inputType = `body: ${signature.bodyType}`;
|
|
540
|
+
inputParam = "body";
|
|
541
|
+
}
|
|
542
|
+
const revalidateStatements = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` revalidateTag('${tag}');`).join("\n") : " // No automatic revalidations";
|
|
543
|
+
return `/**
|
|
544
|
+
* ${endpoint.description || `Server action for ${name}`}
|
|
545
|
+
* @tags ${endpoint.tags?.join(", ") || "none"}
|
|
546
|
+
*/
|
|
547
|
+
export async function ${actionName}(
|
|
548
|
+
${inputType}
|
|
549
|
+
): Promise<ActionResult<${signature.responseType}>> {
|
|
550
|
+
try {
|
|
551
|
+
const result = await serverClient.${name}(${inputParam ? inputParam : ""});
|
|
552
|
+
|
|
553
|
+
// Revalidate related data
|
|
554
|
+
${revalidateStatements}
|
|
555
|
+
|
|
556
|
+
return { success: true, data: result };
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error('[Server Action Error]:', error);
|
|
559
|
+
return {
|
|
560
|
+
success: false,
|
|
561
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}`;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var ServerQueriesGenerator = class extends BaseGenerator {
|
|
568
|
+
async generate() {
|
|
569
|
+
const content = this.generateContent();
|
|
570
|
+
const outputPath = path6.join(this.context.config.outputDir, "queries.ts");
|
|
571
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
572
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
573
|
+
}
|
|
574
|
+
generateContent() {
|
|
575
|
+
const imports = `import { cache } from 'react';
|
|
576
|
+
import { unstable_cache } from 'next/cache';
|
|
577
|
+
import { serverClient } from './server-client';
|
|
578
|
+
import type {
|
|
579
|
+
APIEndpoints,
|
|
580
|
+
ExtractParams,
|
|
581
|
+
ExtractQuery,
|
|
582
|
+
ExtractResponse
|
|
583
|
+
} from './types';
|
|
584
|
+
`;
|
|
585
|
+
const queries = [];
|
|
586
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
|
|
587
|
+
if (this.isQueryEndpoint(endpoint)) {
|
|
588
|
+
queries.push(this.generateServerQuery(name, endpoint));
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return imports + "\n" + queries.join("\n\n");
|
|
592
|
+
}
|
|
593
|
+
generateServerQuery(name, endpoint) {
|
|
594
|
+
const queryName = `${name}Query`;
|
|
595
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
596
|
+
const queryTags = this.getQueryTags(endpoint);
|
|
597
|
+
const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
|
|
598
|
+
const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
|
|
599
|
+
const paramsList = [paramDef, queryDef].filter(Boolean).join(",\n ");
|
|
600
|
+
const clientCallArgs = [];
|
|
601
|
+
if (signature.hasParams) clientCallArgs.push("params");
|
|
602
|
+
if (signature.hasQuery) clientCallArgs.push("query");
|
|
603
|
+
const cacheKeyParts = [
|
|
604
|
+
`'${name}'`
|
|
605
|
+
];
|
|
606
|
+
if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
|
|
607
|
+
if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
|
|
608
|
+
return `/**
|
|
609
|
+
* ${endpoint.description || `Server query for ${name}`}
|
|
610
|
+
* @tags ${queryTags.join(", ") || "none"}
|
|
611
|
+
*/
|
|
612
|
+
export const ${queryName} = cache(async (
|
|
613
|
+
${paramsList}
|
|
614
|
+
): Promise<${signature.responseType}> => {
|
|
615
|
+
return unstable_cache(
|
|
616
|
+
async () => serverClient.${name}(${clientCallArgs.join(", ")}),
|
|
617
|
+
[${cacheKeyParts.join(", ")}],
|
|
618
|
+
{
|
|
619
|
+
tags: [${queryTags.map((tag) => `'${tag}'`).join(", ")}],
|
|
620
|
+
revalidate: 3600, // 1 hour default, can be overridden
|
|
621
|
+
}
|
|
622
|
+
)();
|
|
623
|
+
});`;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var TypesGenerator = class extends BaseGenerator {
|
|
627
|
+
async generate() {
|
|
628
|
+
const content = this.generateContent();
|
|
629
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
630
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
631
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
632
|
+
}
|
|
633
|
+
generateContent() {
|
|
634
|
+
return `// Auto-generated type definitions
|
|
635
|
+
// Do not edit this file manually
|
|
636
|
+
|
|
637
|
+
import type { z } from 'zod';
|
|
638
|
+
|
|
639
|
+
// Re-export endpoint configuration types
|
|
640
|
+
export type { APIConfig, APIEndpoint, HTTPMethod } from '@vietbus/api-codegen/config';
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Type helper to extract params schema from an endpoint
|
|
644
|
+
*/
|
|
645
|
+
export type ExtractParams<T> = T extends { params: infer P extends z.ZodType }
|
|
646
|
+
? z.infer<P>
|
|
647
|
+
: never;
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Type helper to extract query schema from an endpoint
|
|
651
|
+
*/
|
|
652
|
+
export type ExtractQuery<T> = T extends { query: infer Q extends z.ZodType }
|
|
653
|
+
? z.infer<Q>
|
|
654
|
+
: never;
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Type helper to extract body schema from an endpoint
|
|
658
|
+
*/
|
|
659
|
+
export type ExtractBody<T> = T extends { body: infer B extends z.ZodType }
|
|
660
|
+
? z.infer<B>
|
|
661
|
+
: never;
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Type helper to extract response schema from an endpoint
|
|
665
|
+
*/
|
|
666
|
+
export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType }
|
|
667
|
+
? z.infer<R>
|
|
668
|
+
: never;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Import your API config to get typed endpoints
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* import { apiConfig } from './config/endpoints';
|
|
675
|
+
* export type APIEndpoints = typeof apiConfig.endpoints;
|
|
676
|
+
*/
|
|
677
|
+
export type APIEndpoints = Record<string, any>;
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
var ClientGenerator = class extends BaseGenerator {
|
|
682
|
+
async generate() {
|
|
683
|
+
await this.generateClientFile();
|
|
684
|
+
if (this.context.config.provider === "nextjs") {
|
|
685
|
+
await this.generateServerClientFile();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async generateClientFile() {
|
|
689
|
+
const content = this.generateClientContent();
|
|
690
|
+
const outputPath = path6.join(this.context.config.outputDir, "client.ts");
|
|
691
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
692
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
693
|
+
}
|
|
694
|
+
async generateServerClientFile() {
|
|
695
|
+
const content = this.generateServerClientContent();
|
|
696
|
+
const outputPath = path6.join(this.context.config.outputDir, "server-client.ts");
|
|
697
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
698
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
699
|
+
}
|
|
700
|
+
generateClientContent() {
|
|
701
|
+
const useClientDirective = this.context.config.options?.useClientDirective ?? true;
|
|
702
|
+
return `${useClientDirective ? "'use client';\n" : ""}
|
|
703
|
+
import { createAPIClient } from '@cushin/api-codegen/client';
|
|
704
|
+
import type { AuthCallbacks } from '@cushin/api-codegen/client';
|
|
705
|
+
import { apiConfig } from '../config/endpoints';
|
|
706
|
+
import type { APIEndpoints } from './types';
|
|
707
|
+
|
|
708
|
+
// Type-safe API client methods
|
|
709
|
+
type APIClientMethods = {
|
|
710
|
+
[K in keyof APIEndpoints]: APIEndpoints[K] extends {
|
|
711
|
+
method: infer M;
|
|
712
|
+
params?: infer P;
|
|
713
|
+
query?: infer Q;
|
|
714
|
+
body?: infer B;
|
|
715
|
+
response: infer R;
|
|
716
|
+
}
|
|
717
|
+
? M extends 'GET'
|
|
718
|
+
? P extends { _type: any }
|
|
719
|
+
? Q extends { _type: any }
|
|
720
|
+
? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
|
|
721
|
+
: (params: P['_type']) => Promise<R['_type']>
|
|
722
|
+
: Q extends { _type: any }
|
|
723
|
+
? (query?: Q['_type']) => Promise<R['_type']>
|
|
724
|
+
: () => Promise<R['_type']>
|
|
725
|
+
: P extends { _type: any }
|
|
726
|
+
? B extends { _type: any }
|
|
727
|
+
? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
|
|
728
|
+
: (params: P['_type']) => Promise<R['_type']>
|
|
729
|
+
: B extends { _type: any }
|
|
730
|
+
? (body: B['_type']) => Promise<R['_type']>
|
|
731
|
+
: () => Promise<R['_type']>
|
|
732
|
+
: never;
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
// Export singleton instance (will be initialized later)
|
|
736
|
+
export let apiClient: APIClientMethods & {
|
|
737
|
+
refreshAuth: () => Promise<void>;
|
|
738
|
+
updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Initialize API client with auth callbacks
|
|
743
|
+
* Call this function in your auth provider setup
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* const authCallbacks = {
|
|
747
|
+
* getTokens: () => getStoredTokens(),
|
|
748
|
+
* setTokens: (tokens) => storeTokens(tokens),
|
|
749
|
+
* clearTokens: () => clearStoredTokens(),
|
|
750
|
+
* onAuthError: () => router.push('/login'),
|
|
751
|
+
* onRefreshToken: async () => {
|
|
752
|
+
* const newToken = await refreshAccessToken();
|
|
753
|
+
* return newToken;
|
|
754
|
+
* },
|
|
755
|
+
* };
|
|
756
|
+
*
|
|
757
|
+
* initializeAPIClient(authCallbacks);
|
|
758
|
+
*/
|
|
759
|
+
export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
|
|
760
|
+
apiClient = createAPIClient(apiConfig, authCallbacks) as any;
|
|
761
|
+
return apiClient;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// Export for custom usage
|
|
765
|
+
export { createAPIClient };
|
|
766
|
+
export type { AuthCallbacks };
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
generateServerClientContent() {
|
|
770
|
+
return `import { createAPIClient } from '@cushin/api-codegen/client';
|
|
771
|
+
import { apiConfig } from '../config/endpoints';
|
|
772
|
+
import type { APIEndpoints } from './types';
|
|
773
|
+
|
|
774
|
+
// Type-safe API client methods for server-side
|
|
775
|
+
type APIClientMethods = {
|
|
776
|
+
[K in keyof APIEndpoints]: APIEndpoints[K] extends {
|
|
777
|
+
method: infer M;
|
|
778
|
+
params?: infer P;
|
|
779
|
+
query?: infer Q;
|
|
780
|
+
body?: infer B;
|
|
781
|
+
response: infer R;
|
|
782
|
+
}
|
|
783
|
+
? M extends 'GET'
|
|
784
|
+
? P extends { _type: any }
|
|
785
|
+
? Q extends { _type: any }
|
|
786
|
+
? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
|
|
787
|
+
: (params: P['_type']) => Promise<R['_type']>
|
|
788
|
+
: Q extends { _type: any }
|
|
789
|
+
? (query?: Q['_type']) => Promise<R['_type']>
|
|
790
|
+
: () => Promise<R['_type']>
|
|
791
|
+
: P extends { _type: any }
|
|
792
|
+
? B extends { _type: any }
|
|
793
|
+
? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
|
|
794
|
+
: (params: P['_type']) => Promise<R['_type']>
|
|
795
|
+
: B extends { _type: any }
|
|
796
|
+
? (body: B['_type']) => Promise<R['_type']>
|
|
797
|
+
: () => Promise<R['_type']>
|
|
798
|
+
: never;
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Server-side API client (no auth, direct API calls)
|
|
803
|
+
* Use this in Server Components, Server Actions, and Route Handlers
|
|
804
|
+
*/
|
|
805
|
+
export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
|
|
806
|
+
`;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/generators/index.ts
|
|
811
|
+
var CodeGenerator = class {
|
|
812
|
+
constructor(context) {
|
|
813
|
+
this.context = context;
|
|
814
|
+
}
|
|
815
|
+
async generate() {
|
|
816
|
+
const generators = this.getGenerators();
|
|
817
|
+
for (const generator of generators) {
|
|
818
|
+
await generator.generate();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
getGenerators() {
|
|
822
|
+
const generators = [];
|
|
823
|
+
generators.push(new TypesGenerator(this.context));
|
|
824
|
+
if (this.context.config.generateClient) {
|
|
825
|
+
generators.push(new ClientGenerator(this.context));
|
|
826
|
+
}
|
|
827
|
+
if (this.context.config.generateHooks) {
|
|
828
|
+
generators.push(new HooksGenerator(this.context));
|
|
829
|
+
}
|
|
830
|
+
if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {
|
|
831
|
+
generators.push(new ServerActionsGenerator(this.context));
|
|
832
|
+
}
|
|
833
|
+
if (this.context.config.generateServerQueries && this.context.config.provider === "nextjs") {
|
|
834
|
+
generators.push(new ServerQueriesGenerator(this.context));
|
|
835
|
+
}
|
|
836
|
+
return generators;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/core/codegen.ts
|
|
841
|
+
var CodegenCore = class {
|
|
842
|
+
constructor(config) {
|
|
843
|
+
this.config = config;
|
|
844
|
+
}
|
|
845
|
+
async execute() {
|
|
846
|
+
const apiConfig = await this.loadAPIConfig();
|
|
847
|
+
this.config.apiConfig = apiConfig;
|
|
848
|
+
const generator = new CodeGenerator({
|
|
849
|
+
config: this.config,
|
|
850
|
+
apiConfig
|
|
851
|
+
});
|
|
852
|
+
await generator.generate();
|
|
853
|
+
}
|
|
854
|
+
async loadAPIConfig() {
|
|
855
|
+
try {
|
|
856
|
+
const fileUrl = pathToFileURL(this.config.endpointsPath).href;
|
|
857
|
+
const module = await import(fileUrl);
|
|
858
|
+
const apiConfig = module.apiConfig || module.default?.apiConfig || module.default || module;
|
|
859
|
+
if (!apiConfig || !apiConfig.endpoints) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
'Invalid API config: must export an object with "endpoints" property'
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
return apiConfig;
|
|
865
|
+
} catch (error) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
`Failed to load endpoints from "${this.config.endpointsPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
export { APIError, AuthError, CodeGenerator, CodegenCore, createAPIClient, defineConfig, defineEndpoint, defineEndpoints, loadConfig, validateConfig };
|
|
874
|
+
//# sourceMappingURL=index.js.map
|
|
875
|
+
//# sourceMappingURL=index.js.map
|