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.
- package/README.md +84 -6
- package/dist/generators/components/connector-generator/generator.js +1 -0
- package/dist/generators/components/connector-generator/templates.d.ts +1 -1
- package/dist/generators/components/connector-generator/templates.js +175 -44
- package/dist/generators/shared/runtime/connector-types.d.ts +104 -0
- package/dist/generators/shared/runtime/connector-types.js +10 -0
- package/dist/generators/shared/runtime/useFormConnector.js +8 -1
- package/dist/generators/shared/runtime/useListConnector.d.ts +5 -3
- package/dist/generators/shared/runtime/useListConnector.js +19 -10
- package/dist/generators/use-async-data/generator.js +4 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncData.d.ts +8 -2
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.d.ts +9 -3
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
- package/dist/generators/use-async-data/templates.js +24 -8
- package/dist/generators/use-fetch/generator.js +4 -0
- package/dist/generators/use-fetch/runtime/useApiRequest.d.ts +9 -2
- package/dist/generators/use-fetch/templates.js +9 -5
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/generators/components/connector-generator/generator.ts +1 -0
- package/src/generators/components/connector-generator/templates.ts +211 -44
- package/src/generators/shared/runtime/connector-types.ts +142 -0
- package/src/generators/shared/runtime/useFormConnector.ts +9 -1
- package/src/generators/shared/runtime/useListConnector.ts +22 -10
- package/src/generators/use-async-data/generator.ts +8 -0
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +37 -9
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +34 -12
- package/src/generators/use-async-data/templates.ts +24 -9
- package/src/generators/use-fetch/generator.ts +8 -0
- package/src/generators/use-fetch/runtime/useApiRequest.ts +34 -4
- package/src/generators/use-fetch/templates.ts +9 -6
- package/src/index.ts +2 -1
- package/dist/generators/tanstack-query/generator.d.ts +0 -5
- package/dist/generators/tanstack-query/generator.js +0 -11
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
ο»Ώ
|
|
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.
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
//
|
|
43
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 +
|
|
197
|
-
//
|
|
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
|
|
3
|
-
*
|
|
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(
|
|
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
|
|
16
|
-
*
|
|
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(
|
|
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 =
|
|
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:
|
|
110
|
+
columns: resolvedColumns,
|
|
102
111
|
loading,
|
|
103
112
|
error,
|
|
104
113
|
// Pagination
|