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.
- 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
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
57
|
-
|
|
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
|
}
|
|
@@ -9,39 +9,29 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Copied to the user's project alongside the generated connectors.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
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
|
-
// ──
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
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
|
-
|
|
30
|
+
composableFn(id); // updates the generated _detailIdRef
|
|
39
31
|
await composable.refresh?.();
|
|
40
32
|
}
|
|
41
33
|
function clear() {
|
|
42
|
-
|
|
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
|
-
//
|
|
77
|
-
if (composable.
|
|
78
|
-
await composable.
|
|
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 =
|
|
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
|
-
//
|
|
39
|
-
const watchSources =
|
|
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(
|
|
220
|
+
const result = useAsyncData(resolvedKey, fetchFn, {
|
|
212
221
|
immediate,
|
|
213
222
|
lazy,
|
|
214
223
|
server,
|
|
215
224
|
dedupe,
|
|
216
|
-
watch:
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,
|