nuxt-openapi-hyperfetch 0.3.8-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.
Files changed (64) hide show
  1. package/README.md +218 -212
  2. package/dist/generators/components/connector-generator/templates.js +67 -17
  3. package/dist/generators/components/schema-analyzer/intent-detector.js +1 -12
  4. package/dist/generators/components/schema-analyzer/openapi-reader.js +10 -1
  5. package/dist/generators/components/schema-analyzer/resource-grouper.js +7 -0
  6. package/dist/generators/components/schema-analyzer/schema-field-mapper.js +1 -22
  7. package/dist/generators/components/schema-analyzer/types.d.ts +10 -0
  8. package/dist/generators/connectors/generator.d.ts +12 -0
  9. package/dist/generators/connectors/generator.js +115 -0
  10. package/dist/generators/connectors/runtime/connector-types.d.ts +147 -0
  11. package/dist/generators/connectors/runtime/connector-types.js +10 -0
  12. package/dist/generators/connectors/runtime/useCreateConnector.d.ts +26 -0
  13. package/dist/generators/connectors/runtime/useCreateConnector.js +156 -0
  14. package/dist/generators/connectors/runtime/useDeleteConnector.d.ts +30 -0
  15. package/dist/generators/connectors/runtime/useDeleteConnector.js +143 -0
  16. package/dist/generators/connectors/runtime/useGetAllConnector.d.ts +25 -0
  17. package/dist/generators/connectors/runtime/useGetAllConnector.js +127 -0
  18. package/dist/generators/connectors/runtime/useGetConnector.d.ts +15 -0
  19. package/dist/generators/connectors/runtime/useGetConnector.js +99 -0
  20. package/dist/generators/connectors/runtime/useUpdateConnector.d.ts +34 -0
  21. package/dist/generators/connectors/runtime/useUpdateConnector.js +211 -0
  22. package/dist/generators/connectors/runtime/zod-error-merger.d.ts +23 -0
  23. package/dist/generators/connectors/runtime/zod-error-merger.js +106 -0
  24. package/dist/generators/connectors/templates.d.ts +4 -0
  25. package/dist/generators/connectors/templates.js +376 -0
  26. package/dist/generators/connectors/types.d.ts +37 -0
  27. package/dist/generators/connectors/types.js +7 -0
  28. package/dist/generators/shared/runtime/useDeleteConnector.js +4 -2
  29. package/dist/generators/shared/runtime/useDetailConnector.d.ts +0 -1
  30. package/dist/generators/shared/runtime/useDetailConnector.js +9 -20
  31. package/dist/generators/shared/runtime/useFormConnector.js +4 -3
  32. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +14 -5
  33. package/dist/generators/use-async-data/templates.js +20 -16
  34. package/dist/generators/use-fetch/templates.js +1 -1
  35. package/dist/index.js +1 -16
  36. package/dist/module/index.js +2 -3
  37. package/package.json +4 -3
  38. package/src/cli/prompts.ts +1 -7
  39. package/src/generators/components/connector-generator/templates.ts +97 -22
  40. package/src/generators/components/schema-analyzer/intent-detector.ts +1 -16
  41. package/src/generators/components/schema-analyzer/openapi-reader.ts +14 -1
  42. package/src/generators/components/schema-analyzer/resource-grouper.ts +9 -0
  43. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +1 -26
  44. package/src/generators/components/schema-analyzer/types.ts +11 -0
  45. package/src/generators/connectors/generator.ts +137 -0
  46. package/src/generators/connectors/runtime/connector-types.ts +207 -0
  47. package/src/generators/connectors/runtime/useCreateConnector.ts +199 -0
  48. package/src/generators/connectors/runtime/useDeleteConnector.ts +179 -0
  49. package/src/generators/connectors/runtime/useGetAllConnector.ts +151 -0
  50. package/src/generators/connectors/runtime/useGetConnector.ts +120 -0
  51. package/src/generators/connectors/runtime/useUpdateConnector.ts +257 -0
  52. package/src/generators/connectors/runtime/zod-error-merger.ts +119 -0
  53. package/src/generators/connectors/templates.ts +481 -0
  54. package/src/generators/connectors/types.ts +39 -0
  55. package/src/generators/shared/runtime/useDeleteConnector.ts +4 -2
  56. package/src/generators/shared/runtime/useDetailConnector.ts +8 -19
  57. package/src/generators/shared/runtime/useFormConnector.ts +4 -3
  58. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +16 -5
  59. package/src/generators/use-async-data/templates.ts +24 -16
  60. package/src/generators/use-fetch/templates.ts +1 -1
  61. package/src/index.ts +2 -19
  62. package/src/module/index.ts +2 -5
  63. package/docs/generated-components.md +0 -615
  64. package/docs/headless-composables-ui.md +0 -569
@@ -0,0 +1,481 @@
1
+ import { pascalCase, kebabCase } from 'change-case';
2
+ import type { ResourceInfo } from '../components/schema-analyzer/types.js';
3
+
4
+ // ─── File header ──────────────────────────────────────────────────────────────
5
+
6
+ function generateFileHeader(): string {
7
+ return `/**
8
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
9
+ *
10
+ * This file was automatically generated by nuxt-openapi-generator.
11
+ * Any manual changes will be overwritten on the next generation.
12
+ *
13
+ * @generated by nuxt-openapi-generator
14
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
15
+ */
16
+
17
+ /* eslint-disable */
18
+ `;
19
+ }
20
+
21
+ // ─── Naming helpers ───────────────────────────────────────────────────────────
22
+
23
+ function toAsyncDataName(operationId: string): string {
24
+ return `useAsyncData${pascalCase(operationId)}`;
25
+ }
26
+
27
+ // ─── URL builder ─────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Convert an OpenAPI path template into a JS arrow function string.
31
+ * '/pet/{petId}' + 'petId' → '(id: string | number) => `/pet/${id}`'
32
+ */
33
+ function buildUrlFn(path: string, pathParam?: string): string {
34
+ if (!pathParam) {
35
+ return `'${path}'`;
36
+ }
37
+ const urlTemplate = path.replace(`{${pathParam}}`, '${id}');
38
+ return `(id: string | number) => \`${urlTemplate}\``;
39
+ }
40
+
41
+ // ─── Section builders ─────────────────────────────────────────────────────────
42
+
43
+ function buildImports(
44
+ resource: ResourceInfo,
45
+ composablesRelDir: string,
46
+ sdkRelDir: string,
47
+ runtimeRelDir: string
48
+ ): string {
49
+ const lines: string[] = [];
50
+
51
+ // zod — always needed for schema declarations
52
+ lines.push(`import { z } from 'zod';`);
53
+ lines.push('');
54
+
55
+ // connector-types — return type interfaces
56
+ const typeImports: string[] = ['GetAllConnectorReturn'];
57
+ if (resource.detailEndpoint) {
58
+ typeImports.push('GetConnectorReturn');
59
+ }
60
+ if (resource.createEndpoint) {
61
+ typeImports.push('CreateConnectorReturn');
62
+ }
63
+ if (resource.updateEndpoint) {
64
+ typeImports.push('UpdateConnectorReturn');
65
+ }
66
+ if (resource.deleteEndpoint) {
67
+ typeImports.push('DeleteConnectorReturn');
68
+ }
69
+ lines.push(`import type { ${typeImports.join(', ')} } from '${runtimeRelDir}/connector-types';`);
70
+ lines.push('');
71
+
72
+ // SDK model type -- import using the inferred item type name (from $ref) if available
73
+ const modelTypeName = resource.itemTypeName;
74
+ if (modelTypeName) {
75
+ lines.push(`import type { ${modelTypeName} } from '${sdkRelDir}';`);
76
+ lines.push('');
77
+ }
78
+
79
+ // SDK request type -- only when the list endpoint actually has query parameters
80
+ if (resource.listEndpoint && resource.listEndpoint.hasQueryParams) {
81
+ const requestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
82
+ lines.push(`import type { ${requestTypeName} } from '${sdkRelDir}';`);
83
+ lines.push('');
84
+ }
85
+
86
+ // runtime connectors
87
+ const runtimeImports: string[] = ['useGetAllConnector'];
88
+ if (resource.detailEndpoint) {
89
+ runtimeImports.push('useGetConnector');
90
+ }
91
+ if (resource.createEndpoint) {
92
+ runtimeImports.push('useCreateConnector');
93
+ }
94
+ if (resource.updateEndpoint) {
95
+ runtimeImports.push('useUpdateConnector');
96
+ }
97
+ if (resource.deleteEndpoint) {
98
+ runtimeImports.push('useDeleteConnector');
99
+ }
100
+
101
+ for (const helper of runtimeImports) {
102
+ lines.push(`import { ${helper} } from '${runtimeRelDir}/${helper}';`);
103
+ }
104
+ lines.push('');
105
+
106
+ // useAsyncData composable — only needed for getAll (list endpoint)
107
+ if (resource.listEndpoint) {
108
+ const name = toAsyncDataName(resource.listEndpoint.operationId);
109
+ lines.push(`import { ${name} } from '${composablesRelDir}/${name}';`);
110
+ lines.push('');
111
+ }
112
+
113
+ return lines.join('\n');
114
+ }
115
+
116
+ function buildZodSchemas(resource: ResourceInfo): string {
117
+ const lines: string[] = [];
118
+ const pascal = pascalCase(resource.name);
119
+
120
+ if (resource.zodSchemas.create) {
121
+ lines.push(`const ${pascal}CreateSchema = ${resource.zodSchemas.create};`);
122
+ lines.push('');
123
+ }
124
+ if (resource.zodSchemas.update) {
125
+ lines.push(`const ${pascal}UpdateSchema = ${resource.zodSchemas.update};`);
126
+ lines.push('');
127
+ }
128
+ if (resource.zodSchemas.create) {
129
+ lines.push(`type ${pascal}CreateInput = z.infer<typeof ${pascal}CreateSchema>;`);
130
+ }
131
+ if (resource.zodSchemas.update) {
132
+ lines.push(`type ${pascal}UpdateInput = z.infer<typeof ${pascal}UpdateSchema>;`);
133
+ }
134
+
135
+ return lines.join('\n');
136
+ }
137
+
138
+ function buildColumns(resource: ResourceInfo): string {
139
+ if (!resource.columns || resource.columns.length === 0) {
140
+ return '';
141
+ }
142
+
143
+ const camel = resource.composableName
144
+ .replace(/^use/, '')
145
+ .replace(/Connector$/, '')
146
+ .replace(/^./, (c) => c.toLowerCase());
147
+ const varName = `${camel}Columns`;
148
+ const entries = resource.columns
149
+ .map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
150
+ .join(',\n');
151
+ return `const ${varName} = [\n${entries},\n];`;
152
+ }
153
+
154
+ function buildFields(resource: ResourceInfo): string {
155
+ const fields = resource.formFields?.create ?? resource.formFields?.update;
156
+ if (!fields || fields.length === 0) {
157
+ return '';
158
+ }
159
+
160
+ const camel = resource.composableName
161
+ .replace(/^use/, '')
162
+ .replace(/Connector$/, '')
163
+ .replace(/^./, (c) => c.toLowerCase());
164
+ const varName = `${camel}Fields`;
165
+ const entries = fields
166
+ .map((f) => {
167
+ const opts = f.options
168
+ ? `, options: [${f.options.map((o) => `{ label: '${o.label}', value: '${o.value}' }`).join(', ')}]`
169
+ : '';
170
+ return ` { key: '${f.key}', label: '${f.label}', type: '${f.type}', required: ${f.required}${opts} }`;
171
+ })
172
+ .join(',\n');
173
+ return `const ${varName} = [\n${entries},\n];`;
174
+ }
175
+
176
+ function buildOptionsInterface(resource: ResourceInfo): string {
177
+ const typeName = `${pascalCase(resource.composableName)}Options`;
178
+ const hasColumns = resource.columns && resource.columns.length > 0;
179
+ const fields: string[] = [];
180
+
181
+ if (resource.listEndpoint && hasColumns) {
182
+ fields.push(` columnLabels?: Record<string, string>;`);
183
+ fields.push(` columnLabel?: (key: string) => string;`);
184
+ }
185
+ if (resource.createEndpoint && resource.zodSchemas.create) {
186
+ const pascal = pascalCase(resource.name);
187
+ fields.push(
188
+ ` createSchema?: z.ZodTypeAny | ((base: typeof ${pascal}CreateSchema) => z.ZodTypeAny);`
189
+ );
190
+ }
191
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
192
+ const pascal = pascalCase(resource.name);
193
+ fields.push(
194
+ ` updateSchema?: z.ZodTypeAny | ((base: typeof ${pascal}UpdateSchema) => z.ZodTypeAny);`
195
+ );
196
+ }
197
+ if (resource.createEndpoint || resource.updateEndpoint || resource.deleteEndpoint) {
198
+ fields.push(` onRequest?: (ctx: any) => void | Promise<void> | Record<string, any>;`);
199
+ fields.push(` onSuccess?: (data: any, ctx: { operation: string }) => void;`);
200
+ fields.push(` onError?: (err: any, ctx: { operation: string }) => void;`);
201
+ fields.push(` onFinish?: (ctx: any) => void;`);
202
+ fields.push(
203
+ ` skipGlobalCallbacks?: boolean | Array<'onRequest' | 'onSuccess' | 'onError' | 'onFinish'>;`
204
+ );
205
+ }
206
+ fields.push(` baseURL?: string;`);
207
+
208
+ if (fields.length === 1) {
209
+ // only baseURL — minimal interface
210
+ return [`interface ${typeName} {`, ...fields, `}`].join('\n');
211
+ }
212
+ return [`interface ${typeName} {`, ...fields, `}`].join('\n');
213
+ }
214
+
215
+ function buildReturnType(resource: ResourceInfo): string {
216
+ const pascal = resource.itemTypeName ?? pascalCase(resource.name);
217
+ const localPascal = pascalCase(resource.name);
218
+ const typeName = `${pascalCase(resource.composableName)}Return`;
219
+ const fields: string[] = [];
220
+
221
+ fields.push(` getAll: GetAllConnectorReturn<${pascal}>;`);
222
+ if (resource.detailEndpoint) {
223
+ fields.push(` get: GetConnectorReturn<${pascal}>;`);
224
+ }
225
+ if (resource.createEndpoint) {
226
+ const inputType = resource.zodSchemas.create
227
+ ? `${localPascal}CreateInput`
228
+ : `Record<string, unknown>`;
229
+ fields.push(` create: CreateConnectorReturn<${inputType}>;`);
230
+ }
231
+ if (resource.updateEndpoint) {
232
+ const inputType = resource.zodSchemas.update
233
+ ? `${localPascal}UpdateInput`
234
+ : `Record<string, unknown>`;
235
+ fields.push(` update: UpdateConnectorReturn<${inputType}>;`);
236
+ }
237
+ if (resource.deleteEndpoint) {
238
+ fields.push(` del: DeleteConnectorReturn<${pascal}>;`);
239
+ }
240
+
241
+ return [`type ${typeName} = {`, ...fields, `};`].join('\n');
242
+ }
243
+
244
+ function buildFunctionBody(resource: ResourceInfo): string {
245
+ const pascal = resource.itemTypeName ?? pascalCase(resource.name);
246
+ const localPascal = pascalCase(resource.name);
247
+ const hasColumns = resource.columns && resource.columns.length > 0;
248
+ const hasFields = !!(resource.formFields?.create?.length || resource.formFields?.update?.length);
249
+ const camel = resource.composableName
250
+ .replace(/^use/, '')
251
+ .replace(/Connector$/, '')
252
+ .replace(/^./, (c) => c.toLowerCase());
253
+
254
+ const columnsVar = `${camel}Columns`;
255
+ const fieldsVar = `${camel}Fields`;
256
+ const optionsTypeName = `${pascalCase(resource.composableName)}Options`;
257
+ const returnTypeName = `${pascalCase(resource.composableName)}Return`;
258
+ const hasMutations = !!(
259
+ resource.createEndpoint ||
260
+ resource.updateEndpoint ||
261
+ resource.deleteEndpoint
262
+ );
263
+
264
+ // Options destructure
265
+ const optionKeys: string[] = [];
266
+ if (resource.listEndpoint && hasColumns) {
267
+ optionKeys.push('columnLabels', 'columnLabel');
268
+ }
269
+ if (resource.createEndpoint && resource.zodSchemas.create) {
270
+ optionKeys.push('createSchema');
271
+ }
272
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
273
+ optionKeys.push('updateSchema');
274
+ }
275
+ if (hasMutations) {
276
+ optionKeys.push('onRequest', 'onSuccess', 'onError', 'onFinish', 'skipGlobalCallbacks');
277
+ }
278
+ optionKeys.push('baseURL');
279
+ const optionsDestructure = ` const { ${optionKeys.join(', ')} } = options;\n`;
280
+
281
+ const lines: string[] = [];
282
+
283
+ // ── Function signature ─────────────────────────────────────────────────────
284
+ if (resource.listEndpoint) {
285
+ const hasQueryParams = resource.listEndpoint.hasQueryParams;
286
+ const listRequestTypeName = hasQueryParams
287
+ ? `${pascalCase(resource.listEndpoint.operationId)}Request`
288
+ : `Record<string, never>`;
289
+ lines.push(
290
+ `export function ${resource.composableName}(source: () => unknown, options?: ${optionsTypeName}): ${returnTypeName};`,
291
+ `export function ${resource.composableName}(params?: ${listRequestTypeName}, options?: ${optionsTypeName}): ${returnTypeName};`,
292
+ `export function ${resource.composableName}(paramsOrSource?: ${listRequestTypeName} | (() => unknown), options: ${optionsTypeName} = {}): ${returnTypeName} {`
293
+ );
294
+ } else {
295
+ lines.push(
296
+ `export function ${resource.composableName}(options: ${optionsTypeName} = {}): ${returnTypeName} {`
297
+ );
298
+ lines.push(` const paramsOrSource = undefined;`);
299
+ }
300
+
301
+ lines.push(optionsDestructure.trimEnd());
302
+ lines.push('');
303
+
304
+ // ── getAll ─────────────────────────────────────────────────────────────────
305
+ if (resource.listEndpoint) {
306
+ const fn = toAsyncDataName(resource.listEndpoint.operationId);
307
+ const hasQueryParams = resource.listEndpoint.hasQueryParams;
308
+ const listRequestTypeName = hasQueryParams
309
+ ? `${pascalCase(resource.listEndpoint.operationId)}Request`
310
+ : `Record<string, never>`;
311
+ const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
312
+ const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
313
+ const allArgs = [columnsArg, labelArgs].filter(Boolean).join(', ');
314
+ const opts = allArgs ? `{ ${allArgs} }` : '{}';
315
+
316
+ lines.push(
317
+ ` const isFactory = typeof paramsOrSource === 'function';`,
318
+ ` const listFactory = isFactory`,
319
+ ` ? (paramsOrSource as () => unknown)`,
320
+ ` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName});`,
321
+ ` const getAll = useGetAllConnector(listFactory, ${opts}) as unknown as GetAllConnectorReturn<${pascal}>;`
322
+ );
323
+ } else {
324
+ lines.push(
325
+ ` const getAll = useGetAllConnector(() => ({}), {}) as unknown as GetAllConnectorReturn<${pascal}>;`
326
+ );
327
+ }
328
+ lines.push('');
329
+
330
+ // ── get ────────────────────────────────────────────────────────────────────
331
+ if (resource.detailEndpoint) {
332
+ const pathParam = resource.detailEndpoint.pathParams[0] ?? 'id';
333
+ const urlFn = buildUrlFn(resource.detailEndpoint.path, pathParam);
334
+ const fieldsArg = hasFields ? `fields: ${fieldsVar}` : '';
335
+ const args = [
336
+ 'baseURL',
337
+ 'onRequest',
338
+ 'onSuccess',
339
+ 'onError',
340
+ 'onFinish',
341
+ 'skipGlobalCallbacks',
342
+ fieldsArg,
343
+ ]
344
+ .filter(Boolean)
345
+ .join(', ');
346
+ lines.push(
347
+ ` const get = useGetConnector(${urlFn}, { ${args} }) as unknown as GetConnectorReturn<${pascal}>;`
348
+ );
349
+ lines.push('');
350
+ }
351
+
352
+ // ── create ─────────────────────────────────────────────────────────────────
353
+ if (resource.createEndpoint) {
354
+ const inputType = resource.zodSchemas.create
355
+ ? `${localPascal}CreateInput`
356
+ : `Record<string, unknown>`;
357
+ const schemaArg = resource.zodSchemas.create
358
+ ? `schema: ${localPascal}CreateSchema, schemaOverride: createSchema,`
359
+ : '';
360
+ const fieldsArg = hasFields ? `fields: ${fieldsVar},` : '';
361
+ const method = resource.createEndpoint.method;
362
+ lines.push(
363
+ ` const create = useCreateConnector('${resource.createEndpoint.path}', {`,
364
+ ` method: '${method}',`,
365
+ ...(schemaArg ? [` ${schemaArg}`] : []),
366
+ ...(fieldsArg ? [` ${fieldsArg}`] : []),
367
+ ` onRequest, onSuccess, onError, onFinish,`,
368
+ ` baseURL, skipGlobalCallbacks,`,
369
+ ` }) as unknown as CreateConnectorReturn<${inputType}>;`
370
+ );
371
+ lines.push('');
372
+ }
373
+
374
+ // ── update ─────────────────────────────────────────────────────────────────
375
+ if (resource.updateEndpoint) {
376
+ const inputType = resource.zodSchemas.update
377
+ ? `${localPascal}UpdateInput`
378
+ : `Record<string, unknown>`;
379
+ const pathParam = resource.updateEndpoint.pathParams[0]; // undefined when ID comes from body
380
+ const urlFn = buildUrlFn(resource.updateEndpoint.path, pathParam);
381
+ const schemaArg = resource.zodSchemas.update
382
+ ? `schema: ${localPascal}UpdateSchema, schemaOverride: updateSchema,`
383
+ : '';
384
+ const fieldsArg = hasFields ? `fields: ${fieldsVar},` : '';
385
+ const method = resource.updateEndpoint.method;
386
+ lines.push(
387
+ ` const update = useUpdateConnector(${urlFn}, {`,
388
+ ` method: '${method}',`,
389
+ ...(schemaArg ? [` ${schemaArg}`] : []),
390
+ ...(fieldsArg ? [` ${fieldsArg}`] : []),
391
+ ` onRequest, onSuccess, onError, onFinish,`,
392
+ ` baseURL, skipGlobalCallbacks,`,
393
+ ` }) as unknown as UpdateConnectorReturn<${inputType}>;`
394
+ );
395
+ lines.push('');
396
+ }
397
+
398
+ // ── del ────────────────────────────────────────────────────────────────────
399
+ if (resource.deleteEndpoint) {
400
+ const pathParam = resource.deleteEndpoint.pathParams[0];
401
+ const urlFn = buildUrlFn(resource.deleteEndpoint.path, pathParam);
402
+ // idFn: extract the ID from the staged item — try the path param name first, then .id
403
+ const idFn = pathParam
404
+ ? `(item: any) => item?.${pathParam} ?? item?.id ?? item`
405
+ : `(item: any) => item?.id ?? item`;
406
+ lines.push(
407
+ ` const del = useDeleteConnector(`,
408
+ ` ${idFn},`,
409
+ ` ${urlFn},`,
410
+ ` { onRequest, onSuccess, onError, onFinish, baseURL, skipGlobalCallbacks }`,
411
+ ` ) as unknown as DeleteConnectorReturn<${pascal}>;`
412
+ );
413
+ lines.push('');
414
+ }
415
+
416
+ // ── return ─────────────────────────────────────────────────────────────────
417
+ const returnKeys: string[] = ['getAll'];
418
+ if (resource.detailEndpoint) {
419
+ returnKeys.push('get');
420
+ }
421
+ if (resource.createEndpoint) {
422
+ returnKeys.push('create');
423
+ }
424
+ if (resource.updateEndpoint) {
425
+ returnKeys.push('update');
426
+ }
427
+ if (resource.deleteEndpoint) {
428
+ returnKeys.push('del');
429
+ }
430
+
431
+ lines.push(` return { ${returnKeys.join(', ')} } as ${returnTypeName};`);
432
+ lines.push(`}`);
433
+
434
+ return lines.join('\n');
435
+ }
436
+
437
+ // ─── Public API ───────────────────────────────────────────────────────────────
438
+
439
+ export function generateConnectorFile(
440
+ resource: ResourceInfo,
441
+ composablesRelDir: string,
442
+ sdkRelDir = '../..',
443
+ runtimeRelDir = '../../runtime'
444
+ ): string {
445
+ const header = generateFileHeader();
446
+ const imports = buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir);
447
+ const schemas = buildZodSchemas(resource);
448
+ const columns = buildColumns(resource);
449
+ const fields = buildFields(resource);
450
+ const optionsInterface = buildOptionsInterface(resource);
451
+ const returnType = buildReturnType(resource);
452
+ const fn = buildFunctionBody(resource);
453
+
454
+ const parts: string[] = [header, imports];
455
+ if (schemas.trim()) {
456
+ parts.push(schemas);
457
+ }
458
+ if (columns.trim()) {
459
+ parts.push(columns);
460
+ }
461
+ if (fields.trim()) {
462
+ parts.push(fields);
463
+ }
464
+ parts.push(optionsInterface);
465
+ parts.push(returnType);
466
+ parts.push(fn);
467
+
468
+ return parts.join('\n') + '\n';
469
+ }
470
+
471
+ export function connectorFileName(composableName: string): string {
472
+ return `${kebabCase(composableName)}.ts`;
473
+ }
474
+
475
+ export function generateConnectorIndexFile(composableNames: string[]): string {
476
+ const header = generateFileHeader();
477
+ const exports = composableNames
478
+ .map((name) => `export { ${name} } from './${kebabCase(name)}';`)
479
+ .join('\n');
480
+ return `${header}${exports}\n`;
481
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Types for the new Connector Generator.
3
+ *
4
+ * The Connector Generator reads the ResourceMap produced by the Schema Analyzer
5
+ * and writes one `use{Resource}Connector.ts` file per resource using $fetch for mutations.
6
+ */
7
+
8
+ export interface ConnectorGeneratorOptions {
9
+ /** Absolute or relative path to the OpenAPI YAML/JSON spec */
10
+ inputSpec: string;
11
+ /** Directory where connector files will be written. E.g. ./composables/connectors */
12
+ outputDir: string;
13
+ /**
14
+ * Directory where the useAsyncData composables live (only used for getAll/list),
15
+ * expressed as a path relative to outputDir. Defaults to '../use-async-data/composables'.
16
+ */
17
+ composablesRelDir?: string;
18
+ /**
19
+ * Directory where runtime helpers will be copied to, expressed relative to
20
+ * outputDir. Defaults to '../runtime'.
21
+ */
22
+ runtimeRelDir?: string;
23
+ /**
24
+ * Base URL for API requests. If not provided, connectors will read from
25
+ * useRuntimeConfig().public.apiBaseUrl at runtime.
26
+ */
27
+ baseUrl?: string;
28
+ }
29
+
30
+ export interface ConnectorFileInfo {
31
+ /** PascalCase resource name. E.g. 'Pet' */
32
+ resourceName: string;
33
+ /** Generated composable function name. E.g. 'usePetsConnector' */
34
+ composableName: string;
35
+ /** Output filename (kebab-case). E.g. 'use-pets-connector.ts' */
36
+ fileName: string;
37
+ /** Formatted TypeScript source ready to be written to disk */
38
+ content: string;
39
+ }
@@ -63,8 +63,9 @@ export function useDeleteConnector(composableFn, options = {}) {
63
63
  // Pass the full target item; the generated composable extracts the id it needs
64
64
  const composable = composableFn(target.value);
65
65
 
66
- if (composable.execute) {
67
- await composable.execute();
66
+ // refresh() bypasses Nuxt SSR payload cache, forcing a real network request
67
+ if (composable.refresh) {
68
+ await composable.refresh();
68
69
  }
69
70
 
70
71
  const err = composable.error?.value;
@@ -78,6 +79,7 @@ export function useDeleteConnector(composableFn, options = {}) {
78
79
 
79
80
  onSuccess.value?.(deletedItem);
80
81
  } catch (err) {
82
+ console.error('[useDeleteConnector] confirm error:', err);
81
83
  error.value = err;
82
84
  onError.value?.(err);
83
85
  } finally {
@@ -18,20 +18,11 @@ import { ref, computed } from 'vue';
18
18
  export function useDetailConnector(composableFn, options = {}) {
19
19
  const { fields = [] } = options;
20
20
 
21
- // ── Reactive ID ref passed to the composable wrapper ──────────────────────
22
- // The generated connector wraps the composable like:
23
- // (idRef, opts) => useAsyncDataGetPetById(computed(() => ({ petId: idRef.value })), opts)
24
- // This lets us update idRef.value in load(id) so refresh() picks up the new value.
25
- const idRef = ref(null);
26
-
27
- // ── Execute the underlying composable ──────────────────────────────────────
28
- // watch: false + immediate: false + lazy: true prevent any fetch until
29
- // load(id) is called explicitly. The URL is only evaluated on refresh().
30
- const composable = composableFn(idRef, {
31
- watch: false,
32
- immediate: false,
33
- lazy: true,
34
- });
21
+ // ── Execute the underlying composable lazily (only when load(id) is called) ─
22
+ // composableFn is a generated wrapper: (id) => { _idRef.value = id; return _composable }
23
+ // Calling it with null initializes the composable in setup context (safe p.value is { param: null })
24
+ // Calling it in load(id) updates the ref before refresh()
25
+ const composable = composableFn(null);
35
26
 
36
27
  // ── Derived state ──────────────────────────────────────────────────────────
37
28
 
@@ -42,13 +33,12 @@ export function useDetailConnector(composableFn, options = {}) {
42
33
  // ── Actions ────────────────────────────────────────────────────────────────
43
34
 
44
35
  async function load(id) {
45
- idRef.value = id;
36
+ composableFn(id); // updates the generated _detailIdRef
46
37
  await composable.refresh?.();
47
38
  }
48
39
 
49
40
  function clear() {
50
- idRef.value = null;
51
- if (composable.data) composable.data.value = null;
41
+ composableFn(null);
52
42
  }
53
43
 
54
44
  return {
@@ -62,8 +52,7 @@ export function useDetailConnector(composableFn, options = {}) {
62
52
  load,
63
53
  clear,
64
54
 
65
- // Expose composable for advanced use
55
+ // Expose composable for advanced use (e.g. useFormConnector loadWith)
66
56
  _composable: composable,
67
- _idRef: idRef,
68
57
  };
69
58
  }
@@ -94,9 +94,9 @@ export function useFormConnector(composableFn, options = {}) {
94
94
  // The mutation composable accepts the model as its payload
95
95
  const composable = composableFn(model.value);
96
96
 
97
- // Wait for the async data to resolve
98
- if (composable.execute) {
99
- await composable.execute();
97
+ // refresh() bypasses Nuxt SSR payload cache, forcing a real network request
98
+ if (composable.refresh) {
99
+ await composable.refresh();
100
100
  }
101
101
 
102
102
  const data = composable.data?.value;
@@ -108,6 +108,7 @@ export function useFormConnector(composableFn, options = {}) {
108
108
 
109
109
  onSuccess.value?.(data);
110
110
  } catch (err) {
111
+ console.error('[useFormConnector] submit error:', err);
111
112
  submitError.value = err;
112
113
  onError.value?.(err);
113
114
  } finally {
@@ -112,7 +112,7 @@ export function useApiAsyncData<
112
112
  lazy = false,
113
113
  server = true,
114
114
  dedupe = 'cancel',
115
- watch: watchOption = true,
115
+ watch: watchOption = undefined,
116
116
  paginated,
117
117
  initialPage,
118
118
  initialPerPage,
@@ -142,10 +142,15 @@ export function useApiAsyncData<
142
142
  );
143
143
  }
144
144
 
145
+ // For mutations (POST/PUT/PATCH/DELETE), auto-watch defaults to OFF unless explicitly enabled.
146
+ // For GET, auto-watch defaults to ON (reactive params, pagination).
147
+ const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes((method as string).toUpperCase());
148
+ const effectiveWatchOption = watchOption !== undefined ? watchOption : !isMutation;
149
+
145
150
  // Create reactive watch sources — use refs/computeds directly so Vue can track them
146
- // watchOption: false disables auto-refresh entirely
151
+ // effectiveWatchOption: false disables auto-refresh entirely
147
152
  const watchSources =
148
- watchOption === false
153
+ effectiveWatchOption === false
149
154
  ? []
150
155
  : [
151
156
  ...(typeof url === 'function' ? [url] : []),
@@ -322,13 +327,19 @@ export function useApiAsyncData<
322
327
  }
323
328
  };
324
329
 
330
+ // For mutations: use a static UUID-based key to prevent reactive key tracking.
331
+ // A reactive key function causes Nuxt to re-fetch whenever any Ref accessed inside it changes
332
+ // (e.g. URL params), which triggers duplicate calls when combined with manual .refresh().
333
+ // GETs keep the reactive computedKey for proper per-params cache isolation.
334
+ const resolvedKey = isMutation ? `${key}-${crypto.randomUUID()}` : computedKey;
335
+
325
336
  // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
326
- const result = useAsyncData<InferData<T, Options>>(computedKey, fetchFn, {
337
+ const result = useAsyncData<InferData<T, Options>>(resolvedKey, fetchFn, {
327
338
  immediate,
328
339
  lazy,
329
340
  server,
330
341
  dedupe,
331
- watch: watchOption === false ? [] : watchSources,
342
+ watch: effectiveWatchOption === false ? [] : watchSources,
332
343
  });
333
344
 
334
345
  if (!paginated) return result;