nuxt-openapi-hyperfetch 0.2.8-alpha.1 β†’ 0.3.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +84 -6
  2. package/dist/generators/components/connector-generator/generator.js +1 -0
  3. package/dist/generators/components/connector-generator/templates.d.ts +1 -1
  4. package/dist/generators/components/connector-generator/templates.js +175 -44
  5. package/dist/generators/shared/runtime/connector-types.d.ts +104 -0
  6. package/dist/generators/shared/runtime/connector-types.js +10 -0
  7. package/dist/generators/shared/runtime/useFormConnector.js +8 -1
  8. package/dist/generators/shared/runtime/useListConnector.d.ts +5 -3
  9. package/dist/generators/shared/runtime/useListConnector.js +19 -10
  10. package/dist/generators/use-async-data/generator.js +4 -0
  11. package/dist/generators/use-async-data/runtime/useApiAsyncData.d.ts +8 -2
  12. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
  13. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.d.ts +9 -3
  14. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
  15. package/dist/generators/use-async-data/templates.js +24 -8
  16. package/dist/generators/use-fetch/generator.js +4 -0
  17. package/dist/generators/use-fetch/runtime/useApiRequest.d.ts +9 -2
  18. package/dist/generators/use-fetch/templates.js +9 -5
  19. package/dist/index.js +2 -1
  20. package/package.json +1 -1
  21. package/src/generators/components/connector-generator/generator.ts +1 -0
  22. package/src/generators/components/connector-generator/templates.ts +211 -44
  23. package/src/generators/shared/runtime/connector-types.ts +142 -0
  24. package/src/generators/shared/runtime/useFormConnector.ts +9 -1
  25. package/src/generators/shared/runtime/useListConnector.ts +22 -10
  26. package/src/generators/use-async-data/generator.ts +8 -0
  27. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +37 -9
  28. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +34 -12
  29. package/src/generators/use-async-data/templates.ts +24 -9
  30. package/src/generators/use-fetch/generator.ts +8 -0
  31. package/src/generators/use-fetch/runtime/useApiRequest.ts +34 -4
  32. package/src/generators/use-fetch/templates.ts +9 -6
  33. package/src/index.ts +2 -1
  34. package/dist/generators/tanstack-query/generator.d.ts +0 -5
  35. package/dist/generators/tanstack-query/generator.js +0 -11
package/README.md CHANGED
@@ -1,4 +1,8 @@
1
- ο»Ώ# πŸš€ Nuxt OpenAPI Generator
1
+ ο»Ώ<p align="center">
2
+ <img src="./public/nuxt-openapi-hyperfetch-logo.png" alt="Nuxt OpenAPI Hyperfetch logo" width="260" />
3
+ </p>
4
+
5
+ # πŸš€ Nuxt OpenAPI Generator
2
6
 
3
7
  **Generate type-safe, SSR-compatible Nuxt composables from OpenAPI/Swagger specifications.**
4
8
 
@@ -6,7 +10,7 @@
6
10
 
7
11
  ---
8
12
 
9
- Transform your API documentation into production-ready **100% Nuxt-native** codeβ€”`useFetch` composables, `useAsyncData` composables, and Nuxt Server Routesβ€”with full TypeScript support, lifecycle callbacks, and request interception. No third-party runtime, no wrappers: just Nuxt.
13
+ Transform your API documentation into production-ready **100% Nuxt-native** codeβ€”`useFetch` composables, `useAsyncData` composables, and Nuxt Server Routesβ€”with full TypeScript support, lifecycle callbacks, and request interception. Use it either as a CLI with `nxh generate` or as a Nuxt module wired directly from `nuxt.config.ts`.
10
14
 
11
15
  ---
12
16
 
@@ -52,6 +56,8 @@ export default {
52
56
 
53
57
  ## πŸ“¦ Installation
54
58
 
59
+ ### Use as CLI
60
+
55
61
  ```bash
56
62
  npm install -g nuxt-openapi-hyperfetch
57
63
  # or
@@ -66,11 +72,49 @@ Or use directly with npx:
66
72
  npx nuxt-openapi-hyperfetch generate
67
73
  ```
68
74
 
75
+ ### Use as Nuxt module
76
+
77
+ Install it in your Nuxt project:
78
+
79
+ ```bash
80
+ npm install -D nuxt-openapi-hyperfetch
81
+ # or
82
+ pnpm add -D nuxt-openapi-hyperfetch
83
+ # or
84
+ yarn add -D nuxt-openapi-hyperfetch
85
+ ```
86
+
87
+ Then register the module in `nuxt.config.ts`:
88
+
89
+ ```ts
90
+ export default defineNuxtConfig({
91
+ modules: ['nuxt-openapi-hyperfetch'],
92
+
93
+ openApiHyperFetch: {
94
+ input: './swagger.yaml',
95
+ output: './composables/api',
96
+ generators: ['useFetch', 'useAsyncData'],
97
+ backend: 'heyapi',
98
+ enableDevBuild: true,
99
+ enableProductionBuild: true,
100
+ enableAutoGeneration: false,
101
+ enableAutoImport: true,
102
+ createUseAsyncDataConnectors: false,
103
+ },
104
+ })
105
+ ```
106
+
107
+ The module uses `openApiHyperFetch` as its Nuxt config key and runs generation during Nuxt build hooks. If you include `nuxtServer` in `generators`, you can also configure `serverRoutePath` and `enableBff` here.
108
+
69
109
  ---
70
110
 
71
111
  ## πŸš€ Quick Start
72
112
 
73
- ### 1. Run the generator
113
+ ### 1. Run the generator with the CLI
114
+
115
+ <p align="center">
116
+ <img src="./public/nuxt-openapi-hyperfetch-cli.png" alt="Nuxt OpenAPI Hyperfetch CLI" width="720" />
117
+ </p>
74
118
 
75
119
  ```bash
76
120
  nxh generate
@@ -89,7 +133,41 @@ Or pass arguments directly:
89
133
  nxh generate -i ./swagger.yaml -o ./api
90
134
  ```
91
135
 
92
- ### 2. Generated output
136
+ ### 2. Or generate through the Nuxt module
137
+
138
+ If you prefer generation to run from Nuxt itself, add the module and configure it in `nuxt.config.ts`:
139
+
140
+ ```ts
141
+ export default defineNuxtConfig({
142
+ modules: ['nuxt-openapi-hyperfetch'],
143
+
144
+ openApiHyperFetch: {
145
+ input: './swagger.yaml',
146
+ output: './composables/api',
147
+ generators: ['useFetch', 'useAsyncData', 'nuxtServer'],
148
+ backend: 'heyapi',
149
+ serverRoutePath: 'server/routes/api',
150
+ enableBff: false,
151
+ enableAutoImport: true,
152
+ enableAutoGeneration: true,
153
+ },
154
+ })
155
+ ```
156
+
157
+ Useful module options:
158
+
159
+ - `input`: OpenAPI file path relative to the Nuxt root.
160
+ - `output`: Directory where the generated SDK/composables are written.
161
+ - `generators`: Any combination of `useFetch`, `useAsyncData`, and `nuxtServer`.
162
+ - `backend`: `heyapi` or `official`.
163
+ - `enableDevBuild` / `enableProductionBuild`: Control generation before dev/build.
164
+ - `enableAutoGeneration`: Regenerate when the input spec changes in dev mode.
165
+ - `enableAutoImport`: Auto-register generated composables for Nuxt auto-imports.
166
+ - `createUseAsyncDataConnectors`: Generate headless connectors on top of `useAsyncData`.
167
+ - `serverRoutePath`: Output path for generated Nuxt server routes.
168
+ - `enableBff`: Enable the BFF transformer layer for server routes.
169
+
170
+ ### 3. Generated output
93
171
 
94
172
  ```
95
173
  api/
@@ -110,7 +188,7 @@ api/
110
188
  +-- index.ts
111
189
  ```
112
190
 
113
- ### 3. Configure the API base URL
191
+ ### 4. Configure the API base URL
114
192
 
115
193
  Add to `nuxt.config.ts`:
116
194
 
@@ -132,7 +210,7 @@ NUXT_PUBLIC_API_BASE_URL=https://api.example.com
132
210
 
133
211
  All generated `useFetch` and `useAsyncData` composables will automatically use this as `baseURL`. You can still override it per-composable via `options.baseURL`.
134
212
 
135
- ### 4. Use in your Nuxt app
213
+ ### 5. Use in your Nuxt app
136
214
 
137
215
  ```vue
138
216
  <script setup lang="ts">
@@ -7,6 +7,7 @@ import { generateConnectorFile, connectorFileName, generateConnectorIndexFile, }
7
7
  import { createClackLogger } from '../../../cli/logger.js';
8
8
  // Runtime files that must be copied to the user's project
9
9
  const RUNTIME_FILES = [
10
+ 'connector-types.ts',
10
11
  'useListConnector.ts',
11
12
  'useDetailConnector.ts',
12
13
  'useFormConnector.ts',
@@ -6,7 +6,7 @@ import type { ResourceInfo } from '../schema-analyzer/types.js';
6
6
  * @param composablesRelDir Relative path from the connector dir to the
7
7
  * useAsyncData composables dir (e.g. '../use-async-data')
8
8
  */
9
- export declare function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string): string;
9
+ export declare function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string, sdkRelDir?: string): string;
10
10
  /**
11
11
  * Derive the output filename for a connector.
12
12
  * 'usePetsConnector' β†’ 'use-pets-connector.ts'
@@ -12,7 +12,6 @@ function generateFileHeader() {
12
12
  */
13
13
 
14
14
  /* eslint-disable */
15
- // @ts-nocheck
16
15
  `;
17
16
  }
18
17
  // ─── Naming helpers ───────────────────────────────────────────────────────────
@@ -34,16 +33,33 @@ function toFileName(composableName) {
34
33
  /**
35
34
  * Build all `import` lines for a resource connector.
36
35
  */
37
- function buildImports(resource, composablesRelDir) {
36
+ function buildImports(resource, composablesRelDir, sdkRelDir) {
38
37
  const lines = [];
39
38
  // zod
40
39
  lines.push(`import { z } from 'zod';`);
41
40
  lines.push('');
42
- // runtime helpers (Nuxt alias β€” set up by the Nuxt module)
43
- const runtimeHelpers = [];
41
+ // connector-types β€” structural interfaces for return types
42
+ const connectorTypeImports = ['ListConnectorReturn'];
43
+ if (resource.detailEndpoint) {
44
+ connectorTypeImports.push('DetailConnectorReturn');
45
+ }
46
+ if (resource.createEndpoint || resource.updateEndpoint) {
47
+ connectorTypeImports.push('FormConnectorReturn');
48
+ }
49
+ if (resource.deleteEndpoint) {
50
+ connectorTypeImports.push('DeleteConnectorReturn');
51
+ }
52
+ lines.push(`import type { ${connectorTypeImports.join(', ')} } from '#nxh/runtime/connector-types';`);
53
+ lines.push('');
54
+ // SDK request/response types (for the params overload signature)
44
55
  if (resource.listEndpoint) {
45
- runtimeHelpers.push('useListConnector');
56
+ const requestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
57
+ lines.push(`import type { ${requestTypeName} } from '${sdkRelDir}';`);
58
+ lines.push('');
46
59
  }
60
+ // runtime helpers (Nuxt alias β€” set up by the Nuxt module)
61
+ // useListConnector is always imported to support the optional factory pattern
62
+ const runtimeHelpers = ['useListConnector'];
47
63
  if (resource.detailEndpoint) {
48
64
  runtimeHelpers.push('useDetailConnector');
49
65
  }
@@ -103,63 +119,157 @@ function buildZodSchemas(resource) {
103
119
  }
104
120
  return lines.join('\n');
105
121
  }
122
+ /**
123
+ * Build a const array with the column definitions inferred from the resource.
124
+ * Returns an empty string if the resource has no columns.
125
+ */
126
+ function buildColumns(resource) {
127
+ if (!resource.columns || resource.columns.length === 0) {
128
+ return '';
129
+ }
130
+ const camel = resource.composableName
131
+ .replace(/^use/, '')
132
+ .replace(/Connector$/, '')
133
+ .replace(/^./, (c) => c.toLowerCase());
134
+ const varName = `${camel}Columns`;
135
+ const entries = resource.columns
136
+ .map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
137
+ .join(',\n');
138
+ return `const ${varName} = [\n${entries},\n];`;
139
+ }
140
+ /**
141
+ * Build the TypeScript options interface for a connector.
142
+ * Only includes fields relevant to the endpoints present on the resource.
143
+ */
144
+ function buildOptionsInterface(resource) {
145
+ const typeName = `${pascalCase(resource.composableName)}Options`;
146
+ const hasColumns = resource.columns && resource.columns.length > 0;
147
+ const fields = [];
148
+ if (resource.listEndpoint && hasColumns) {
149
+ fields.push(` columnLabels?: Record<string, string>;`);
150
+ fields.push(` columnLabel?: (key: string) => string;`);
151
+ }
152
+ if (resource.createEndpoint && resource.zodSchemas.create) {
153
+ fields.push(` createSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
154
+ }
155
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
156
+ fields.push(` updateSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
157
+ }
158
+ if (fields.length === 0) {
159
+ return `type ${typeName} = Record<string, never>;`;
160
+ }
161
+ return [`interface ${typeName} {`, ...fields, `}`].join('\n');
162
+ }
163
+ /**
164
+ * Build the TypeScript return type for a connector.
165
+ */
166
+ function buildReturnType(resource) {
167
+ const pascal = pascalCase(resource.name);
168
+ const typeName = `${pascalCase(resource.composableName)}Return`;
169
+ const fields = [];
170
+ // table is always present in the return type:
171
+ // - if listEndpoint exists β†’ ListConnectorReturn<T> (always defined)
172
+ // - if no listEndpoint β†’ ListConnectorReturn<unknown> | undefined (only when factory passed)
173
+ if (resource.listEndpoint) {
174
+ fields.push(` table: ListConnectorReturn<${pascal}>;`);
175
+ }
176
+ else {
177
+ fields.push(` table: ListConnectorReturn<unknown> | undefined;`);
178
+ }
179
+ if (resource.detailEndpoint) {
180
+ fields.push(` detail: DetailConnectorReturn<${pascal}>;`);
181
+ }
182
+ if (resource.createEndpoint) {
183
+ const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
184
+ fields.push(` createForm: FormConnectorReturn<${inputType}>;`);
185
+ }
186
+ if (resource.updateEndpoint) {
187
+ const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
188
+ fields.push(` updateForm: FormConnectorReturn<${inputType}>;`);
189
+ }
190
+ if (resource.deleteEndpoint) {
191
+ fields.push(` deleteAction: DeleteConnectorReturn<${pascal}>;`);
192
+ }
193
+ return [`type ${typeName} = {`, ...fields, `};`].join('\n');
194
+ }
106
195
  /**
107
196
  * Build the body of the exported connector function.
108
197
  */
109
198
  function buildFunctionBody(resource) {
110
199
  const pascal = pascalCase(resource.name);
200
+ const hasColumns = resource.columns && resource.columns.length > 0;
201
+ const camel = resource.composableName
202
+ .replace(/^use/, '')
203
+ .replace(/Connector$/, '')
204
+ .replace(/^./, (c) => c.toLowerCase());
205
+ const columnsVar = `${camel}Columns`;
111
206
  const subConnectors = [];
207
+ // Derived type names β€” must match buildOptionsInterface / buildReturnType
208
+ const optionsTypeName = `${pascalCase(resource.composableName)}Options`;
209
+ const returnTypeName = `${pascalCase(resource.composableName)}Return`;
210
+ // Destructure options param β€” only what's relevant for this resource
211
+ const optionKeys = [];
212
+ if (resource.listEndpoint && hasColumns) {
213
+ optionKeys.push('columnLabels', 'columnLabel');
214
+ }
215
+ if (resource.createEndpoint && resource.zodSchemas.create) {
216
+ optionKeys.push('createSchema');
217
+ }
218
+ if (resource.updateEndpoint && resource.zodSchemas.update) {
219
+ optionKeys.push('updateSchema');
220
+ }
221
+ const optionsDestructure = optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
222
+ // ── List / table sub-connector ─────────────────────────────────────────────
112
223
  if (resource.listEndpoint) {
113
224
  const fn = toAsyncDataName(resource.listEndpoint.operationId);
114
- // paginated: true tells useListConnector to expose pagination helpers
115
- // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
116
- // We set it whenever the spec declares a list endpoint that has a response schema,
117
- // which is a reliable proxy for "this API returns structured data worth paginating".
118
- const opts = resource.listEndpoint.responseSchema ? '{ paginated: true }' : '{}';
119
- subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
225
+ const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
226
+ const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
227
+ const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
228
+ const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
229
+ const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
230
+ const opts = allArgs ? `{ ${allArgs} }` : '{}';
231
+ // Factory: if the first arg is a function the user provided their own composable;
232
+ // otherwise build a default factory from the plain params object.
233
+ subConnectors.push(` const isFactory = typeof paramsOrSource === 'function';`, ` const listFactory = isFactory`, ` ? (paramsOrSource as () => unknown)`, ` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName});`, ` const table = useListConnector(listFactory, ${opts}) as unknown as ListConnectorReturn<${pascal}>;`);
234
+ }
235
+ else {
236
+ // No list endpoint β€” support optional factory for developer-provided list
237
+ subConnectors.push(` const table = paramsOrSource`, ` ? (useListConnector(paramsOrSource as () => unknown, {}) as unknown as ListConnectorReturn<unknown>)`, ` : undefined;`);
120
238
  }
121
239
  if (resource.detailEndpoint) {
122
240
  const fn = toAsyncDataName(resource.detailEndpoint.operationId);
123
- subConnectors.push(` const detail = useDetailConnector(${fn});`);
241
+ subConnectors.push(` const detail = useDetailConnector(${fn}) as unknown as DetailConnectorReturn<${pascal}>;`);
124
242
  }
125
243
  if (resource.createEndpoint) {
126
244
  const fn = toAsyncDataName(resource.createEndpoint.operationId);
127
- const schemaArg = resource.zodSchemas.create ? `{ schema: ${pascal}CreateSchema }` : '{}';
128
- subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
245
+ const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
246
+ const schemaArg = resource.zodSchemas.create
247
+ ? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
248
+ : '{}';
249
+ subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
129
250
  }
130
251
  if (resource.updateEndpoint) {
131
252
  const fn = toAsyncDataName(resource.updateEndpoint.operationId);
132
253
  const hasDetail = !!resource.detailEndpoint;
133
- // Build the options argument for useFormConnector:
134
- // schema β†’ Zod schema for client-side validation before submission
135
- // loadWith β†’ reference to the detail connector so the form auto-fills
136
- // when detail.item changes (user clicks "Edit" on a row)
137
- //
138
- // Four combinations are possible depending on what the spec provides:
254
+ const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
139
255
  let schemaArg = '{}';
140
256
  if (resource.zodSchemas.update && hasDetail) {
141
- // Best case: validate AND pre-fill from detail
142
- schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
257
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
143
258
  }
144
259
  else if (resource.zodSchemas.update) {
145
- // Validate, but no detail endpoint to pre-fill from
146
- schemaArg = `{ schema: ${pascal}UpdateSchema }`;
260
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
147
261
  }
148
262
  else if (hasDetail) {
149
- // No Zod schema (no request body in spec), but still pre-fill from detail
150
263
  schemaArg = `{ loadWith: detail }`;
151
264
  }
152
- subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg});`);
265
+ subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
153
266
  }
154
267
  if (resource.deleteEndpoint) {
155
268
  const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
156
- subConnectors.push(` const deleteAction = useDeleteConnector(${fn});`);
157
- }
158
- // Return object β€” only include what was built
159
- const returnKeys = [];
160
- if (resource.listEndpoint) {
161
- returnKeys.push('table');
269
+ subConnectors.push(` const deleteAction = useDeleteConnector(${fn}) as unknown as DeleteConnectorReturn<${pascal}>;`);
162
270
  }
271
+ // Return object β€” always includes table (undefined when no list + no factory)
272
+ const returnKeys = ['table'];
163
273
  if (resource.detailEndpoint) {
164
274
  returnKeys.push('detail');
165
275
  }
@@ -172,13 +282,27 @@ function buildFunctionBody(resource) {
172
282
  if (resource.deleteEndpoint) {
173
283
  returnKeys.push('deleteAction');
174
284
  }
175
- const returnStatement = ` return { ${returnKeys.join(', ')} };`;
176
- return [
177
- `export function ${resource.composableName}() {`,
178
- ...subConnectors,
179
- returnStatement,
180
- `}`,
181
- ].join('\n');
285
+ const returnStatement = ` return { ${returnKeys.join(', ')} } as ${returnTypeName};`;
286
+ // ── Function signature ─────────────────────────────────────────────────────
287
+ // Resources WITH a list endpoint: two overloads (factory | params).
288
+ // Resources WITHOUT a list endpoint: single signature with optional factory.
289
+ const lines = [];
290
+ if (resource.listEndpoint) {
291
+ const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
292
+ 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} {`);
293
+ }
294
+ else {
295
+ lines.push(`export function ${resource.composableName}(source?: () => unknown, options: ${optionsTypeName} = {}): ${returnTypeName} {`);
296
+ // Alias so the body can use paramsOrSource uniformly
297
+ lines.push(` const paramsOrSource = source;`);
298
+ }
299
+ if (optionsDestructure.trim()) {
300
+ lines.push(optionsDestructure.trimEnd());
301
+ }
302
+ lines.push(...subConnectors);
303
+ lines.push(returnStatement);
304
+ lines.push(`}`);
305
+ return lines.join('\n');
182
306
  }
183
307
  // ─── Public API ───────────────────────────────────────────────────────────────
184
308
  /**
@@ -188,18 +312,25 @@ function buildFunctionBody(resource) {
188
312
  * @param composablesRelDir Relative path from the connector dir to the
189
313
  * useAsyncData composables dir (e.g. '../use-async-data')
190
314
  */
191
- export function generateConnectorFile(resource, composablesRelDir) {
315
+ export function generateConnectorFile(resource, composablesRelDir, sdkRelDir = '../..') {
192
316
  const header = generateFileHeader();
193
- const imports = buildImports(resource, composablesRelDir);
317
+ const imports = buildImports(resource, composablesRelDir, sdkRelDir);
194
318
  const schemas = buildZodSchemas(resource);
319
+ const columns = buildColumns(resource);
320
+ const optionsInterface = buildOptionsInterface(resource);
321
+ const returnType = buildReturnType(resource);
195
322
  const fn = buildFunctionBody(resource);
196
- // Assemble file: header + imports + (optional) Zod blocks + function body.
197
- // Each section ends with its own trailing newline; join with \n adds one blank
198
- // line between sections, which matches Prettier's output for this structure.
323
+ // Assemble file: header + imports + (optional) Zod blocks + columns const +
324
+ // options interface + return type + function body.
199
325
  const parts = [header, imports];
200
326
  if (schemas.trim()) {
201
327
  parts.push(schemas);
202
328
  }
329
+ if (columns.trim()) {
330
+ parts.push(columns);
331
+ }
332
+ parts.push(optionsInterface);
333
+ parts.push(returnType);
203
334
  parts.push(fn);
204
335
  return parts.join('\n') + '\n';
205
336
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * connector-types.ts β€” Structural return type interfaces for the 4 runtime connectors.
3
+ *
4
+ * Uses locally-defined minimal type aliases for Ref/ComputedRef/ShallowRef so this
5
+ * file compiles in the CLI context (where Vue is not installed) and remains
6
+ * structurally compatible 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 ShallowRef<T> = {
14
+ value: T;
15
+ };
16
+ type ComputedRef<T> = {
17
+ readonly value: T;
18
+ };
19
+ export interface ColumnDef {
20
+ key: string;
21
+ label: string;
22
+ type: string;
23
+ }
24
+ export interface FormFieldDef {
25
+ key: string;
26
+ label: string;
27
+ type: string;
28
+ required: boolean;
29
+ options?: {
30
+ label: string;
31
+ value: string;
32
+ }[];
33
+ placeholder?: string;
34
+ hidden?: boolean;
35
+ }
36
+ export interface PaginationState {
37
+ page: number;
38
+ perPage: number;
39
+ total: number;
40
+ totalPages: number;
41
+ goToPage: (page: number) => void;
42
+ nextPage: () => void;
43
+ prevPage: () => void;
44
+ setPerPage: (n: number) => void;
45
+ }
46
+ export interface ListConnectorReturn<TRow = unknown> {
47
+ rows: ComputedRef<TRow[]>;
48
+ columns: ComputedRef<ColumnDef[]>;
49
+ loading: ComputedRef<boolean>;
50
+ error: ComputedRef<unknown>;
51
+ pagination: ComputedRef<PaginationState | null>;
52
+ goToPage: (page: number) => void;
53
+ nextPage: () => void;
54
+ prevPage: () => void;
55
+ setPerPage: (n: number) => void;
56
+ selected: Ref<TRow[]>;
57
+ onRowSelect: (row: TRow) => void;
58
+ clearSelection: () => void;
59
+ refresh: () => void;
60
+ create: () => void;
61
+ update: (row: TRow) => void;
62
+ remove: (row: TRow) => void;
63
+ _createTrigger: Ref<number>;
64
+ _updateTarget: ShallowRef<TRow | null>;
65
+ _deleteTarget: ShallowRef<TRow | null>;
66
+ }
67
+ export interface DetailConnectorReturn<TItem = unknown> {
68
+ item: ComputedRef<TItem | null>;
69
+ loading: ComputedRef<boolean>;
70
+ error: ComputedRef<unknown>;
71
+ fields: ComputedRef<FormFieldDef[]>;
72
+ load: (id: string | number) => Promise<void>;
73
+ clear: () => void;
74
+ _composable: unknown;
75
+ _currentId: Ref<string | number | null>;
76
+ }
77
+ export interface FormConnectorReturn<TInput = Record<string, unknown>> {
78
+ model: Ref<Partial<TInput>>;
79
+ errors: Ref<Record<string, string[]>>;
80
+ loading: Ref<boolean>;
81
+ submitError: Ref<unknown>;
82
+ submitted: Ref<boolean>;
83
+ isValid: ComputedRef<boolean>;
84
+ hasErrors: ComputedRef<boolean>;
85
+ fields: ComputedRef<FormFieldDef[]>;
86
+ onSuccess: Ref<((data: unknown) => void) | null>;
87
+ onError: Ref<((err: unknown) => void) | null>;
88
+ submit: () => Promise<void>;
89
+ reset: () => void;
90
+ setValues: (data: Partial<TInput>) => void;
91
+ }
92
+ export interface DeleteConnectorReturn<TItem = unknown> {
93
+ target: Ref<TItem | null>;
94
+ isOpen: Ref<boolean>;
95
+ loading: Ref<boolean>;
96
+ error: Ref<unknown>;
97
+ hasTarget: ComputedRef<boolean>;
98
+ onSuccess: Ref<((item: TItem) => void) | null>;
99
+ onError: Ref<((err: unknown) => void) | null>;
100
+ setTarget: (item: TItem) => void;
101
+ cancel: () => void;
102
+ confirm: () => Promise<void>;
103
+ }
104
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * connector-types.ts β€” Structural return type interfaces for the 4 runtime connectors.
3
+ *
4
+ * Uses locally-defined minimal type aliases for Ref/ComputedRef/ShallowRef so this
5
+ * file compiles in the CLI context (where Vue is not installed) and remains
6
+ * structurally compatible 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 {};
@@ -18,7 +18,14 @@ import { mergeZodErrors } from './zod-error-merger.js';
18
18
  * @param options { schema, fields, loadWith?, errorConfig? }
19
19
  */
20
20
  export function useFormConnector(composableFn, options = {}) {
21
- const { schema, fields = [], loadWith = null, errorConfig = {} } = options;
21
+ const { schema: baseSchema, schemaOverride, fields = [], loadWith = null, errorConfig = {} } = options;
22
+ // Resolve the active schema:
23
+ // schemaOverride(base) β€” extend or refine the generated schema
24
+ // schemaOverride β€” replace the generated schema entirely
25
+ // baseSchema β€” the generated schema unchanged (default)
26
+ const schema = schemaOverride
27
+ ? (typeof schemaOverride === 'function' ? schemaOverride(baseSchema) : schemaOverride)
28
+ : baseSchema;
22
29
  // ── Form state ─────────────────────────────────────────────────────────────
23
30
  const model = ref({});
24
31
  const errors = ref({});
@@ -1,8 +1,10 @@
1
1
  /**
2
- * @param composableFn The generated useAsyncData composable, e.g. useAsyncDataGetPets
3
- * @param options Configuration for the list connector
2
+ * @param factory A zero-argument function that calls and returns the underlying
3
+ * useAsyncData composable, e.g. () => useAsyncDataGetPets(params)
4
+ * The factory is called once during connector setup (inside setup()).
5
+ * @param options Configuration for the list connector
4
6
  */
5
- export declare function useListConnector(composableFn: any, options?: {}): {
7
+ export declare function useListConnector(factory: any, options?: {}): {
6
8
  rows: any;
7
9
  columns: any;
8
10
  loading: any;
@@ -12,13 +12,15 @@
12
12
  */
13
13
  import { ref, computed, shallowRef } from 'vue';
14
14
  /**
15
- * @param composableFn The generated useAsyncData composable, e.g. useAsyncDataGetPets
16
- * @param options Configuration for the list connector
15
+ * @param factory A zero-argument function that calls and returns the underlying
16
+ * useAsyncData composable, e.g. () => useAsyncDataGetPets(params)
17
+ * The factory is called once during connector setup (inside setup()).
18
+ * @param options Configuration for the list connector
17
19
  */
18
- export function useListConnector(composableFn, options = {}) {
19
- const { paginated = false, columns = [] } = options;
20
+ export function useListConnector(factory, options = {}) {
21
+ const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
20
22
  // ── Execute the underlying composable ──────────────────────────────────────
21
- const composable = composableFn({ paginated });
23
+ const composable = factory();
22
24
  // ── Derived state ──────────────────────────────────────────────────────────
23
25
  const rows = computed(() => {
24
26
  const data = composable.data?.value;
@@ -36,16 +38,16 @@ export function useListConnector(composableFn, options = {}) {
36
38
  // Pagination β€” passthrough from the underlying composable when paginated: true
37
39
  const pagination = computed(() => composable.pagination?.value ?? null);
38
40
  function goToPage(page) {
39
- composable.goToPage?.(page);
41
+ composable.pagination?.value?.goToPage?.(page);
40
42
  }
41
43
  function nextPage() {
42
- composable.nextPage?.();
44
+ composable.pagination?.value?.nextPage?.();
43
45
  }
44
46
  function prevPage() {
45
- composable.prevPage?.();
47
+ composable.pagination?.value?.prevPage?.();
46
48
  }
47
49
  function setPerPage(n) {
48
- composable.setPerPage?.(n);
50
+ composable.pagination?.value?.setPerPage?.(n);
49
51
  }
50
52
  // ── Row selection ──────────────────────────────────────────────────────────
51
53
  const selected = ref([]);
@@ -95,10 +97,17 @@ export function useListConnector(composableFn, options = {}) {
95
97
  function remove(row) {
96
98
  _deleteTarget.value = row;
97
99
  }
100
+ // Apply label overrides: columnLabel function takes priority over columnLabels map
101
+ const resolvedColumns = computed(() => columns.map((col) => ({
102
+ ...col,
103
+ label: columnLabel
104
+ ? columnLabel(col.key)
105
+ : (columnLabels[col.key] ?? col.label),
106
+ })));
98
107
  return {
99
108
  // State
100
109
  rows,
101
- columns: computed(() => columns),
110
+ columns: resolvedColumns,
102
111
  loading,
103
112
  error,
104
113
  // Pagination