nuxt-openapi-hyperfetch 0.3.81-beta → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +220 -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,376 @@
1
+ import { pascalCase, kebabCase } from 'change-case';
2
+ // ─── File header ──────────────────────────────────────────────────────────────
3
+ function generateFileHeader() {
4
+ return `/**
5
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
6
+ *
7
+ * This file was automatically generated by nuxt-openapi-generator.
8
+ * Any manual changes will be overwritten on the next generation.
9
+ *
10
+ * @generated by nuxt-openapi-generator
11
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
12
+ */
13
+
14
+ /* eslint-disable */
15
+ `;
16
+ }
17
+ // ─── Naming helpers ───────────────────────────────────────────────────────────
18
+ function toAsyncDataName(operationId) {
19
+ return `useAsyncData${pascalCase(operationId)}`;
20
+ }
21
+ // ─── URL builder ─────────────────────────────────────────────────────────────
22
+ /**
23
+ * Convert an OpenAPI path template into a JS arrow function string.
24
+ * '/pet/{petId}' + 'petId' → '(id: string | number) => `/pet/${id}`'
25
+ */
26
+ function buildUrlFn(path, pathParam) {
27
+ if (!pathParam) {
28
+ return `'${path}'`;
29
+ }
30
+ const urlTemplate = path.replace(`{${pathParam}}`, '${id}');
31
+ return `(id: string | number) => \`${urlTemplate}\``;
32
+ }
33
+ // ─── Section builders ─────────────────────────────────────────────────────────
34
+ function buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir) {
35
+ const lines = [];
36
+ // zod — always needed for schema declarations
37
+ lines.push(`import { z } from 'zod';`);
38
+ lines.push('');
39
+ // connector-types — return type interfaces
40
+ const typeImports = ['GetAllConnectorReturn'];
41
+ if (resource.detailEndpoint) {
42
+ typeImports.push('GetConnectorReturn');
43
+ }
44
+ if (resource.createEndpoint) {
45
+ typeImports.push('CreateConnectorReturn');
46
+ }
47
+ if (resource.updateEndpoint) {
48
+ typeImports.push('UpdateConnectorReturn');
49
+ }
50
+ if (resource.deleteEndpoint) {
51
+ typeImports.push('DeleteConnectorReturn');
52
+ }
53
+ lines.push(`import type { ${typeImports.join(', ')} } from '${runtimeRelDir}/connector-types';`);
54
+ lines.push('');
55
+ // SDK model type -- import using the inferred item type name (from $ref) if available
56
+ const modelTypeName = resource.itemTypeName;
57
+ if (modelTypeName) {
58
+ lines.push(`import type { ${modelTypeName} } from '${sdkRelDir}';`);
59
+ lines.push('');
60
+ }
61
+ // SDK request type -- only when the list endpoint actually has query parameters
62
+ if (resource.listEndpoint && resource.listEndpoint.hasQueryParams) {
63
+ const requestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
64
+ lines.push(`import type { ${requestTypeName} } from '${sdkRelDir}';`);
65
+ lines.push('');
66
+ }
67
+ // runtime connectors
68
+ const runtimeImports = ['useGetAllConnector'];
69
+ if (resource.detailEndpoint) {
70
+ runtimeImports.push('useGetConnector');
71
+ }
72
+ if (resource.createEndpoint) {
73
+ runtimeImports.push('useCreateConnector');
74
+ }
75
+ if (resource.updateEndpoint) {
76
+ runtimeImports.push('useUpdateConnector');
77
+ }
78
+ if (resource.deleteEndpoint) {
79
+ runtimeImports.push('useDeleteConnector');
80
+ }
81
+ for (const helper of runtimeImports) {
82
+ lines.push(`import { ${helper} } from '${runtimeRelDir}/${helper}';`);
83
+ }
84
+ lines.push('');
85
+ // useAsyncData composable — only needed for getAll (list endpoint)
86
+ if (resource.listEndpoint) {
87
+ const name = toAsyncDataName(resource.listEndpoint.operationId);
88
+ lines.push(`import { ${name} } from '${composablesRelDir}/${name}';`);
89
+ lines.push('');
90
+ }
91
+ return lines.join('\n');
92
+ }
93
+ function buildZodSchemas(resource) {
94
+ const lines = [];
95
+ const pascal = pascalCase(resource.name);
96
+ if (resource.zodSchemas.create) {
97
+ lines.push(`const ${pascal}CreateSchema = ${resource.zodSchemas.create};`);
98
+ lines.push('');
99
+ }
100
+ if (resource.zodSchemas.update) {
101
+ lines.push(`const ${pascal}UpdateSchema = ${resource.zodSchemas.update};`);
102
+ lines.push('');
103
+ }
104
+ if (resource.zodSchemas.create) {
105
+ lines.push(`type ${pascal}CreateInput = z.infer<typeof ${pascal}CreateSchema>;`);
106
+ }
107
+ if (resource.zodSchemas.update) {
108
+ lines.push(`type ${pascal}UpdateInput = z.infer<typeof ${pascal}UpdateSchema>;`);
109
+ }
110
+ return lines.join('\n');
111
+ }
112
+ function buildColumns(resource) {
113
+ if (!resource.columns || resource.columns.length === 0) {
114
+ return '';
115
+ }
116
+ const camel = resource.composableName
117
+ .replace(/^use/, '')
118
+ .replace(/Connector$/, '')
119
+ .replace(/^./, (c) => c.toLowerCase());
120
+ const varName = `${camel}Columns`;
121
+ const entries = resource.columns
122
+ .map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
123
+ .join(',\n');
124
+ return `const ${varName} = [\n${entries},\n];`;
125
+ }
126
+ function buildFields(resource) {
127
+ const fields = resource.formFields?.create ?? resource.formFields?.update;
128
+ if (!fields || fields.length === 0) {
129
+ return '';
130
+ }
131
+ const camel = resource.composableName
132
+ .replace(/^use/, '')
133
+ .replace(/Connector$/, '')
134
+ .replace(/^./, (c) => c.toLowerCase());
135
+ const varName = `${camel}Fields`;
136
+ const entries = fields
137
+ .map((f) => {
138
+ const opts = f.options
139
+ ? `, options: [${f.options.map((o) => `{ label: '${o.label}', value: '${o.value}' }`).join(', ')}]`
140
+ : '';
141
+ return ` { key: '${f.key}', label: '${f.label}', type: '${f.type}', required: ${f.required}${opts} }`;
142
+ })
143
+ .join(',\n');
144
+ return `const ${varName} = [\n${entries},\n];`;
145
+ }
146
+ function buildOptionsInterface(resource) {
147
+ const typeName = `${pascalCase(resource.composableName)}Options`;
148
+ const hasColumns = resource.columns && resource.columns.length > 0;
149
+ const fields = [];
150
+ if (resource.listEndpoint && hasColumns) {
151
+ fields.push(` columnLabels?: Record<string, string>;`);
152
+ fields.push(` columnLabel?: (key: string) => string;`);
153
+ }
154
+ if (resource.createEndpoint && resource.zodSchemas.create) {
155
+ const pascal = pascalCase(resource.name);
156
+ fields.push(` createSchema?: z.ZodTypeAny | ((base: typeof ${pascal}CreateSchema) => z.ZodTypeAny);`);
157
+ }
158
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
159
+ const pascal = pascalCase(resource.name);
160
+ fields.push(` updateSchema?: z.ZodTypeAny | ((base: typeof ${pascal}UpdateSchema) => z.ZodTypeAny);`);
161
+ }
162
+ if (resource.createEndpoint || resource.updateEndpoint || resource.deleteEndpoint) {
163
+ fields.push(` onRequest?: (ctx: any) => void | Promise<void> | Record<string, any>;`);
164
+ fields.push(` onSuccess?: (data: any, ctx: { operation: string }) => void;`);
165
+ fields.push(` onError?: (err: any, ctx: { operation: string }) => void;`);
166
+ fields.push(` onFinish?: (ctx: any) => void;`);
167
+ fields.push(` skipGlobalCallbacks?: boolean | Array<'onRequest' | 'onSuccess' | 'onError' | 'onFinish'>;`);
168
+ }
169
+ fields.push(` baseURL?: string;`);
170
+ if (fields.length === 1) {
171
+ // only baseURL — minimal interface
172
+ return [`interface ${typeName} {`, ...fields, `}`].join('\n');
173
+ }
174
+ return [`interface ${typeName} {`, ...fields, `}`].join('\n');
175
+ }
176
+ function buildReturnType(resource) {
177
+ const pascal = resource.itemTypeName ?? pascalCase(resource.name);
178
+ const localPascal = pascalCase(resource.name);
179
+ const typeName = `${pascalCase(resource.composableName)}Return`;
180
+ const fields = [];
181
+ fields.push(` getAll: GetAllConnectorReturn<${pascal}>;`);
182
+ if (resource.detailEndpoint) {
183
+ fields.push(` get: GetConnectorReturn<${pascal}>;`);
184
+ }
185
+ if (resource.createEndpoint) {
186
+ const inputType = resource.zodSchemas.create
187
+ ? `${localPascal}CreateInput`
188
+ : `Record<string, unknown>`;
189
+ fields.push(` create: CreateConnectorReturn<${inputType}>;`);
190
+ }
191
+ if (resource.updateEndpoint) {
192
+ const inputType = resource.zodSchemas.update
193
+ ? `${localPascal}UpdateInput`
194
+ : `Record<string, unknown>`;
195
+ fields.push(` update: UpdateConnectorReturn<${inputType}>;`);
196
+ }
197
+ if (resource.deleteEndpoint) {
198
+ fields.push(` del: DeleteConnectorReturn<${pascal}>;`);
199
+ }
200
+ return [`type ${typeName} = {`, ...fields, `};`].join('\n');
201
+ }
202
+ function buildFunctionBody(resource) {
203
+ const pascal = resource.itemTypeName ?? pascalCase(resource.name);
204
+ const localPascal = pascalCase(resource.name);
205
+ const hasColumns = resource.columns && resource.columns.length > 0;
206
+ const hasFields = !!(resource.formFields?.create?.length || resource.formFields?.update?.length);
207
+ const camel = resource.composableName
208
+ .replace(/^use/, '')
209
+ .replace(/Connector$/, '')
210
+ .replace(/^./, (c) => c.toLowerCase());
211
+ const columnsVar = `${camel}Columns`;
212
+ const fieldsVar = `${camel}Fields`;
213
+ const optionsTypeName = `${pascalCase(resource.composableName)}Options`;
214
+ const returnTypeName = `${pascalCase(resource.composableName)}Return`;
215
+ const hasMutations = !!(resource.createEndpoint ||
216
+ resource.updateEndpoint ||
217
+ resource.deleteEndpoint);
218
+ // Options destructure
219
+ const optionKeys = [];
220
+ if (resource.listEndpoint && hasColumns) {
221
+ optionKeys.push('columnLabels', 'columnLabel');
222
+ }
223
+ if (resource.createEndpoint && resource.zodSchemas.create) {
224
+ optionKeys.push('createSchema');
225
+ }
226
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
227
+ optionKeys.push('updateSchema');
228
+ }
229
+ if (hasMutations) {
230
+ optionKeys.push('onRequest', 'onSuccess', 'onError', 'onFinish', 'skipGlobalCallbacks');
231
+ }
232
+ optionKeys.push('baseURL');
233
+ const optionsDestructure = ` const { ${optionKeys.join(', ')} } = options;\n`;
234
+ const lines = [];
235
+ // ── Function signature ─────────────────────────────────────────────────────
236
+ if (resource.listEndpoint) {
237
+ const hasQueryParams = resource.listEndpoint.hasQueryParams;
238
+ const listRequestTypeName = hasQueryParams
239
+ ? `${pascalCase(resource.listEndpoint.operationId)}Request`
240
+ : `Record<string, never>`;
241
+ lines.push(`export function ${resource.composableName}(source: () => unknown, options?: ${optionsTypeName}): ${returnTypeName};`, `export function ${resource.composableName}(params?: ${listRequestTypeName}, options?: ${optionsTypeName}): ${returnTypeName};`, `export function ${resource.composableName}(paramsOrSource?: ${listRequestTypeName} | (() => unknown), options: ${optionsTypeName} = {}): ${returnTypeName} {`);
242
+ }
243
+ else {
244
+ lines.push(`export function ${resource.composableName}(options: ${optionsTypeName} = {}): ${returnTypeName} {`);
245
+ lines.push(` const paramsOrSource = undefined;`);
246
+ }
247
+ lines.push(optionsDestructure.trimEnd());
248
+ lines.push('');
249
+ // ── getAll ─────────────────────────────────────────────────────────────────
250
+ if (resource.listEndpoint) {
251
+ const fn = toAsyncDataName(resource.listEndpoint.operationId);
252
+ const hasQueryParams = resource.listEndpoint.hasQueryParams;
253
+ const listRequestTypeName = hasQueryParams
254
+ ? `${pascalCase(resource.listEndpoint.operationId)}Request`
255
+ : `Record<string, never>`;
256
+ const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
257
+ const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
258
+ const allArgs = [columnsArg, labelArgs].filter(Boolean).join(', ');
259
+ const opts = allArgs ? `{ ${allArgs} }` : '{}';
260
+ lines.push(` const isFactory = typeof paramsOrSource === 'function';`, ` const listFactory = isFactory`, ` ? (paramsOrSource as () => unknown)`, ` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName});`, ` const getAll = useGetAllConnector(listFactory, ${opts}) as unknown as GetAllConnectorReturn<${pascal}>;`);
261
+ }
262
+ else {
263
+ lines.push(` const getAll = useGetAllConnector(() => ({}), {}) as unknown as GetAllConnectorReturn<${pascal}>;`);
264
+ }
265
+ lines.push('');
266
+ // ── get ────────────────────────────────────────────────────────────────────
267
+ if (resource.detailEndpoint) {
268
+ const pathParam = resource.detailEndpoint.pathParams[0] ?? 'id';
269
+ const urlFn = buildUrlFn(resource.detailEndpoint.path, pathParam);
270
+ const fieldsArg = hasFields ? `fields: ${fieldsVar}` : '';
271
+ const args = [
272
+ 'baseURL',
273
+ 'onRequest',
274
+ 'onSuccess',
275
+ 'onError',
276
+ 'onFinish',
277
+ 'skipGlobalCallbacks',
278
+ fieldsArg,
279
+ ]
280
+ .filter(Boolean)
281
+ .join(', ');
282
+ lines.push(` const get = useGetConnector(${urlFn}, { ${args} }) as unknown as GetConnectorReturn<${pascal}>;`);
283
+ lines.push('');
284
+ }
285
+ // ── create ─────────────────────────────────────────────────────────────────
286
+ if (resource.createEndpoint) {
287
+ const inputType = resource.zodSchemas.create
288
+ ? `${localPascal}CreateInput`
289
+ : `Record<string, unknown>`;
290
+ const schemaArg = resource.zodSchemas.create
291
+ ? `schema: ${localPascal}CreateSchema, schemaOverride: createSchema,`
292
+ : '';
293
+ const fieldsArg = hasFields ? `fields: ${fieldsVar},` : '';
294
+ const method = resource.createEndpoint.method;
295
+ lines.push(` const create = useCreateConnector('${resource.createEndpoint.path}', {`, ` method: '${method}',`, ...(schemaArg ? [` ${schemaArg}`] : []), ...(fieldsArg ? [` ${fieldsArg}`] : []), ` onRequest, onSuccess, onError, onFinish,`, ` baseURL, skipGlobalCallbacks,`, ` }) as unknown as CreateConnectorReturn<${inputType}>;`);
296
+ lines.push('');
297
+ }
298
+ // ── update ─────────────────────────────────────────────────────────────────
299
+ if (resource.updateEndpoint) {
300
+ const inputType = resource.zodSchemas.update
301
+ ? `${localPascal}UpdateInput`
302
+ : `Record<string, unknown>`;
303
+ const pathParam = resource.updateEndpoint.pathParams[0]; // undefined when ID comes from body
304
+ const urlFn = buildUrlFn(resource.updateEndpoint.path, pathParam);
305
+ const schemaArg = resource.zodSchemas.update
306
+ ? `schema: ${localPascal}UpdateSchema, schemaOverride: updateSchema,`
307
+ : '';
308
+ const fieldsArg = hasFields ? `fields: ${fieldsVar},` : '';
309
+ const method = resource.updateEndpoint.method;
310
+ lines.push(` const update = useUpdateConnector(${urlFn}, {`, ` method: '${method}',`, ...(schemaArg ? [` ${schemaArg}`] : []), ...(fieldsArg ? [` ${fieldsArg}`] : []), ` onRequest, onSuccess, onError, onFinish,`, ` baseURL, skipGlobalCallbacks,`, ` }) as unknown as UpdateConnectorReturn<${inputType}>;`);
311
+ lines.push('');
312
+ }
313
+ // ── del ────────────────────────────────────────────────────────────────────
314
+ if (resource.deleteEndpoint) {
315
+ const pathParam = resource.deleteEndpoint.pathParams[0];
316
+ const urlFn = buildUrlFn(resource.deleteEndpoint.path, pathParam);
317
+ // idFn: extract the ID from the staged item — try the path param name first, then .id
318
+ const idFn = pathParam
319
+ ? `(item: any) => item?.${pathParam} ?? item?.id ?? item`
320
+ : `(item: any) => item?.id ?? item`;
321
+ lines.push(` const del = useDeleteConnector(`, ` ${idFn},`, ` ${urlFn},`, ` { onRequest, onSuccess, onError, onFinish, baseURL, skipGlobalCallbacks }`, ` ) as unknown as DeleteConnectorReturn<${pascal}>;`);
322
+ lines.push('');
323
+ }
324
+ // ── return ─────────────────────────────────────────────────────────────────
325
+ const returnKeys = ['getAll'];
326
+ if (resource.detailEndpoint) {
327
+ returnKeys.push('get');
328
+ }
329
+ if (resource.createEndpoint) {
330
+ returnKeys.push('create');
331
+ }
332
+ if (resource.updateEndpoint) {
333
+ returnKeys.push('update');
334
+ }
335
+ if (resource.deleteEndpoint) {
336
+ returnKeys.push('del');
337
+ }
338
+ lines.push(` return { ${returnKeys.join(', ')} } as ${returnTypeName};`);
339
+ lines.push(`}`);
340
+ return lines.join('\n');
341
+ }
342
+ // ─── Public API ───────────────────────────────────────────────────────────────
343
+ export function generateConnectorFile(resource, composablesRelDir, sdkRelDir = '../..', runtimeRelDir = '../../runtime') {
344
+ const header = generateFileHeader();
345
+ const imports = buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir);
346
+ const schemas = buildZodSchemas(resource);
347
+ const columns = buildColumns(resource);
348
+ const fields = buildFields(resource);
349
+ const optionsInterface = buildOptionsInterface(resource);
350
+ const returnType = buildReturnType(resource);
351
+ const fn = buildFunctionBody(resource);
352
+ const parts = [header, imports];
353
+ if (schemas.trim()) {
354
+ parts.push(schemas);
355
+ }
356
+ if (columns.trim()) {
357
+ parts.push(columns);
358
+ }
359
+ if (fields.trim()) {
360
+ parts.push(fields);
361
+ }
362
+ parts.push(optionsInterface);
363
+ parts.push(returnType);
364
+ parts.push(fn);
365
+ return parts.join('\n') + '\n';
366
+ }
367
+ export function connectorFileName(composableName) {
368
+ return `${kebabCase(composableName)}.ts`;
369
+ }
370
+ export function generateConnectorIndexFile(composableNames) {
371
+ const header = generateFileHeader();
372
+ const exports = composableNames
373
+ .map((name) => `export { ${name} } from './${kebabCase(name)}';`)
374
+ .join('\n');
375
+ return `${header}${exports}\n`;
376
+ }
@@ -0,0 +1,37 @@
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
+ export interface ConnectorGeneratorOptions {
8
+ /** Absolute or relative path to the OpenAPI YAML/JSON spec */
9
+ inputSpec: string;
10
+ /** Directory where connector files will be written. E.g. ./composables/connectors */
11
+ outputDir: string;
12
+ /**
13
+ * Directory where the useAsyncData composables live (only used for getAll/list),
14
+ * expressed as a path relative to outputDir. Defaults to '../use-async-data/composables'.
15
+ */
16
+ composablesRelDir?: string;
17
+ /**
18
+ * Directory where runtime helpers will be copied to, expressed relative to
19
+ * outputDir. Defaults to '../runtime'.
20
+ */
21
+ runtimeRelDir?: string;
22
+ /**
23
+ * Base URL for API requests. If not provided, connectors will read from
24
+ * useRuntimeConfig().public.apiBaseUrl at runtime.
25
+ */
26
+ baseUrl?: string;
27
+ }
28
+ export interface ConnectorFileInfo {
29
+ /** PascalCase resource name. E.g. 'Pet' */
30
+ resourceName: string;
31
+ /** Generated composable function name. E.g. 'usePetsConnector' */
32
+ composableName: string;
33
+ /** Output filename (kebab-case). E.g. 'use-pets-connector.ts' */
34
+ fileName: string;
35
+ /** Formatted TypeScript source ready to be written to disk */
36
+ content: string;
37
+ }
@@ -0,0 +1,7 @@
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
+ export {};
@@ -53,8 +53,9 @@ export function useDeleteConnector(composableFn, options = {}) {
53
53
  try {
54
54
  // Pass the full target item; the generated composable extracts the id it needs
55
55
  const composable = composableFn(target.value);
56
- if (composable.execute) {
57
- await composable.execute();
56
+ // refresh() bypasses Nuxt SSR payload cache, forcing a real network request
57
+ if (composable.refresh) {
58
+ await composable.refresh();
58
59
  }
59
60
  const err = composable.error?.value;
60
61
  if (err) {
@@ -66,6 +67,7 @@ export function useDeleteConnector(composableFn, options = {}) {
66
67
  onSuccess.value?.(deletedItem);
67
68
  }
68
69
  catch (err) {
70
+ console.error('[useDeleteConnector] confirm error:', err);
69
71
  error.value = err;
70
72
  onError.value?.(err);
71
73
  }
@@ -10,5 +10,4 @@ export declare function useDetailConnector(composableFn: any, options?: {}): {
10
10
  load: (id: any) => Promise<void>;
11
11
  clear: () => void;
12
12
  _composable: any;
13
- _idRef: any;
14
13
  };
@@ -9,39 +9,29 @@
9
9
  *
10
10
  * Copied to the user's project alongside the generated connectors.
11
11
  */
12
- import { ref, computed } from 'vue';
12
+ import { computed } from 'vue';
13
13
  /**
14
14
  * @param composableFn The generated useAsyncData composable, e.g. useAsyncDataGetPetById
15
15
  * @param options Optional configuration
16
16
  */
17
17
  export function useDetailConnector(composableFn, options = {}) {
18
18
  const { fields = [] } = options;
19
- // ── Reactive ID ref passed to the composable wrapper ──────────────────────
20
- // The generated connector wraps the composable like:
21
- // (idRef, opts) => useAsyncDataGetPetById(computed(() => ({ petId: idRef.value })), opts)
22
- // This lets us update idRef.value in load(id) so refresh() picks up the new value.
23
- const idRef = ref(null);
24
- // ── Execute the underlying composable ──────────────────────────────────────
25
- // watch: false + immediate: false + lazy: true prevent any fetch until
26
- // load(id) is called explicitly. The URL is only evaluated on refresh().
27
- const composable = composableFn(idRef, {
28
- watch: false,
29
- immediate: false,
30
- lazy: true,
31
- });
19
+ // ── Execute the underlying composable lazily (only when load(id) is called) ─
20
+ // composableFn is a generated wrapper: (id) => { _idRef.value = id; return _composable }
21
+ // Calling it with null initializes the composable in setup context (safe p.value is { param: null })
22
+ // Calling it in load(id) updates the ref before refresh()
23
+ const composable = composableFn(null);
32
24
  // ── Derived state ──────────────────────────────────────────────────────────
33
25
  const item = computed(() => composable.data?.value ?? null);
34
26
  const loading = computed(() => composable.pending?.value ?? false);
35
27
  const error = computed(() => composable.error?.value ?? null);
36
28
  // ── Actions ────────────────────────────────────────────────────────────────
37
29
  async function load(id) {
38
- idRef.value = id;
30
+ composableFn(id); // updates the generated _detailIdRef
39
31
  await composable.refresh?.();
40
32
  }
41
33
  function clear() {
42
- idRef.value = null;
43
- if (composable.data)
44
- composable.data.value = null;
34
+ composableFn(null);
45
35
  }
46
36
  return {
47
37
  // State
@@ -52,8 +42,7 @@ export function useDetailConnector(composableFn, options = {}) {
52
42
  // Actions
53
43
  load,
54
44
  clear,
55
- // Expose composable for advanced use
45
+ // Expose composable for advanced use (e.g. useFormConnector loadWith)
56
46
  _composable: composable,
57
- _idRef: idRef,
58
47
  };
59
48
  }
@@ -73,9 +73,9 @@ export function useFormConnector(composableFn, options = {}) {
73
73
  try {
74
74
  // The mutation composable accepts the model as its payload
75
75
  const composable = composableFn(model.value);
76
- // Wait for the async data to resolve
77
- if (composable.execute) {
78
- await composable.execute();
76
+ // refresh() bypasses Nuxt SSR payload cache, forcing a real network request
77
+ if (composable.refresh) {
78
+ await composable.refresh();
79
79
  }
80
80
  const data = composable.data?.value;
81
81
  const err = composable.error?.value;
@@ -85,6 +85,7 @@ export function useFormConnector(composableFn, options = {}) {
85
85
  onSuccess.value?.(data);
86
86
  }
87
87
  catch (err) {
88
+ console.error('[useFormConnector] submit error:', err);
88
89
  submitError.value = err;
89
90
  onError.value?.(err);
90
91
  }
@@ -16,7 +16,7 @@ import { getGlobalApiPagination, buildPaginationRequest, extractPaginationMetaFr
16
16
  * - Watch pattern for reactive parameters
17
17
  */
18
18
  export function useApiAsyncData(key, url, options) {
19
- const { method = 'GET', body, headers = {}, params, baseURL, cacheKey, transform, pick, onRequest, onSuccess, onError, onFinish, skipGlobalCallbacks, immediate = true, lazy = false, server = true, dedupe = 'cancel', watch: watchOption = true, paginated, initialPage, initialPerPage, paginationConfig, ...restOptions } = options || {};
19
+ const { method = 'GET', body, headers = {}, params, baseURL, cacheKey, transform, pick, onRequest, onSuccess, onError, onFinish, skipGlobalCallbacks, immediate = true, lazy = false, server = true, dedupe = 'cancel', watch: watchOption = undefined, paginated, initialPage, initialPerPage, paginationConfig, ...restOptions } = options || {};
20
20
  // ---------------------------------------------------------------------------
21
21
  // Pagination setup
22
22
  // ---------------------------------------------------------------------------
@@ -34,9 +34,13 @@ export function useApiAsyncData(key, url, options) {
34
34
  if (!resolvedBaseURL) {
35
35
  console.warn('[nuxt-openapi-hyperfetch] No baseURL configured. Set runtimeConfig.public.apiBaseUrl in nuxt.config.ts or pass baseURL in options.');
36
36
  }
37
+ // For mutations (POST/PUT/PATCH/DELETE), auto-watch defaults to OFF unless explicitly enabled.
38
+ // For GET, auto-watch defaults to ON (reactive params, pagination).
39
+ const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase());
40
+ const effectiveWatchOption = watchOption !== undefined ? watchOption : !isMutation;
37
41
  // Create reactive watch sources — use refs/computeds directly so Vue can track them
38
- // watchOption: false disables auto-refresh entirely
39
- const watchSources = watchOption === false
42
+ // effectiveWatchOption: false disables auto-refresh entirely
43
+ const watchSources = effectiveWatchOption === false
40
44
  ? []
41
45
  : [
42
46
  ...(typeof url === 'function' ? [url] : []),
@@ -207,13 +211,18 @@ export function useApiAsyncData(key, url, options) {
207
211
  }
208
212
  }
209
213
  };
214
+ // For mutations: use a static UUID-based key to prevent reactive key tracking.
215
+ // A reactive key function causes Nuxt to re-fetch whenever any Ref accessed inside it changes
216
+ // (e.g. URL params), which triggers duplicate calls when combined with manual .refresh().
217
+ // GETs keep the reactive computedKey for proper per-params cache isolation.
218
+ const resolvedKey = isMutation ? `${key}-${crypto.randomUUID()}` : computedKey;
210
219
  // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
211
- const result = useAsyncData(computedKey, fetchFn, {
220
+ const result = useAsyncData(resolvedKey, fetchFn, {
212
221
  immediate,
213
222
  lazy,
214
223
  server,
215
224
  dedupe,
216
- watch: watchOption === false ? [] : watchSources,
225
+ watch: effectiveWatchOption === false ? [] : watchSources,
217
226
  });
218
227
  if (!paginated)
219
228
  return result;
@@ -13,7 +13,6 @@ function generateFileHeader() {
13
13
  */
14
14
 
15
15
  /* eslint-disable */
16
- // @ts-nocheck
17
16
  `;
18
17
  }
19
18
  /**
@@ -90,6 +89,17 @@ function generateImports(method, apiImportPath, isRaw) {
90
89
  else {
91
90
  imports += `import { useApiAsyncData, type ApiAsyncDataOptions } from '../runtime/useApiAsyncData';`;
92
91
  }
92
+ // Vue imports needed by the generated function body
93
+ const vueImports = ['shallowRef'];
94
+ if (method.requestType) {
95
+ vueImports.push('isRef');
96
+ }
97
+ if (method.hasBody || method.hasQueryParams || method.pathParams.length > 0) {
98
+ vueImports.push('computed');
99
+ }
100
+ const vueTypeImports = method.requestType ? ['Ref', 'ComputedRef'] : [];
101
+ const allVueImports = [...vueImports, ...vueTypeImports.map((t) => `type ${t}`)].join(', ');
102
+ imports += `\nimport { ${allVueImports} } from 'vue';`;
93
103
  return imports;
94
104
  }
95
105
  /**
@@ -124,24 +134,18 @@ function generateFunctionBody(method, isRaw, generateOptions) {
124
134
  const returnType = isRaw
125
135
  ? `ReturnType<typeof ${wrapperFunction}<${responseType}, DataT, PickT, Options>>`
126
136
  : `ReturnType<typeof ${wrapperFunction}<${responseType}, Options>>`;
127
- const pInit = hasParams ? `\n const p = shallowRef(params)` : '';
137
+ const pInit = hasParams ? `\n const p = isRef(params) ? params : shallowRef(params)` : '';
128
138
  const argsExtraction = hasParams
129
139
  ? ` const _hasKey = typeof args[0] === 'string'\n const params = _hasKey ? args[1] : args[0]\n const options = _hasKey ? { cacheKey: args[0], ...args[2] } : args[1]`
130
140
  : ` const _hasKey = typeof args[0] === 'string'\n const options = _hasKey ? { cacheKey: args[0], ...args[1] } : args[0]`;
131
- return `${description}export function ${composableName}<
132
- DataT = ${responseType},
133
- PickT extends ReadonlyArray<string> | undefined = undefined,
134
- Options extends ${optionsType} = ${optionsDefaultType}
135
- >(key: string, ${args}): ${returnType}
136
- export function ${composableName}<
137
- DataT = ${responseType},
138
- PickT extends ReadonlyArray<string> | undefined = undefined,
139
- Options extends ${optionsType} = ${optionsDefaultType}
140
- >(${args}): ${returnType}
141
- export function ${composableName}(...args: any[]) {
142
- ${argsExtraction}${pInit}
143
- return ${wrapperCall}(${key}, ${url}, ${fetchOptions})
144
- }`;
141
+ const genericTypeParams = `<\n DataT = ${responseType},\n PickT extends ReadonlyArray<string> | undefined = undefined,\n Options extends ${optionsType} = ${optionsDefaultType}\n>`;
142
+ const refParamsStr = hasParams
143
+ ? `params: Ref<${method.requestType}> | ComputedRef<${method.requestType}>, ${optionsArg}`
144
+ : '';
145
+ const refOverloads = hasParams
146
+ ? `\nexport function ${composableName}${genericTypeParams}(${refParamsStr}): ${returnType}\nexport function ${composableName}${genericTypeParams}(key: string, ${refParamsStr}): ${returnType}`
147
+ : '';
148
+ return `${description}export function ${composableName}${genericTypeParams}(${args}): ${returnType}\nexport function ${composableName}${genericTypeParams}(key: string, ${args}): ${returnType}${refOverloads}\nexport function ${composableName}(...args: any[]) {\n${argsExtraction}${pInit}\n return ${wrapperCall}(${key}, ${url}, ${fetchOptions})\n}`;
145
149
  }
146
150
  /**
147
151
  * Generate URL (with path params if needed)
@@ -94,7 +94,7 @@ function generateFunctionBody(method, options) {
94
94
  const url = generateUrl(method);
95
95
  const fetchOptions = generateFetchOptions(method, options);
96
96
  const description = method.description ? `/**\n * ${method.description}\n */\n` : '';
97
- const pInit = hasParams ? `\n const p = shallowRef(params)` : '';
97
+ const pInit = hasParams ? `\n const p = isRef(params) ? params : shallowRef(params)` : '';
98
98
  return `${description}export const ${method.composableName} = <
99
99
  DataT = ${responseType},
100
100
  PickT extends ReadonlyArray<string> | undefined = undefined,