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,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
|
-
|
|
67
|
-
|
|
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
|
-
// ──
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
const
|
|
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
|
-
|
|
36
|
+
composableFn(id); // updates the generated _detailIdRef
|
|
46
37
|
await composable.refresh?.();
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
function clear() {
|
|
50
|
-
|
|
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
|
-
//
|
|
98
|
-
if (composable.
|
|
99
|
-
await composable.
|
|
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 =
|
|
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
|
-
//
|
|
151
|
+
// effectiveWatchOption: false disables auto-refresh entirely
|
|
147
152
|
const watchSources =
|
|
148
|
-
|
|
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>>(
|
|
337
|
+
const result = useAsyncData<InferData<T, Options>>(resolvedKey, fetchFn, {
|
|
327
338
|
immediate,
|
|
328
339
|
lazy,
|
|
329
340
|
server,
|
|
330
341
|
dedupe,
|
|
331
|
-
watch:
|
|
342
|
+
watch: effectiveWatchOption === false ? [] : watchSources,
|
|
332
343
|
});
|
|
333
344
|
|
|
334
345
|
if (!paginated) return result;
|