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
|
@@ -23,8 +23,8 @@ function toAsyncDataName(operationId) {
|
|
|
23
23
|
return `useAsyncData${pascalCase(operationId)}`;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* composable name →
|
|
27
|
-
* 'useAsyncDataGetPets' → '
|
|
26
|
+
* composable name → kebab-case file name (without .ts).
|
|
27
|
+
* 'useAsyncDataGetPets' → 'use-async-data-get-pets'
|
|
28
28
|
*/
|
|
29
29
|
function toFileName(composableName) {
|
|
30
30
|
return composableName;
|
|
@@ -35,14 +35,20 @@ function toFileName(composableName) {
|
|
|
35
35
|
*/
|
|
36
36
|
function buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir) {
|
|
37
37
|
const lines = [];
|
|
38
|
+
// vue — shallowRef needed when form, delete, or detail endpoints exist
|
|
39
|
+
const needsShallowRef = !!(resource.createEndpoint ||
|
|
40
|
+
resource.updateEndpoint ||
|
|
41
|
+
resource.deleteEndpoint ||
|
|
42
|
+
resource.detailEndpoint);
|
|
43
|
+
const needsComputed = !!resource.detailEndpoint;
|
|
44
|
+
if (needsShallowRef) {
|
|
45
|
+
const vueImports = needsComputed ? `shallowRef, computed` : `shallowRef`;
|
|
46
|
+
lines.push(`import { ${vueImports} } from 'vue';`);
|
|
47
|
+
lines.push('');
|
|
48
|
+
}
|
|
38
49
|
// zod
|
|
39
50
|
lines.push(`import { z } from 'zod';`);
|
|
40
51
|
lines.push('');
|
|
41
|
-
// Vue — computed needed for the detail connector wrapper
|
|
42
|
-
if (resource.detailEndpoint) {
|
|
43
|
-
lines.push(`import { computed } from 'vue';`);
|
|
44
|
-
lines.push('');
|
|
45
|
-
}
|
|
46
52
|
// connector-types — structural interfaces for return types
|
|
47
53
|
const connectorTypeImports = ['ListConnectorReturn'];
|
|
48
54
|
if (resource.detailEndpoint) {
|
|
@@ -160,6 +166,12 @@ function buildOptionsInterface(resource) {
|
|
|
160
166
|
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
161
167
|
fields.push(` updateSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
|
|
162
168
|
}
|
|
169
|
+
if (resource.createEndpoint || resource.updateEndpoint || resource.deleteEndpoint) {
|
|
170
|
+
fields.push(` onRequest?: (ctx: any) => void | Promise<void>;`);
|
|
171
|
+
fields.push(` onSuccess?: (data: any) => void;`);
|
|
172
|
+
fields.push(` onError?: (err: any) => void;`);
|
|
173
|
+
fields.push(` onFinish?: () => void;`);
|
|
174
|
+
}
|
|
163
175
|
if (fields.length === 0) {
|
|
164
176
|
return `type ${typeName} = Record<string, never>;`;
|
|
165
177
|
}
|
|
@@ -185,11 +197,15 @@ function buildReturnType(resource) {
|
|
|
185
197
|
fields.push(` detail: DetailConnectorReturn<${pascal}>;`);
|
|
186
198
|
}
|
|
187
199
|
if (resource.createEndpoint) {
|
|
188
|
-
const inputType = resource.zodSchemas.create
|
|
200
|
+
const inputType = resource.zodSchemas.create
|
|
201
|
+
? `${pascal}CreateInput`
|
|
202
|
+
: `Record<string, unknown>`;
|
|
189
203
|
fields.push(` createForm: FormConnectorReturn<${inputType}>;`);
|
|
190
204
|
}
|
|
191
205
|
if (resource.updateEndpoint) {
|
|
192
|
-
const inputType = resource.zodSchemas.update
|
|
206
|
+
const inputType = resource.zodSchemas.update
|
|
207
|
+
? `${pascal}UpdateInput`
|
|
208
|
+
: `Record<string, unknown>`;
|
|
193
209
|
fields.push(` updateForm: FormConnectorReturn<${inputType}>;`);
|
|
194
210
|
}
|
|
195
211
|
if (resource.deleteEndpoint) {
|
|
@@ -197,6 +213,17 @@ function buildReturnType(resource) {
|
|
|
197
213
|
}
|
|
198
214
|
return [`type ${typeName} = {`, ...fields, `};`].join('\n');
|
|
199
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Build the 3 generated lines for a composable that is keyed by a single path param
|
|
218
|
+
* (detail, delete). Uses computed(() => ({ param: ref.value })) so that p.value is
|
|
219
|
+
* always an object during setup — avoids `null.param` crash when Nuxt evaluates computedKey.
|
|
220
|
+
*/
|
|
221
|
+
function buildPathParamComposableLines(prefix, fn, pathParam) {
|
|
222
|
+
return [
|
|
223
|
+
` const ${prefix}Ref = shallowRef(null);`,
|
|
224
|
+
` const ${prefix}Composable = ${fn}(computed(() => ({ ${pathParam}: ${prefix}Ref.value })) as any, { immediate: false });`,
|
|
225
|
+
];
|
|
226
|
+
}
|
|
200
227
|
/**
|
|
201
228
|
* Build the body of the exported connector function.
|
|
202
229
|
*/
|
|
@@ -223,6 +250,12 @@ function buildFunctionBody(resource) {
|
|
|
223
250
|
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
224
251
|
optionKeys.push('updateSchema');
|
|
225
252
|
}
|
|
253
|
+
const hasMutations = !!(resource.createEndpoint ||
|
|
254
|
+
resource.updateEndpoint ||
|
|
255
|
+
resource.deleteEndpoint);
|
|
256
|
+
if (hasMutations) {
|
|
257
|
+
optionKeys.push('onRequest', 'onSuccess', 'onError', 'onFinish');
|
|
258
|
+
}
|
|
226
259
|
const optionsDestructure = optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
|
|
227
260
|
// ── List / table sub-connector ─────────────────────────────────────────────
|
|
228
261
|
if (resource.listEndpoint) {
|
|
@@ -243,23 +276,28 @@ function buildFunctionBody(resource) {
|
|
|
243
276
|
}
|
|
244
277
|
if (resource.detailEndpoint) {
|
|
245
278
|
const fn = toAsyncDataName(resource.detailEndpoint.operationId);
|
|
246
|
-
// Wrap the composable to map the generic idRef to the actual path param name.
|
|
247
|
-
// useDetailConnector passes a ref<id> and calls refresh() after updating it.
|
|
248
279
|
const pathParam = resource.detailEndpoint.pathParams[0] ?? 'id';
|
|
249
|
-
subConnectors.push(
|
|
280
|
+
subConnectors.push(...buildPathParamComposableLines('_detail', fn, pathParam));
|
|
281
|
+
subConnectors.push(` const detail = useDetailConnector((id: any) => { _detailRef.value = id; return _detailComposable; }) as unknown as DetailConnectorReturn<${pascal}>;`);
|
|
250
282
|
}
|
|
251
283
|
if (resource.createEndpoint) {
|
|
252
284
|
const fn = toAsyncDataName(resource.createEndpoint.operationId);
|
|
253
|
-
const inputType = resource.zodSchemas.create
|
|
285
|
+
const inputType = resource.zodSchemas.create
|
|
286
|
+
? `${pascal}CreateInput`
|
|
287
|
+
: `Record<string, unknown>`;
|
|
254
288
|
const schemaArg = resource.zodSchemas.create
|
|
255
289
|
? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
|
|
256
290
|
: '{}';
|
|
257
|
-
subConnectors.push(` const
|
|
291
|
+
subConnectors.push(` const _createRef = shallowRef({});`);
|
|
292
|
+
subConnectors.push(` const _createComposable = ${fn}(_createRef as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`);
|
|
293
|
+
subConnectors.push(` const createForm = useFormConnector((p: any) => { _createRef.value = p; return _createComposable; }, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
|
|
258
294
|
}
|
|
259
295
|
if (resource.updateEndpoint) {
|
|
260
296
|
const fn = toAsyncDataName(resource.updateEndpoint.operationId);
|
|
261
297
|
const hasDetail = !!resource.detailEndpoint;
|
|
262
|
-
const inputType = resource.zodSchemas.update
|
|
298
|
+
const inputType = resource.zodSchemas.update
|
|
299
|
+
? `${pascal}UpdateInput`
|
|
300
|
+
: `Record<string, unknown>`;
|
|
263
301
|
let schemaArg = '{}';
|
|
264
302
|
if (resource.zodSchemas.update && hasDetail) {
|
|
265
303
|
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
|
|
@@ -270,11 +308,23 @@ function buildFunctionBody(resource) {
|
|
|
270
308
|
else if (hasDetail) {
|
|
271
309
|
schemaArg = `{ loadWith: detail }`;
|
|
272
310
|
}
|
|
273
|
-
subConnectors.push(` const
|
|
311
|
+
subConnectors.push(` const _updateRef = shallowRef({});`);
|
|
312
|
+
subConnectors.push(` const _updateComposable = ${fn}(_updateRef as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`);
|
|
313
|
+
subConnectors.push(` const updateForm = useFormConnector((p: any) => { _updateRef.value = p; return _updateComposable; }, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
|
|
274
314
|
}
|
|
275
315
|
if (resource.deleteEndpoint) {
|
|
276
316
|
const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
|
|
277
|
-
|
|
317
|
+
const pathParam = resource.deleteEndpoint.pathParams[0];
|
|
318
|
+
if (pathParam) {
|
|
319
|
+
subConnectors.push(...buildPathParamComposableLines('_delete', fn, pathParam));
|
|
320
|
+
subConnectors.push(` const _deleteComposableWithHooks = ${fn}(computed(() => ({ ${pathParam}: _deleteRef.value })) as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`);
|
|
321
|
+
subConnectors.push(` const deleteAction = useDeleteConnector((item: any) => { _deleteRef.value = item?.${pathParam} ?? item?.id ?? item; return _deleteComposableWithHooks; }) as unknown as DeleteConnectorReturn<${pascal}>;`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
subConnectors.push(` const _deleteRef = shallowRef({});`);
|
|
325
|
+
subConnectors.push(` const _deleteComposable = ${fn}(_deleteRef as any, { immediate: false, onRequest, onSuccess, onError, onFinish });`);
|
|
326
|
+
subConnectors.push(` const deleteAction = useDeleteConnector((p: any) => { _deleteRef.value = p; return _deleteComposable; }) as unknown as DeleteConnectorReturn<${pascal}>;`);
|
|
327
|
+
}
|
|
278
328
|
}
|
|
279
329
|
// Return object — always includes table (undefined when no list + no factory)
|
|
280
330
|
const returnKeys = ['table'];
|
|
@@ -36,13 +36,6 @@ function getSuccessResponseSchema(operation) {
|
|
|
36
36
|
function isArraySchema(schema) {
|
|
37
37
|
return schema.type === 'array' || schema.items !== undefined;
|
|
38
38
|
}
|
|
39
|
-
/** True when schema is a primitive scalar (string, number, integer, boolean) — not a resource */
|
|
40
|
-
function isPrimitiveSchema(schema) {
|
|
41
|
-
return (schema.type === 'string' ||
|
|
42
|
-
schema.type === 'number' ||
|
|
43
|
-
schema.type === 'integer' ||
|
|
44
|
-
schema.type === 'boolean');
|
|
45
|
-
}
|
|
46
39
|
// ─── Request body schema ──────────────────────────────────────────────────────
|
|
47
40
|
function getRequestBodySchema(operation) {
|
|
48
41
|
if (!operation.requestBody?.content) {
|
|
@@ -91,11 +84,6 @@ export function detectIntent(method, path, operation) {
|
|
|
91
84
|
if (isArraySchema(responseSchema)) {
|
|
92
85
|
return 'list';
|
|
93
86
|
}
|
|
94
|
-
// Primitive response (string, number, boolean) → not a CRUD resource
|
|
95
|
-
// e.g. GET /user/login returns a string token — not a list or detail
|
|
96
|
-
if (isPrimitiveSchema(responseSchema)) {
|
|
97
|
-
return 'unknown';
|
|
98
|
-
}
|
|
99
87
|
// Object response — distinguish list vs detail by path structure:
|
|
100
88
|
// GET /pets/{id} → has path param → detail (single item fetch)
|
|
101
89
|
// GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
|
|
@@ -134,6 +122,7 @@ export function extractEndpoints(path, pathItem) {
|
|
|
134
122
|
intent,
|
|
135
123
|
hasPathParams: pathParams.length > 0,
|
|
136
124
|
pathParams,
|
|
125
|
+
hasQueryParams: (operation.parameters ?? []).some((p) => p.in === 'query'),
|
|
137
126
|
};
|
|
138
127
|
// Attach response schema for GET intents
|
|
139
128
|
if (method === 'GET') {
|
|
@@ -39,7 +39,16 @@ function resolveRefs(node, root, visited = new Set()) {
|
|
|
39
39
|
const resolved = resolvePointer(root, ref);
|
|
40
40
|
const newVisited = new Set(visited);
|
|
41
41
|
newVisited.add(ref);
|
|
42
|
-
|
|
42
|
+
const resolvedFull = resolveRefs(resolved, root, newVisited);
|
|
43
|
+
// Annotate resolved schemas from #/components/schemas/Xxx with the original
|
|
44
|
+
// component name so downstream consumers can recover the model type name.
|
|
45
|
+
if (typeof resolvedFull === 'object' &&
|
|
46
|
+
resolvedFull !== null &&
|
|
47
|
+
ref.startsWith('#/components/schemas/')) {
|
|
48
|
+
const refName = ref.split('/').pop();
|
|
49
|
+
return { ...resolvedFull, 'x-ref-name': refName };
|
|
50
|
+
}
|
|
51
|
+
return resolvedFull;
|
|
43
52
|
}
|
|
44
53
|
const result = {};
|
|
45
54
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -85,10 +85,17 @@ export function buildResourceMap(spec) {
|
|
|
85
85
|
? buildZodSchema(updateEp.requestBodySchema)
|
|
86
86
|
: undefined;
|
|
87
87
|
const resourceName = pascalCase(tag);
|
|
88
|
+
// Infer the SDK model type name from the original $ref component name.
|
|
89
|
+
// Priority: detail response > list items > list response (may be envelope object).
|
|
90
|
+
const itemTypeName = detailEp?.responseSchema?.['x-ref-name'] ??
|
|
91
|
+
listEp?.responseSchema?.items?.['x-ref-name'] ??
|
|
92
|
+
listEp?.responseSchema?.['x-ref-name'] ??
|
|
93
|
+
undefined;
|
|
88
94
|
const info = {
|
|
89
95
|
name: resourceName,
|
|
90
96
|
tag,
|
|
91
97
|
composableName: toConnectorName(tag),
|
|
98
|
+
itemTypeName,
|
|
92
99
|
endpoints,
|
|
93
100
|
listEndpoint: listEp,
|
|
94
101
|
detailEndpoint: detailEp,
|
|
@@ -132,7 +132,7 @@ function baseZodExpr(prop) {
|
|
|
132
132
|
case 'array':
|
|
133
133
|
return arrayZodExpr(prop);
|
|
134
134
|
case 'object':
|
|
135
|
-
return
|
|
135
|
+
return 'z.record(z.string(), z.unknown())';
|
|
136
136
|
default:
|
|
137
137
|
// $ref already resolved, unknown type → permissive
|
|
138
138
|
return 'z.unknown()';
|
|
@@ -181,27 +181,6 @@ function numberZodExpr(prop) {
|
|
|
181
181
|
}
|
|
182
182
|
return expr;
|
|
183
183
|
}
|
|
184
|
-
function objectZodExpr(prop) {
|
|
185
|
-
const { additionalProperties } = prop;
|
|
186
|
-
// additionalProperties: false or undefined → plain object
|
|
187
|
-
if (!additionalProperties || additionalProperties === true) {
|
|
188
|
-
return 'z.record(z.unknown())';
|
|
189
|
-
}
|
|
190
|
-
// additionalProperties has a known primitive type → typed record
|
|
191
|
-
const valueExpr = additionalPropsZodExpr(additionalProperties);
|
|
192
|
-
return `z.record(${valueExpr})`;
|
|
193
|
-
}
|
|
194
|
-
function additionalPropsZodExpr(schema) {
|
|
195
|
-
switch (schema.type) {
|
|
196
|
-
case 'string': return stringZodExpr(schema);
|
|
197
|
-
case 'integer': return integerZodExpr(schema);
|
|
198
|
-
case 'number': return numberZodExpr(schema);
|
|
199
|
-
case 'boolean': return 'z.boolean()';
|
|
200
|
-
case 'array': return arrayZodExpr(schema);
|
|
201
|
-
case 'object': return objectZodExpr(schema);
|
|
202
|
-
default: return 'z.unknown()';
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
184
|
function arrayZodExpr(prop) {
|
|
206
185
|
const itemExpr = prop.items ? baseZodExpr(prop.items) : 'z.unknown()';
|
|
207
186
|
let expr = `z.array(${itemExpr})`;
|
|
@@ -27,6 +27,8 @@ export interface OpenApiPropertySchema {
|
|
|
27
27
|
allOf?: OpenApiPropertySchema[];
|
|
28
28
|
oneOf?: OpenApiPropertySchema[];
|
|
29
29
|
anyOf?: OpenApiPropertySchema[];
|
|
30
|
+
/** Injected by the $ref resolver — original component schema name, e.g. 'Pet' */
|
|
31
|
+
'x-ref-name'?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface OpenApiSchema extends OpenApiPropertySchema {
|
|
32
34
|
required?: string[];
|
|
@@ -96,6 +98,8 @@ export interface EndpointInfo {
|
|
|
96
98
|
responseSchema?: OpenApiSchema;
|
|
97
99
|
hasPathParams: boolean;
|
|
98
100
|
pathParams: string[];
|
|
101
|
+
/** True when the operation has at least one query parameter */
|
|
102
|
+
hasQueryParams: boolean;
|
|
99
103
|
}
|
|
100
104
|
export type FieldType = 'input' | 'textarea' | 'select' | 'checkbox' | 'datepicker' | 'number';
|
|
101
105
|
export interface FormFieldDef {
|
|
@@ -136,6 +140,12 @@ export interface ResourceInfo {
|
|
|
136
140
|
updateEndpoint?: EndpointInfo;
|
|
137
141
|
/** The one endpoint detected as delete (DELETE) */
|
|
138
142
|
deleteEndpoint?: EndpointInfo;
|
|
143
|
+
/**
|
|
144
|
+
* Inferred item model type name (e.g. 'Pet', 'Order') derived from the
|
|
145
|
+
* response schema's original $ref component name. Used for SDK type imports.
|
|
146
|
+
* Undefined when the response type is anonymous/primitive.
|
|
147
|
+
*/
|
|
148
|
+
itemTypeName?: string;
|
|
139
149
|
/** Columns inferred from the list/detail response schema */
|
|
140
150
|
columns: ColumnDef[];
|
|
141
151
|
/** Form fields inferred from the request body schema */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConnectorGeneratorOptions } from './types.js';
|
|
2
|
+
import { type Logger } from '../../cli/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate headless connector composables from an OpenAPI spec.
|
|
5
|
+
*
|
|
6
|
+
* Steps:
|
|
7
|
+
* 1. Analyze the spec → ResourceMap (Schema Analyzer)
|
|
8
|
+
* 2. For each resource: generate connector source, format, write
|
|
9
|
+
* 3. Write an index barrel file
|
|
10
|
+
* 4. Copy runtime helpers to the user's project
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateConnectors(options: ConnectorGeneratorOptions, logger?: Logger): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { format } from 'prettier';
|
|
5
|
+
import { analyzeSpec } from '../components/schema-analyzer/index.js';
|
|
6
|
+
import { generateConnectorFile, connectorFileName, generateConnectorIndexFile, } from './templates.js';
|
|
7
|
+
import { createClackLogger } from '../../cli/logger.js';
|
|
8
|
+
// Runtime files that must be copied to the user's project
|
|
9
|
+
const RUNTIME_FILES = [
|
|
10
|
+
'connector-types.ts',
|
|
11
|
+
'useGetAllConnector.ts',
|
|
12
|
+
'useGetConnector.ts',
|
|
13
|
+
'useCreateConnector.ts',
|
|
14
|
+
'useUpdateConnector.ts',
|
|
15
|
+
'useDeleteConnector.ts',
|
|
16
|
+
'zod-error-merger.ts',
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Format TypeScript source with Prettier.
|
|
20
|
+
* Falls back to unformatted code on error.
|
|
21
|
+
*/
|
|
22
|
+
async function formatCode(code, logger) {
|
|
23
|
+
try {
|
|
24
|
+
return await format(code, { parser: 'typescript' });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
logger.log.warn(`Prettier formatting failed: ${String(error)}`);
|
|
28
|
+
return code;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate headless connector composables from an OpenAPI spec.
|
|
33
|
+
*
|
|
34
|
+
* Steps:
|
|
35
|
+
* 1. Analyze the spec → ResourceMap (Schema Analyzer)
|
|
36
|
+
* 2. For each resource: generate connector source, format, write
|
|
37
|
+
* 3. Write an index barrel file
|
|
38
|
+
* 4. Copy runtime helpers to the user's project
|
|
39
|
+
*/
|
|
40
|
+
export async function generateConnectors(options, logger = createClackLogger()) {
|
|
41
|
+
const spinner = logger.spinner();
|
|
42
|
+
const outputDir = path.resolve(options.outputDir);
|
|
43
|
+
const composablesRelDir = options.composablesRelDir ?? '../use-async-data';
|
|
44
|
+
const runtimeRelDir = options.runtimeRelDir ?? '../runtime';
|
|
45
|
+
// ── 1. Analyze spec ───────────────────────────────────────────────────────
|
|
46
|
+
spinner.start('Analyzing OpenAPI spec');
|
|
47
|
+
const resourceMap = analyzeSpec(options.inputSpec);
|
|
48
|
+
spinner.stop(`Found ${resourceMap.size} resource(s)`);
|
|
49
|
+
if (resourceMap.size === 0) {
|
|
50
|
+
logger.log.warn('No resources found in spec — nothing to generate');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// ── 2. Prepare output directory ───────────────────────────────────────────
|
|
54
|
+
// emptyDir (not ensureDir) so stale connectors from previous runs are removed.
|
|
55
|
+
spinner.start('Preparing output directory');
|
|
56
|
+
await fs.emptyDir(outputDir);
|
|
57
|
+
spinner.stop('Output directory ready');
|
|
58
|
+
// ── 3. Generate connector files ───────────────────────────────────────────
|
|
59
|
+
spinner.start('Generating connector composables');
|
|
60
|
+
let successCount = 0;
|
|
61
|
+
let errorCount = 0;
|
|
62
|
+
const generatedNames = [];
|
|
63
|
+
for (const resource of resourceMap.values()) {
|
|
64
|
+
try {
|
|
65
|
+
const code = generateConnectorFile(resource, composablesRelDir, '../..', runtimeRelDir);
|
|
66
|
+
const formatted = await formatCode(code, logger);
|
|
67
|
+
const fileName = connectorFileName(resource.composableName);
|
|
68
|
+
const filePath = path.join(outputDir, fileName);
|
|
69
|
+
await fs.writeFile(filePath, formatted, 'utf-8');
|
|
70
|
+
generatedNames.push(resource.composableName);
|
|
71
|
+
successCount++;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.log.error(`Error generating ${resource.composableName}: ${String(error)}`);
|
|
75
|
+
errorCount++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
spinner.stop(`Generated ${successCount} connector(s)`);
|
|
79
|
+
// ── 4. Write barrel index ─────────────────────────────────────────────────
|
|
80
|
+
if (generatedNames.length > 0) {
|
|
81
|
+
try {
|
|
82
|
+
const indexCode = generateConnectorIndexFile(generatedNames);
|
|
83
|
+
const formattedIndex = await formatCode(indexCode, logger);
|
|
84
|
+
await fs.writeFile(path.join(outputDir, 'index.ts'), formattedIndex, 'utf-8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.log.warn(`Could not write connector index: ${String(error)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── 5. Copy runtime helpers ───────────────────────────────────────────────
|
|
91
|
+
// Runtime files live in src/ and must be physical .ts files in the user's project
|
|
92
|
+
// so Nuxt/Vite can type-check them.
|
|
93
|
+
//
|
|
94
|
+
// Path resolution trick:
|
|
95
|
+
// • During development (ts-node / tsx): __dirname ≈ src/generators/connectors/
|
|
96
|
+
// • After `tsc` build: __dirname ≈ dist/generators/connectors/
|
|
97
|
+
//
|
|
98
|
+
// In both cases we step up 3 levels and re-enter src/ to find the runtime sources.
|
|
99
|
+
spinner.start('Copying runtime files');
|
|
100
|
+
const runtimeDir = path.resolve(outputDir, runtimeRelDir);
|
|
101
|
+
await fs.ensureDir(runtimeDir); // ensureDir — other runtime files may live here too
|
|
102
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
103
|
+
const runtimeSrcDir = path.resolve(__dirname, '../../../src/generators/connectors/runtime');
|
|
104
|
+
for (const file of RUNTIME_FILES) {
|
|
105
|
+
const src = path.join(runtimeSrcDir, file);
|
|
106
|
+
const dest = path.join(runtimeDir, file);
|
|
107
|
+
await fs.copyFile(src, dest);
|
|
108
|
+
}
|
|
109
|
+
spinner.stop('Runtime files copied');
|
|
110
|
+
// ── 6. Summary ────────────────────────────────────────────────────────────
|
|
111
|
+
if (errorCount > 0) {
|
|
112
|
+
logger.log.warn(`Completed with ${errorCount} error(s)`);
|
|
113
|
+
}
|
|
114
|
+
logger.log.success(`Generated ${successCount} connector(s) in ${outputDir}`);
|
|
115
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connector-types.ts — Structural return type interfaces for the new connector system.
|
|
3
|
+
*
|
|
4
|
+
* Uses locally-defined minimal type aliases for Ref/ComputedRef so this file compiles
|
|
5
|
+
* in the CLI context (where Vue is not installed) and remains structurally compatible
|
|
6
|
+
* with Vue's actual types in the user's Nuxt project.
|
|
7
|
+
*
|
|
8
|
+
* Copied to the user's project alongside the generated connectors and runtime helpers.
|
|
9
|
+
*/
|
|
10
|
+
type Ref<T> = {
|
|
11
|
+
value: T;
|
|
12
|
+
};
|
|
13
|
+
type ComputedRef<T> = {
|
|
14
|
+
readonly value: T;
|
|
15
|
+
};
|
|
16
|
+
/** Operation name passed as context to connector-level callbacks. */
|
|
17
|
+
export type ConnectorOperation = 'create' | 'update' | 'delete' | 'get' | 'getAll';
|
|
18
|
+
export interface ConnectorCallbackContext {
|
|
19
|
+
operation: ConnectorOperation;
|
|
20
|
+
}
|
|
21
|
+
export interface ColumnDef {
|
|
22
|
+
key: string;
|
|
23
|
+
label: string;
|
|
24
|
+
type: string;
|
|
25
|
+
}
|
|
26
|
+
export interface FormFieldDef {
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
type: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
options?: {
|
|
32
|
+
label: string;
|
|
33
|
+
value: string;
|
|
34
|
+
}[];
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
hidden?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface PaginationState {
|
|
39
|
+
currentPage: number;
|
|
40
|
+
perPage: number;
|
|
41
|
+
total: number;
|
|
42
|
+
totalPages: number;
|
|
43
|
+
hasNextPage: boolean;
|
|
44
|
+
hasPrevPage: boolean;
|
|
45
|
+
goToPage: (page: number) => void;
|
|
46
|
+
nextPage: () => void;
|
|
47
|
+
prevPage: () => void;
|
|
48
|
+
setPerPage: (n: number) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface ConnectorUi {
|
|
51
|
+
isOpen: Ref<boolean>;
|
|
52
|
+
open: (...args: any[]) => void;
|
|
53
|
+
close: () => void;
|
|
54
|
+
}
|
|
55
|
+
export interface GetAllConnectorReturn<TRow = unknown> {
|
|
56
|
+
items: ComputedRef<TRow[]>;
|
|
57
|
+
columns: ComputedRef<ColumnDef[]>;
|
|
58
|
+
loading: ComputedRef<boolean>;
|
|
59
|
+
error: ComputedRef<unknown>;
|
|
60
|
+
pagination: ComputedRef<PaginationState | null>;
|
|
61
|
+
goToPage: (page: number) => void;
|
|
62
|
+
nextPage: () => void;
|
|
63
|
+
prevPage: () => void;
|
|
64
|
+
setPerPage: (n: number) => void;
|
|
65
|
+
selected: Ref<TRow[]>;
|
|
66
|
+
select: (item: TRow) => void;
|
|
67
|
+
deselect: (item: TRow) => void;
|
|
68
|
+
toggleSelect: (item: TRow) => void;
|
|
69
|
+
clearSelection: () => void;
|
|
70
|
+
load: (params?: unknown) => Promise<void>;
|
|
71
|
+
onSuccess: Ref<((items: TRow[]) => void) | null>;
|
|
72
|
+
onError: Ref<((err: unknown) => void) | null>;
|
|
73
|
+
}
|
|
74
|
+
export interface GetConnectorReturn<TItem = unknown> {
|
|
75
|
+
data: Ref<TItem | null>;
|
|
76
|
+
loading: Ref<boolean>;
|
|
77
|
+
error: Ref<unknown>;
|
|
78
|
+
fields: ComputedRef<FormFieldDef[]>;
|
|
79
|
+
load: (id: string | number) => Promise<TItem>;
|
|
80
|
+
clear: () => void;
|
|
81
|
+
onSuccess: (fn: (item: TItem) => void) => void;
|
|
82
|
+
onError: (fn: (err: unknown) => void) => void;
|
|
83
|
+
}
|
|
84
|
+
export interface CreateConnectorReturn<TInput = Record<string, unknown>, TOutput = TInput> {
|
|
85
|
+
model: Ref<Partial<TInput>>;
|
|
86
|
+
errors: Ref<Record<string, string[]>>;
|
|
87
|
+
loading: Ref<boolean>;
|
|
88
|
+
error: Ref<unknown>;
|
|
89
|
+
submitted: Ref<boolean>;
|
|
90
|
+
isValid: ComputedRef<boolean>;
|
|
91
|
+
hasErrors: ComputedRef<boolean>;
|
|
92
|
+
fields: ComputedRef<FormFieldDef[]>;
|
|
93
|
+
execute: (data?: Partial<TInput>) => Promise<TOutput | undefined>;
|
|
94
|
+
refresh: (data?: Partial<TInput>) => Promise<TOutput | undefined>;
|
|
95
|
+
reset: () => void;
|
|
96
|
+
setValues: (data: Partial<TInput>) => void;
|
|
97
|
+
setField: (key: keyof TInput, value: unknown) => void;
|
|
98
|
+
onSuccess: (fn: (data: TOutput) => void) => void;
|
|
99
|
+
onError: (fn: (err: unknown) => void) => void;
|
|
100
|
+
ui: {
|
|
101
|
+
isOpen: Ref<boolean>;
|
|
102
|
+
open: () => void;
|
|
103
|
+
close: () => void;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export interface UpdateConnectorReturn<TInput = Record<string, unknown>, TOutput = TInput> {
|
|
107
|
+
model: Ref<Partial<TInput>>;
|
|
108
|
+
errors: Ref<Record<string, string[]>>;
|
|
109
|
+
loading: Ref<boolean>;
|
|
110
|
+
error: Ref<unknown>;
|
|
111
|
+
submitted: Ref<boolean>;
|
|
112
|
+
isValid: ComputedRef<boolean>;
|
|
113
|
+
hasErrors: ComputedRef<boolean>;
|
|
114
|
+
fields: ComputedRef<FormFieldDef[]>;
|
|
115
|
+
targetId: Ref<string | number | null>;
|
|
116
|
+
load: (id: string | number) => Promise<void>;
|
|
117
|
+
execute: (id: string | number, data?: Partial<TInput>) => Promise<TOutput | undefined>;
|
|
118
|
+
refresh: (id: string | number, data?: Partial<TInput>) => Promise<TOutput | undefined>;
|
|
119
|
+
reset: () => void;
|
|
120
|
+
setValues: (data: Partial<TInput>) => void;
|
|
121
|
+
setField: (key: keyof TInput, value: unknown) => void;
|
|
122
|
+
onSuccess: (fn: (data: TOutput) => void) => void;
|
|
123
|
+
onError: (fn: (err: unknown) => void) => void;
|
|
124
|
+
ui: {
|
|
125
|
+
isOpen: Ref<boolean>;
|
|
126
|
+
open: (item?: Partial<TInput> & Record<string, unknown>) => void;
|
|
127
|
+
close: () => void;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export interface DeleteConnectorReturn<TItem = unknown> {
|
|
131
|
+
staged: Ref<TItem | null>;
|
|
132
|
+
hasStaged: ComputedRef<boolean>;
|
|
133
|
+
loading: Ref<boolean>;
|
|
134
|
+
error: Ref<unknown>;
|
|
135
|
+
stage: (item: TItem) => void;
|
|
136
|
+
cancel: () => void;
|
|
137
|
+
execute: (item?: TItem) => Promise<void>;
|
|
138
|
+
refresh: (item?: TItem) => Promise<void>;
|
|
139
|
+
onSuccess: (fn: (item: TItem) => void) => void;
|
|
140
|
+
onError: (fn: (err: unknown) => void) => void;
|
|
141
|
+
ui: {
|
|
142
|
+
isOpen: Ref<boolean>;
|
|
143
|
+
open: (item: TItem) => void;
|
|
144
|
+
close: () => void;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connector-types.ts — Structural return type interfaces for the new connector system.
|
|
3
|
+
*
|
|
4
|
+
* Uses locally-defined minimal type aliases for Ref/ComputedRef so this file compiles
|
|
5
|
+
* in the CLI context (where Vue is not installed) and remains structurally compatible
|
|
6
|
+
* with Vue's actual types in the user's Nuxt project.
|
|
7
|
+
*
|
|
8
|
+
* Copied to the user's project alongside the generated connectors and runtime helpers.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param url The endpoint URL string. e.g. '/pet'
|
|
3
|
+
* @param options Configuration: schema, fields, method, baseURL, callbacks, etc.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useCreateConnector(url: any, options?: {}): {
|
|
6
|
+
model: any;
|
|
7
|
+
errors: any;
|
|
8
|
+
loading: any;
|
|
9
|
+
error: any;
|
|
10
|
+
submitted: any;
|
|
11
|
+
isValid: any;
|
|
12
|
+
hasErrors: any;
|
|
13
|
+
fields: any;
|
|
14
|
+
execute: (data: any) => Promise<any>;
|
|
15
|
+
refresh: (data: any) => Promise<any>;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
setValues: (data: any) => void;
|
|
18
|
+
setField: (key: any, value: any) => void;
|
|
19
|
+
onSuccess: (fn: any) => void;
|
|
20
|
+
onError: (fn: any) => void;
|
|
21
|
+
ui: {
|
|
22
|
+
isOpen: any;
|
|
23
|
+
open(): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
};
|
|
26
|
+
};
|