nuxt-openapi-hyperfetch 0.3.81-beta → 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/README.md +218 -212
- package/dist/generators/components/connector-generator/templates.js +67 -17
- package/dist/generators/components/schema-analyzer/intent-detector.js +1 -12
- package/dist/generators/components/schema-analyzer/openapi-reader.js +10 -1
- package/dist/generators/components/schema-analyzer/resource-grouper.js +7 -0
- package/dist/generators/components/schema-analyzer/schema-field-mapper.js +1 -22
- package/dist/generators/components/schema-analyzer/types.d.ts +10 -0
- package/dist/generators/connectors/generator.d.ts +12 -0
- package/dist/generators/connectors/generator.js +115 -0
- package/dist/generators/connectors/runtime/connector-types.d.ts +147 -0
- package/dist/generators/connectors/runtime/connector-types.js +10 -0
- package/dist/generators/connectors/runtime/useCreateConnector.d.ts +26 -0
- package/dist/generators/connectors/runtime/useCreateConnector.js +156 -0
- package/dist/generators/connectors/runtime/useDeleteConnector.d.ts +30 -0
- package/dist/generators/connectors/runtime/useDeleteConnector.js +143 -0
- package/dist/generators/connectors/runtime/useGetAllConnector.d.ts +25 -0
- package/dist/generators/connectors/runtime/useGetAllConnector.js +127 -0
- package/dist/generators/connectors/runtime/useGetConnector.d.ts +15 -0
- package/dist/generators/connectors/runtime/useGetConnector.js +99 -0
- package/dist/generators/connectors/runtime/useUpdateConnector.d.ts +34 -0
- package/dist/generators/connectors/runtime/useUpdateConnector.js +211 -0
- package/dist/generators/connectors/runtime/zod-error-merger.d.ts +23 -0
- package/dist/generators/connectors/runtime/zod-error-merger.js +106 -0
- package/dist/generators/connectors/templates.d.ts +4 -0
- package/dist/generators/connectors/templates.js +376 -0
- package/dist/generators/connectors/types.d.ts +37 -0
- package/dist/generators/connectors/types.js +7 -0
- package/dist/generators/shared/runtime/useDeleteConnector.js +4 -2
- package/dist/generators/shared/runtime/useDetailConnector.d.ts +0 -1
- package/dist/generators/shared/runtime/useDetailConnector.js +9 -20
- package/dist/generators/shared/runtime/useFormConnector.js +4 -3
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +14 -5
- package/dist/generators/use-async-data/templates.js +20 -16
- package/dist/generators/use-fetch/templates.js +1 -1
- package/dist/index.js +1 -16
- package/dist/module/index.js +2 -3
- package/package.json +4 -3
- package/src/cli/prompts.ts +1 -7
- package/src/generators/components/connector-generator/templates.ts +97 -22
- package/src/generators/components/schema-analyzer/intent-detector.ts +1 -16
- package/src/generators/components/schema-analyzer/openapi-reader.ts +14 -1
- package/src/generators/components/schema-analyzer/resource-grouper.ts +9 -0
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +1 -26
- package/src/generators/components/schema-analyzer/types.ts +11 -0
- package/src/generators/connectors/generator.ts +137 -0
- package/src/generators/connectors/runtime/connector-types.ts +207 -0
- package/src/generators/connectors/runtime/useCreateConnector.ts +199 -0
- package/src/generators/connectors/runtime/useDeleteConnector.ts +179 -0
- package/src/generators/connectors/runtime/useGetAllConnector.ts +151 -0
- package/src/generators/connectors/runtime/useGetConnector.ts +120 -0
- package/src/generators/connectors/runtime/useUpdateConnector.ts +257 -0
- package/src/generators/connectors/runtime/zod-error-merger.ts +119 -0
- package/src/generators/connectors/templates.ts +481 -0
- package/src/generators/connectors/types.ts +39 -0
- package/src/generators/shared/runtime/useDeleteConnector.ts +4 -2
- package/src/generators/shared/runtime/useDetailConnector.ts +8 -19
- package/src/generators/shared/runtime/useFormConnector.ts +4 -3
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +16 -5
- package/src/generators/use-async-data/templates.ts +24 -16
- package/src/generators/use-fetch/templates.ts +1 -1
- package/src/index.ts +2 -19
- package/src/module/index.ts +2 -5
- package/docs/generated-components.md +0 -615
- package/docs/headless-composables-ui.md +0 -569
package/dist/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
|
-
import { existsSync } from 'fs';
|
|
5
|
-
import * as path from 'path';
|
|
6
4
|
import { generateOpenApiFiles, generateHeyApiFiles, checkJavaInstalled } from './generate.js';
|
|
7
5
|
import { generateUseFetchComposables } from './generators/use-fetch/generator.js';
|
|
8
6
|
import { generateUseAsyncDataComposables } from './generators/use-async-data/generator.js';
|
|
9
7
|
import { generateNuxtServerRoutes } from './generators/nuxt-server/generator.js';
|
|
10
|
-
import { generateConnectors } from './generators/
|
|
8
|
+
import { generateConnectors } from './generators/connectors/generator.js';
|
|
11
9
|
import { promptInitialInputs, promptInputPath, promptComposablesSelection, promptServerRoutePath, promptBffConfig, promptGeneratorBackend, promptConnectors, } from './cli/prompts.js';
|
|
12
10
|
import { MESSAGES } from './cli/messages.js';
|
|
13
11
|
import { displayLogo } from './cli/logo.js';
|
|
@@ -77,7 +75,6 @@ program
|
|
|
77
75
|
: config.generator === 'heyapi'
|
|
78
76
|
? 'heyapi'
|
|
79
77
|
: config.backend;
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
81
78
|
const backend = await promptGeneratorBackend(resolvedBackend);
|
|
82
79
|
// Check Java availability when official backend is selected
|
|
83
80
|
if (backend === 'official' && !checkJavaInstalled()) {
|
|
@@ -158,7 +155,6 @@ program
|
|
|
158
155
|
for (const composable of composables) {
|
|
159
156
|
const spinner = p.spinner();
|
|
160
157
|
spinner.start(`Generating ${composable}...`);
|
|
161
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
162
158
|
const generateOptions = { baseUrl: config.baseUrl, backend };
|
|
163
159
|
try {
|
|
164
160
|
switch (composable) {
|
|
@@ -182,7 +178,6 @@ program
|
|
|
182
178
|
break;
|
|
183
179
|
case 'nuxtServer':
|
|
184
180
|
if (!config.dryRun) {
|
|
185
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
186
181
|
await generateNuxtServerRoutes(outputPath, serverRoutePath, { enableBff, backend });
|
|
187
182
|
spinner.stop(`✓ Generated Nuxt server routes`);
|
|
188
183
|
}
|
|
@@ -198,16 +193,6 @@ program
|
|
|
198
193
|
}
|
|
199
194
|
}
|
|
200
195
|
// Generate headless connectors if requested (requires useAsyncData)
|
|
201
|
-
if (generateConnectorsFlag) {
|
|
202
|
-
// Check zod is available in the user's project by probing their node_modules,
|
|
203
|
-
// not via import() which resolves from the CLI's own context (npx sandbox).
|
|
204
|
-
const zodPath = path.resolve(process.cwd(), 'node_modules', 'zod');
|
|
205
|
-
if (!existsSync(zodPath)) {
|
|
206
|
-
p.log.warn('Skipping connectors: "zod" is not installed in this project.\n' +
|
|
207
|
-
' Run: npm install zod');
|
|
208
|
-
generateConnectorsFlag = false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
196
|
if (generateConnectorsFlag) {
|
|
212
197
|
const spinner = p.spinner();
|
|
213
198
|
spinner.start('Generating headless UI connectors...');
|
package/dist/module/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { checkJavaInstalled } from '../generate.js';
|
|
|
5
5
|
import { generateUseFetchComposables } from '../generators/use-fetch/generator.js';
|
|
6
6
|
import { generateUseAsyncDataComposables } from '../generators/use-async-data/generator.js';
|
|
7
7
|
import { generateNuxtServerRoutes } from '../generators/nuxt-server/generator.js';
|
|
8
|
-
import { generateConnectors } from '../generators/
|
|
8
|
+
import { generateConnectors } from '../generators/connectors/generator.js';
|
|
9
9
|
import { createConsoleLogger } from '../cli/logger.js';
|
|
10
10
|
export default defineNuxtModule({
|
|
11
11
|
meta: {
|
|
@@ -67,8 +67,7 @@ export default defineNuxtModule({
|
|
|
67
67
|
await generateNuxtServerRoutes(resolvedOutput, serverRoutePath, { enableBff: options.enableBff, backend }, logger);
|
|
68
68
|
}
|
|
69
69
|
// 3. Generate headless connectors if requested (requires useAsyncData)
|
|
70
|
-
if (options.createUseAsyncDataConnectors &&
|
|
71
|
-
selectedGenerators.includes('useAsyncData')) {
|
|
70
|
+
if (options.createUseAsyncDataConnectors && selectedGenerators.includes('useAsyncData')) {
|
|
72
71
|
const connectorsOutputDir = path.join(composablesOutputDir, 'connectors');
|
|
73
72
|
await generateConnectors({
|
|
74
73
|
inputSpec: resolvedInput,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-openapi-hyperfetch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nuxt useFetch, useAsyncData
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Nuxt useFetch, useAsyncData, Nuxt server & Headless UI connectors OpenAPI generator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"eslint-plugin-prettier": "^5.5.5",
|
|
75
75
|
"ts-node": "^10.9.2",
|
|
76
76
|
"typescript": "^5.9.3",
|
|
77
|
-
"typescript-eslint": "^8.57.1"
|
|
77
|
+
"typescript-eslint": "^8.57.1",
|
|
78
|
+
"zod": "^4.3.6"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
80
81
|
"@clack/prompts": "^1.1.0",
|
package/src/cli/prompts.ts
CHANGED
|
@@ -5,13 +5,7 @@
|
|
|
5
5
|
import * as p from '@clack/prompts';
|
|
6
6
|
import { checkCancellation, validateNonEmpty } from './utils.js';
|
|
7
7
|
import { MESSAGES, CHOICES, DEFAULTS } from './messages.js';
|
|
8
|
-
import type {
|
|
9
|
-
InitialInputs,
|
|
10
|
-
ComposablesSelection,
|
|
11
|
-
BffConfig,
|
|
12
|
-
ComposableType,
|
|
13
|
-
GeneratorBackend,
|
|
14
|
-
} from './types.js';
|
|
8
|
+
import type { InitialInputs, ComposablesSelection, BffConfig, GeneratorBackend } from './types.js';
|
|
15
9
|
|
|
16
10
|
/**
|
|
17
11
|
* Ask which OpenAPI generator backend to use
|
|
@@ -29,8 +29,8 @@ function toAsyncDataName(operationId: string): string {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* composable name →
|
|
33
|
-
* 'useAsyncDataGetPets' → '
|
|
32
|
+
* composable name → kebab-case file name (without .ts).
|
|
33
|
+
* 'useAsyncDataGetPets' → 'use-async-data-get-pets'
|
|
34
34
|
*/
|
|
35
35
|
function toFileName(composableName: string): string {
|
|
36
36
|
return composableName;
|
|
@@ -41,19 +41,32 @@ function toFileName(composableName: string): string {
|
|
|
41
41
|
/**
|
|
42
42
|
* Build all `import` lines for a resource connector.
|
|
43
43
|
*/
|
|
44
|
-
function buildImports(
|
|
44
|
+
function buildImports(
|
|
45
|
+
resource: ResourceInfo,
|
|
46
|
+
composablesRelDir: string,
|
|
47
|
+
sdkRelDir: string,
|
|
48
|
+
runtimeRelDir: string
|
|
49
|
+
): string {
|
|
45
50
|
const lines: string[] = [];
|
|
46
51
|
|
|
52
|
+
// vue — shallowRef needed when form, delete, or detail endpoints exist
|
|
53
|
+
const needsShallowRef = !!(
|
|
54
|
+
resource.createEndpoint ||
|
|
55
|
+
resource.updateEndpoint ||
|
|
56
|
+
resource.deleteEndpoint ||
|
|
57
|
+
resource.detailEndpoint
|
|
58
|
+
);
|
|
59
|
+
const needsComputed = !!resource.detailEndpoint;
|
|
60
|
+
if (needsShallowRef) {
|
|
61
|
+
const vueImports = needsComputed ? `shallowRef, computed` : `shallowRef`;
|
|
62
|
+
lines.push(`import { ${vueImports} } from 'vue';`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
// zod
|
|
48
67
|
lines.push(`import { z } from 'zod';`);
|
|
49
68
|
lines.push('');
|
|
50
69
|
|
|
51
|
-
// Vue — computed needed for the detail connector wrapper
|
|
52
|
-
if (resource.detailEndpoint) {
|
|
53
|
-
lines.push(`import { computed } from 'vue';`);
|
|
54
|
-
lines.push('');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
70
|
// connector-types — structural interfaces for return types
|
|
58
71
|
const connectorTypeImports: string[] = ['ListConnectorReturn'];
|
|
59
72
|
if (resource.detailEndpoint) {
|
|
@@ -65,7 +78,9 @@ function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelD
|
|
|
65
78
|
if (resource.deleteEndpoint) {
|
|
66
79
|
connectorTypeImports.push('DeleteConnectorReturn');
|
|
67
80
|
}
|
|
68
|
-
lines.push(
|
|
81
|
+
lines.push(
|
|
82
|
+
`import type { ${connectorTypeImports.join(', ')} } from '${runtimeRelDir}/connector-types';`
|
|
83
|
+
);
|
|
69
84
|
lines.push('');
|
|
70
85
|
|
|
71
86
|
// SDK request/response types (for the params overload signature)
|
|
@@ -184,6 +199,12 @@ function buildOptionsInterface(resource: ResourceInfo): string {
|
|
|
184
199
|
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
185
200
|
fields.push(` updateSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
|
|
186
201
|
}
|
|
202
|
+
if (resource.createEndpoint || resource.updateEndpoint || resource.deleteEndpoint) {
|
|
203
|
+
fields.push(` onRequest?: (ctx: any) => void | Promise<void>;`);
|
|
204
|
+
fields.push(` onSuccess?: (data: any) => void;`);
|
|
205
|
+
fields.push(` onError?: (err: any) => void;`);
|
|
206
|
+
fields.push(` onFinish?: () => void;`);
|
|
207
|
+
}
|
|
187
208
|
|
|
188
209
|
if (fields.length === 0) {
|
|
189
210
|
return `type ${typeName} = Record<string, never>;`;
|
|
@@ -213,11 +234,15 @@ function buildReturnType(resource: ResourceInfo): string {
|
|
|
213
234
|
fields.push(` detail: DetailConnectorReturn<${pascal}>;`);
|
|
214
235
|
}
|
|
215
236
|
if (resource.createEndpoint) {
|
|
216
|
-
const inputType = resource.zodSchemas.create
|
|
237
|
+
const inputType = resource.zodSchemas.create
|
|
238
|
+
? `${pascal}CreateInput`
|
|
239
|
+
: `Record<string, unknown>`;
|
|
217
240
|
fields.push(` createForm: FormConnectorReturn<${inputType}>;`);
|
|
218
241
|
}
|
|
219
242
|
if (resource.updateEndpoint) {
|
|
220
|
-
const inputType = resource.zodSchemas.update
|
|
243
|
+
const inputType = resource.zodSchemas.update
|
|
244
|
+
? `${pascal}UpdateInput`
|
|
245
|
+
: `Record<string, unknown>`;
|
|
221
246
|
fields.push(` updateForm: FormConnectorReturn<${inputType}>;`);
|
|
222
247
|
}
|
|
223
248
|
if (resource.deleteEndpoint) {
|
|
@@ -227,6 +252,18 @@ function buildReturnType(resource: ResourceInfo): string {
|
|
|
227
252
|
return [`type ${typeName} = {`, ...fields, `};`].join('\n');
|
|
228
253
|
}
|
|
229
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Build the 3 generated lines for a composable that is keyed by a single path param
|
|
257
|
+
* (detail, delete). Uses computed(() => ({ param: ref.value })) so that p.value is
|
|
258
|
+
* always an object during setup — avoids `null.param` crash when Nuxt evaluates computedKey.
|
|
259
|
+
*/
|
|
260
|
+
function buildPathParamComposableLines(prefix: string, fn: string, pathParam: string): string[] {
|
|
261
|
+
return [
|
|
262
|
+
` const ${prefix}Ref = shallowRef(null);`,
|
|
263
|
+
` const ${prefix}Composable = ${fn}(computed(() => ({ ${pathParam}: ${prefix}Ref.value })) as any, { immediate: false });`,
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
|
|
230
267
|
/**
|
|
231
268
|
* Build the body of the exported connector function.
|
|
232
269
|
*/
|
|
@@ -255,6 +292,14 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
255
292
|
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
256
293
|
optionKeys.push('updateSchema');
|
|
257
294
|
}
|
|
295
|
+
const hasMutations = !!(
|
|
296
|
+
resource.createEndpoint ||
|
|
297
|
+
resource.updateEndpoint ||
|
|
298
|
+
resource.deleteEndpoint
|
|
299
|
+
);
|
|
300
|
+
if (hasMutations) {
|
|
301
|
+
optionKeys.push('onRequest', 'onSuccess', 'onError', 'onFinish');
|
|
302
|
+
}
|
|
258
303
|
|
|
259
304
|
const optionsDestructure =
|
|
260
305
|
optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
|
|
@@ -289,29 +334,36 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
289
334
|
|
|
290
335
|
if (resource.detailEndpoint) {
|
|
291
336
|
const fn = toAsyncDataName(resource.detailEndpoint.operationId);
|
|
292
|
-
// Wrap the composable to map the generic idRef to the actual path param name.
|
|
293
|
-
// useDetailConnector passes a ref<id> and calls refresh() after updating it.
|
|
294
337
|
const pathParam = resource.detailEndpoint.pathParams[0] ?? 'id';
|
|
338
|
+
subConnectors.push(...buildPathParamComposableLines('_detail', fn, pathParam));
|
|
295
339
|
subConnectors.push(
|
|
296
|
-
` const detail = useDetailConnector(
|
|
297
|
-
` (idRef, opts) => ${fn}(computed(() => ({ ${pathParam}: idRef.value })) as any, opts),`,
|
|
298
|
-
` ) as unknown as DetailConnectorReturn<${pascal}>;`
|
|
340
|
+
` const detail = useDetailConnector((id: any) => { _detailRef.value = id; return _detailComposable; }) as unknown as DetailConnectorReturn<${pascal}>;`
|
|
299
341
|
);
|
|
300
342
|
}
|
|
301
343
|
|
|
302
344
|
if (resource.createEndpoint) {
|
|
303
345
|
const fn = toAsyncDataName(resource.createEndpoint.operationId);
|
|
304
|
-
const inputType = resource.zodSchemas.create
|
|
346
|
+
const inputType = resource.zodSchemas.create
|
|
347
|
+
? `${pascal}CreateInput`
|
|
348
|
+
: `Record<string, unknown>`;
|
|
305
349
|
const schemaArg = resource.zodSchemas.create
|
|
306
350
|
? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
|
|
307
351
|
: '{}';
|
|
308
|
-
subConnectors.push(` const
|
|
352
|
+
subConnectors.push(` const _createRef = shallowRef({});`);
|
|
353
|
+
subConnectors.push(
|
|
354
|
+
` const _createComposable = ${fn}(_createRef as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`
|
|
355
|
+
);
|
|
356
|
+
subConnectors.push(
|
|
357
|
+
` const createForm = useFormConnector((p: any) => { _createRef.value = p; return _createComposable; }, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`
|
|
358
|
+
);
|
|
309
359
|
}
|
|
310
360
|
|
|
311
361
|
if (resource.updateEndpoint) {
|
|
312
362
|
const fn = toAsyncDataName(resource.updateEndpoint.operationId);
|
|
313
363
|
const hasDetail = !!resource.detailEndpoint;
|
|
314
|
-
const inputType = resource.zodSchemas.update
|
|
364
|
+
const inputType = resource.zodSchemas.update
|
|
365
|
+
? `${pascal}UpdateInput`
|
|
366
|
+
: `Record<string, unknown>`;
|
|
315
367
|
|
|
316
368
|
let schemaArg = '{}';
|
|
317
369
|
if (resource.zodSchemas.update && hasDetail) {
|
|
@@ -321,12 +373,35 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
321
373
|
} else if (hasDetail) {
|
|
322
374
|
schemaArg = `{ loadWith: detail }`;
|
|
323
375
|
}
|
|
324
|
-
subConnectors.push(` const
|
|
376
|
+
subConnectors.push(` const _updateRef = shallowRef({});`);
|
|
377
|
+
subConnectors.push(
|
|
378
|
+
` const _updateComposable = ${fn}(_updateRef as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`
|
|
379
|
+
);
|
|
380
|
+
subConnectors.push(
|
|
381
|
+
` const updateForm = useFormConnector((p: any) => { _updateRef.value = p; return _updateComposable; }, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`
|
|
382
|
+
);
|
|
325
383
|
}
|
|
326
384
|
|
|
327
385
|
if (resource.deleteEndpoint) {
|
|
328
386
|
const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
|
|
329
|
-
|
|
387
|
+
const pathParam = resource.deleteEndpoint.pathParams[0];
|
|
388
|
+
if (pathParam) {
|
|
389
|
+
subConnectors.push(...buildPathParamComposableLines('_delete', fn, pathParam));
|
|
390
|
+
subConnectors.push(
|
|
391
|
+
` const _deleteComposableWithHooks = ${fn}(computed(() => ({ ${pathParam}: _deleteRef.value })) as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`
|
|
392
|
+
);
|
|
393
|
+
subConnectors.push(
|
|
394
|
+
` const deleteAction = useDeleteConnector((item: any) => { _deleteRef.value = item?.${pathParam} ?? item?.id ?? item; return _deleteComposableWithHooks; }) as unknown as DeleteConnectorReturn<${pascal}>;`
|
|
395
|
+
);
|
|
396
|
+
} else {
|
|
397
|
+
subConnectors.push(` const _deleteRef = shallowRef({});`);
|
|
398
|
+
subConnectors.push(
|
|
399
|
+
` const _deleteComposable = ${fn}(_deleteRef as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`
|
|
400
|
+
);
|
|
401
|
+
subConnectors.push(
|
|
402
|
+
` const deleteAction = useDeleteConnector((p: any) => { _deleteRef.value = p; return _deleteComposable; }) as unknown as DeleteConnectorReturn<${pascal}>;`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
330
405
|
}
|
|
331
406
|
|
|
332
407
|
// Return object — always includes table (undefined when no list + no factory)
|
|
@@ -49,16 +49,6 @@ function isArraySchema(schema: OpenApiPropertySchema): boolean {
|
|
|
49
49
|
return schema.type === 'array' || schema.items !== undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** True when schema is a primitive scalar (string, number, integer, boolean) — not a resource */
|
|
53
|
-
function isPrimitiveSchema(schema: OpenApiPropertySchema): boolean {
|
|
54
|
-
return (
|
|
55
|
-
schema.type === 'string' ||
|
|
56
|
-
schema.type === 'number' ||
|
|
57
|
-
schema.type === 'integer' ||
|
|
58
|
-
schema.type === 'boolean'
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
52
|
// ─── Request body schema ──────────────────────────────────────────────────────
|
|
63
53
|
|
|
64
54
|
function getRequestBodySchema(operation: OpenApiOperation): OpenApiPropertySchema | undefined {
|
|
@@ -123,12 +113,6 @@ export function detectIntent(
|
|
|
123
113
|
return 'list';
|
|
124
114
|
}
|
|
125
115
|
|
|
126
|
-
// Primitive response (string, number, boolean) → not a CRUD resource
|
|
127
|
-
// e.g. GET /user/login returns a string token — not a list or detail
|
|
128
|
-
if (isPrimitiveSchema(responseSchema)) {
|
|
129
|
-
return 'unknown';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
116
|
// Object response — distinguish list vs detail by path structure:
|
|
133
117
|
// GET /pets/{id} → has path param → detail (single item fetch)
|
|
134
118
|
// GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
|
|
@@ -178,6 +162,7 @@ export function extractEndpoints(
|
|
|
178
162
|
intent,
|
|
179
163
|
hasPathParams: pathParams.length > 0,
|
|
180
164
|
pathParams,
|
|
165
|
+
hasQueryParams: (operation.parameters ?? []).some((p) => p.in === 'query'),
|
|
181
166
|
};
|
|
182
167
|
|
|
183
168
|
// Attach response schema for GET intents
|
|
@@ -51,7 +51,20 @@ function resolveRefs(node: unknown, root: OpenApiSpec, visited = new Set<string>
|
|
|
51
51
|
const resolved = resolvePointer(root, ref);
|
|
52
52
|
const newVisited = new Set(visited);
|
|
53
53
|
newVisited.add(ref);
|
|
54
|
-
|
|
54
|
+
const resolvedFull = resolveRefs(resolved, root, newVisited);
|
|
55
|
+
|
|
56
|
+
// Annotate resolved schemas from #/components/schemas/Xxx with the original
|
|
57
|
+
// component name so downstream consumers can recover the model type name.
|
|
58
|
+
if (
|
|
59
|
+
typeof resolvedFull === 'object' &&
|
|
60
|
+
resolvedFull !== null &&
|
|
61
|
+
ref.startsWith('#/components/schemas/')
|
|
62
|
+
) {
|
|
63
|
+
const refName = ref.split('/').pop()!;
|
|
64
|
+
return { ...resolvedFull, 'x-ref-name': refName };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return resolvedFull;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
const result: Record<string, unknown> = {};
|
|
@@ -112,10 +112,19 @@ export function buildResourceMap(spec: OpenApiSpec): ResourceMap {
|
|
|
112
112
|
|
|
113
113
|
const resourceName = pascalCase(tag);
|
|
114
114
|
|
|
115
|
+
// Infer the SDK model type name from the original $ref component name.
|
|
116
|
+
// Priority: detail response > list items > list response (may be envelope object).
|
|
117
|
+
const itemTypeName =
|
|
118
|
+
(detailEp?.responseSchema as any)?.['x-ref-name'] ??
|
|
119
|
+
(listEp?.responseSchema as any)?.items?.['x-ref-name'] ??
|
|
120
|
+
(listEp?.responseSchema as any)?.['x-ref-name'] ??
|
|
121
|
+
undefined;
|
|
122
|
+
|
|
115
123
|
const info: ResourceInfo = {
|
|
116
124
|
name: resourceName,
|
|
117
125
|
tag,
|
|
118
126
|
composableName: toConnectorName(tag),
|
|
127
|
+
itemTypeName,
|
|
119
128
|
endpoints,
|
|
120
129
|
listEndpoint: listEp,
|
|
121
130
|
detailEndpoint: detailEp,
|
|
@@ -167,7 +167,7 @@ function baseZodExpr(prop: OpenApiPropertySchema): string {
|
|
|
167
167
|
return arrayZodExpr(prop);
|
|
168
168
|
|
|
169
169
|
case 'object':
|
|
170
|
-
return
|
|
170
|
+
return 'z.record(z.string(), z.unknown())';
|
|
171
171
|
|
|
172
172
|
default:
|
|
173
173
|
// $ref already resolved, unknown type → permissive
|
|
@@ -224,31 +224,6 @@ function numberZodExpr(prop: OpenApiPropertySchema): string {
|
|
|
224
224
|
return expr;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
function objectZodExpr(prop: OpenApiPropertySchema): string {
|
|
228
|
-
const { additionalProperties } = prop;
|
|
229
|
-
|
|
230
|
-
// additionalProperties: false or undefined → plain object
|
|
231
|
-
if (!additionalProperties || additionalProperties === true) {
|
|
232
|
-
return 'z.record(z.unknown())';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// additionalProperties has a known primitive type → typed record
|
|
236
|
-
const valueExpr = additionalPropsZodExpr(additionalProperties);
|
|
237
|
-
return `z.record(${valueExpr})`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function additionalPropsZodExpr(schema: OpenApiPropertySchema): string {
|
|
241
|
-
switch (schema.type) {
|
|
242
|
-
case 'string': return stringZodExpr(schema);
|
|
243
|
-
case 'integer': return integerZodExpr(schema);
|
|
244
|
-
case 'number': return numberZodExpr(schema);
|
|
245
|
-
case 'boolean': return 'z.boolean()';
|
|
246
|
-
case 'array': return arrayZodExpr(schema);
|
|
247
|
-
case 'object': return objectZodExpr(schema);
|
|
248
|
-
default: return 'z.unknown()';
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
227
|
function arrayZodExpr(prop: OpenApiPropertySchema): string {
|
|
253
228
|
const itemExpr = prop.items ? baseZodExpr(prop.items) : 'z.unknown()';
|
|
254
229
|
let expr = `z.array(${itemExpr})`;
|
|
@@ -33,6 +33,8 @@ export interface OpenApiPropertySchema {
|
|
|
33
33
|
allOf?: OpenApiPropertySchema[];
|
|
34
34
|
oneOf?: OpenApiPropertySchema[];
|
|
35
35
|
anyOf?: OpenApiPropertySchema[];
|
|
36
|
+
/** Injected by the $ref resolver — original component schema name, e.g. 'Pet' */
|
|
37
|
+
'x-ref-name'?: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface OpenApiSchema extends OpenApiPropertySchema {
|
|
@@ -103,6 +105,8 @@ export interface EndpointInfo {
|
|
|
103
105
|
responseSchema?: OpenApiSchema;
|
|
104
106
|
hasPathParams: boolean;
|
|
105
107
|
pathParams: string[];
|
|
108
|
+
/** True when the operation has at least one query parameter */
|
|
109
|
+
hasQueryParams: boolean;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
// ─── Form field definition ───────────────────────────────────────────────────
|
|
@@ -154,6 +158,13 @@ export interface ResourceInfo {
|
|
|
154
158
|
/** The one endpoint detected as delete (DELETE) */
|
|
155
159
|
deleteEndpoint?: EndpointInfo;
|
|
156
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Inferred item model type name (e.g. 'Pet', 'Order') derived from the
|
|
163
|
+
* response schema's original $ref component name. Used for SDK type imports.
|
|
164
|
+
* Undefined when the response type is anonymous/primitive.
|
|
165
|
+
*/
|
|
166
|
+
itemTypeName?: string;
|
|
167
|
+
|
|
157
168
|
/** Columns inferred from the list/detail response schema */
|
|
158
169
|
columns: ColumnDef[];
|
|
159
170
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { format } from 'prettier';
|
|
5
|
+
import { analyzeSpec } from '../components/schema-analyzer/index.js';
|
|
6
|
+
import {
|
|
7
|
+
generateConnectorFile,
|
|
8
|
+
connectorFileName,
|
|
9
|
+
generateConnectorIndexFile,
|
|
10
|
+
} from './templates.js';
|
|
11
|
+
import type { ConnectorGeneratorOptions } from './types.js';
|
|
12
|
+
import { type Logger, createClackLogger } from '../../cli/logger.js';
|
|
13
|
+
|
|
14
|
+
// Runtime files that must be copied to the user's project
|
|
15
|
+
const RUNTIME_FILES = [
|
|
16
|
+
'connector-types.ts',
|
|
17
|
+
'useGetAllConnector.ts',
|
|
18
|
+
'useGetConnector.ts',
|
|
19
|
+
'useCreateConnector.ts',
|
|
20
|
+
'useUpdateConnector.ts',
|
|
21
|
+
'useDeleteConnector.ts',
|
|
22
|
+
'zod-error-merger.ts',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format TypeScript source with Prettier.
|
|
27
|
+
* Falls back to unformatted code on error.
|
|
28
|
+
*/
|
|
29
|
+
async function formatCode(code: string, logger: Logger): Promise<string> {
|
|
30
|
+
try {
|
|
31
|
+
return await format(code, { parser: 'typescript' });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.log.warn(`Prettier formatting failed: ${String(error)}`);
|
|
34
|
+
return code;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate headless connector composables from an OpenAPI spec.
|
|
40
|
+
*
|
|
41
|
+
* Steps:
|
|
42
|
+
* 1. Analyze the spec → ResourceMap (Schema Analyzer)
|
|
43
|
+
* 2. For each resource: generate connector source, format, write
|
|
44
|
+
* 3. Write an index barrel file
|
|
45
|
+
* 4. Copy runtime helpers to the user's project
|
|
46
|
+
*/
|
|
47
|
+
export async function generateConnectors(
|
|
48
|
+
options: ConnectorGeneratorOptions,
|
|
49
|
+
logger: Logger = createClackLogger()
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const spinner = logger.spinner();
|
|
52
|
+
|
|
53
|
+
const outputDir = path.resolve(options.outputDir);
|
|
54
|
+
const composablesRelDir = options.composablesRelDir ?? '../use-async-data';
|
|
55
|
+
const runtimeRelDir = options.runtimeRelDir ?? '../runtime';
|
|
56
|
+
|
|
57
|
+
// ── 1. Analyze spec ───────────────────────────────────────────────────────
|
|
58
|
+
spinner.start('Analyzing OpenAPI spec');
|
|
59
|
+
const resourceMap = analyzeSpec(options.inputSpec);
|
|
60
|
+
spinner.stop(`Found ${resourceMap.size} resource(s)`);
|
|
61
|
+
|
|
62
|
+
if (resourceMap.size === 0) {
|
|
63
|
+
logger.log.warn('No resources found in spec — nothing to generate');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── 2. Prepare output directory ───────────────────────────────────────────
|
|
68
|
+
// emptyDir (not ensureDir) so stale connectors from previous runs are removed.
|
|
69
|
+
spinner.start('Preparing output directory');
|
|
70
|
+
await fs.emptyDir(outputDir);
|
|
71
|
+
spinner.stop('Output directory ready');
|
|
72
|
+
|
|
73
|
+
// ── 3. Generate connector files ───────────────────────────────────────────
|
|
74
|
+
spinner.start('Generating connector composables');
|
|
75
|
+
let successCount = 0;
|
|
76
|
+
let errorCount = 0;
|
|
77
|
+
const generatedNames: string[] = [];
|
|
78
|
+
|
|
79
|
+
for (const resource of resourceMap.values()) {
|
|
80
|
+
try {
|
|
81
|
+
const code = generateConnectorFile(resource, composablesRelDir, '../..', runtimeRelDir);
|
|
82
|
+
const formatted = await formatCode(code, logger);
|
|
83
|
+
const fileName = connectorFileName(resource.composableName);
|
|
84
|
+
const filePath = path.join(outputDir, fileName);
|
|
85
|
+
|
|
86
|
+
await fs.writeFile(filePath, formatted, 'utf-8');
|
|
87
|
+
generatedNames.push(resource.composableName);
|
|
88
|
+
successCount++;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.log.error(`Error generating ${resource.composableName}: ${String(error)}`);
|
|
91
|
+
errorCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
spinner.stop(`Generated ${successCount} connector(s)`);
|
|
96
|
+
|
|
97
|
+
// ── 4. Write barrel index ─────────────────────────────────────────────────
|
|
98
|
+
if (generatedNames.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
const indexCode = generateConnectorIndexFile(generatedNames);
|
|
101
|
+
const formattedIndex = await formatCode(indexCode, logger);
|
|
102
|
+
await fs.writeFile(path.join(outputDir, 'index.ts'), formattedIndex, 'utf-8');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.log.warn(`Could not write connector index: ${String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── 5. Copy runtime helpers ───────────────────────────────────────────────
|
|
109
|
+
// Runtime files live in src/ and must be physical .ts files in the user's project
|
|
110
|
+
// so Nuxt/Vite can type-check them.
|
|
111
|
+
//
|
|
112
|
+
// Path resolution trick:
|
|
113
|
+
// • During development (ts-node / tsx): __dirname ≈ src/generators/connectors/
|
|
114
|
+
// • After `tsc` build: __dirname ≈ dist/generators/connectors/
|
|
115
|
+
//
|
|
116
|
+
// In both cases we step up 3 levels and re-enter src/ to find the runtime sources.
|
|
117
|
+
spinner.start('Copying runtime files');
|
|
118
|
+
const runtimeDir = path.resolve(outputDir, runtimeRelDir);
|
|
119
|
+
await fs.ensureDir(runtimeDir); // ensureDir — other runtime files may live here too
|
|
120
|
+
|
|
121
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
122
|
+
const runtimeSrcDir = path.resolve(__dirname, '../../../src/generators/connectors/runtime');
|
|
123
|
+
|
|
124
|
+
for (const file of RUNTIME_FILES) {
|
|
125
|
+
const src = path.join(runtimeSrcDir, file);
|
|
126
|
+
const dest = path.join(runtimeDir, file);
|
|
127
|
+
await fs.copyFile(src, dest);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
spinner.stop('Runtime files copied');
|
|
131
|
+
|
|
132
|
+
// ── 6. Summary ────────────────────────────────────────────────────────────
|
|
133
|
+
if (errorCount > 0) {
|
|
134
|
+
logger.log.warn(`Completed with ${errorCount} error(s)`);
|
|
135
|
+
}
|
|
136
|
+
logger.log.success(`Generated ${successCount} connector(s) in ${outputDir}`);
|
|
137
|
+
}
|