nuxt-openapi-hyperfetch 0.2.8-alpha.1 β 0.3.0-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/templates.js +56 -7
- package/dist/generators/shared/runtime/useFormConnector.js +8 -1
- package/dist/generators/shared/runtime/useListConnector.js +13 -6
- package/dist/generators/use-async-data/generator.js +4 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
- package/dist/generators/use-fetch/generator.js +4 -0
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/generators/components/connector-generator/templates.ts +60 -7
- package/src/generators/shared/runtime/useFormConnector.ts +9 -1
- package/src/generators/shared/runtime/useListConnector.ts +16 -6
- package/src/generators/use-async-data/generator.ts +8 -0
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +4 -4
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +4 -4
- package/src/generators/use-fetch/generator.ts +8 -0
- 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">
|
|
@@ -103,19 +103,59 @@ function buildZodSchemas(resource) {
|
|
|
103
103
|
}
|
|
104
104
|
return lines.join('\n');
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Build a const array with the column definitions inferred from the resource.
|
|
108
|
+
* Returns an empty string if the resource has no columns.
|
|
109
|
+
*/
|
|
110
|
+
function buildColumns(resource) {
|
|
111
|
+
if (!resource.columns || resource.columns.length === 0) {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
const camel = resource.composableName
|
|
115
|
+
.replace(/^use/, '')
|
|
116
|
+
.replace(/Connector$/, '')
|
|
117
|
+
.replace(/^./, (c) => c.toLowerCase());
|
|
118
|
+
const varName = `${camel}Columns`;
|
|
119
|
+
const entries = resource.columns
|
|
120
|
+
.map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
|
|
121
|
+
.join(',\n');
|
|
122
|
+
return `const ${varName} = [\n${entries},\n];`;
|
|
123
|
+
}
|
|
106
124
|
/**
|
|
107
125
|
* Build the body of the exported connector function.
|
|
108
126
|
*/
|
|
109
127
|
function buildFunctionBody(resource) {
|
|
110
128
|
const pascal = pascalCase(resource.name);
|
|
129
|
+
const hasColumns = resource.columns && resource.columns.length > 0;
|
|
130
|
+
const camel = resource.composableName
|
|
131
|
+
.replace(/^use/, '')
|
|
132
|
+
.replace(/Connector$/, '')
|
|
133
|
+
.replace(/^./, (c) => c.toLowerCase());
|
|
134
|
+
const columnsVar = `${camel}Columns`;
|
|
111
135
|
const subConnectors = [];
|
|
136
|
+
// Destructure options param β only what's relevant for this resource
|
|
137
|
+
const optionKeys = [];
|
|
138
|
+
if (resource.listEndpoint && hasColumns) {
|
|
139
|
+
optionKeys.push('columnLabels', 'columnLabel');
|
|
140
|
+
}
|
|
141
|
+
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
142
|
+
optionKeys.push('createSchema');
|
|
143
|
+
}
|
|
144
|
+
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
145
|
+
optionKeys.push('updateSchema');
|
|
146
|
+
}
|
|
147
|
+
const optionsDestructure = optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
|
|
112
148
|
if (resource.listEndpoint) {
|
|
113
149
|
const fn = toAsyncDataName(resource.listEndpoint.operationId);
|
|
114
150
|
// paginated: true tells useListConnector to expose pagination helpers
|
|
115
151
|
// (goToPage, nextPage, prevPage, setPerPage, pagination ref).
|
|
116
152
|
// We set it whenever the spec declares a list endpoint that has a response schema,
|
|
117
153
|
// which is a reliable proxy for "this API returns structured data worth paginating".
|
|
118
|
-
const
|
|
154
|
+
const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
|
|
155
|
+
const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
|
|
156
|
+
const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
|
|
157
|
+
const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
|
|
158
|
+
const opts = allArgs ? `{ ${allArgs} }` : '{}';
|
|
119
159
|
subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
|
|
120
160
|
}
|
|
121
161
|
if (resource.detailEndpoint) {
|
|
@@ -124,7 +164,9 @@ function buildFunctionBody(resource) {
|
|
|
124
164
|
}
|
|
125
165
|
if (resource.createEndpoint) {
|
|
126
166
|
const fn = toAsyncDataName(resource.createEndpoint.operationId);
|
|
127
|
-
const schemaArg = resource.zodSchemas.create
|
|
167
|
+
const schemaArg = resource.zodSchemas.create
|
|
168
|
+
? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
|
|
169
|
+
: '{}';
|
|
128
170
|
subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
|
|
129
171
|
}
|
|
130
172
|
if (resource.updateEndpoint) {
|
|
@@ -139,11 +181,11 @@ function buildFunctionBody(resource) {
|
|
|
139
181
|
let schemaArg = '{}';
|
|
140
182
|
if (resource.zodSchemas.update && hasDetail) {
|
|
141
183
|
// Best case: validate AND pre-fill from detail
|
|
142
|
-
schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
|
|
184
|
+
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
|
|
143
185
|
}
|
|
144
186
|
else if (resource.zodSchemas.update) {
|
|
145
187
|
// Validate, but no detail endpoint to pre-fill from
|
|
146
|
-
schemaArg = `{ schema: ${pascal}UpdateSchema }`;
|
|
188
|
+
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
|
|
147
189
|
}
|
|
148
190
|
else if (hasDetail) {
|
|
149
191
|
// No Zod schema (no request body in spec), but still pre-fill from detail
|
|
@@ -174,11 +216,14 @@ function buildFunctionBody(resource) {
|
|
|
174
216
|
}
|
|
175
217
|
const returnStatement = ` return { ${returnKeys.join(', ')} };`;
|
|
176
218
|
return [
|
|
177
|
-
`export function ${resource.composableName}() {`,
|
|
219
|
+
`export function ${resource.composableName}(options = {}) {`,
|
|
220
|
+
optionsDestructure.trimEnd(),
|
|
178
221
|
...subConnectors,
|
|
179
222
|
returnStatement,
|
|
180
223
|
`}`,
|
|
181
|
-
]
|
|
224
|
+
]
|
|
225
|
+
.filter((s) => s !== '')
|
|
226
|
+
.join('\n');
|
|
182
227
|
}
|
|
183
228
|
// βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
184
229
|
/**
|
|
@@ -192,14 +237,18 @@ export function generateConnectorFile(resource, composablesRelDir) {
|
|
|
192
237
|
const header = generateFileHeader();
|
|
193
238
|
const imports = buildImports(resource, composablesRelDir);
|
|
194
239
|
const schemas = buildZodSchemas(resource);
|
|
240
|
+
const columns = buildColumns(resource);
|
|
195
241
|
const fn = buildFunctionBody(resource);
|
|
196
|
-
// Assemble file: header + imports + (optional) Zod blocks + function body.
|
|
242
|
+
// Assemble file: header + imports + (optional) Zod blocks + columns const + function body.
|
|
197
243
|
// Each section ends with its own trailing newline; join with \n adds one blank
|
|
198
244
|
// line between sections, which matches Prettier's output for this structure.
|
|
199
245
|
const parts = [header, imports];
|
|
200
246
|
if (schemas.trim()) {
|
|
201
247
|
parts.push(schemas);
|
|
202
248
|
}
|
|
249
|
+
if (columns.trim()) {
|
|
250
|
+
parts.push(columns);
|
|
251
|
+
}
|
|
203
252
|
parts.push(fn);
|
|
204
253
|
return parts.join('\n') + '\n';
|
|
205
254
|
}
|
|
@@ -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({});
|
|
@@ -16,7 +16,7 @@ import { ref, computed, shallowRef } from 'vue';
|
|
|
16
16
|
* @param options Configuration for the list connector
|
|
17
17
|
*/
|
|
18
18
|
export function useListConnector(composableFn, options = {}) {
|
|
19
|
-
const { paginated = false, columns = [] } = options;
|
|
19
|
+
const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
|
|
20
20
|
// ββ Execute the underlying composable ββββββββββββββββββββββββββββββββββββββ
|
|
21
21
|
const composable = composableFn({ paginated });
|
|
22
22
|
// ββ Derived state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -36,16 +36,16 @@ export function useListConnector(composableFn, options = {}) {
|
|
|
36
36
|
// Pagination β passthrough from the underlying composable when paginated: true
|
|
37
37
|
const pagination = computed(() => composable.pagination?.value ?? null);
|
|
38
38
|
function goToPage(page) {
|
|
39
|
-
composable.goToPage?.(page);
|
|
39
|
+
composable.pagination?.value?.goToPage?.(page);
|
|
40
40
|
}
|
|
41
41
|
function nextPage() {
|
|
42
|
-
composable.nextPage?.();
|
|
42
|
+
composable.pagination?.value?.nextPage?.();
|
|
43
43
|
}
|
|
44
44
|
function prevPage() {
|
|
45
|
-
composable.prevPage?.();
|
|
45
|
+
composable.pagination?.value?.prevPage?.();
|
|
46
46
|
}
|
|
47
47
|
function setPerPage(n) {
|
|
48
|
-
composable.setPerPage?.(n);
|
|
48
|
+
composable.pagination?.value?.setPerPage?.(n);
|
|
49
49
|
}
|
|
50
50
|
// ββ Row selection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
51
51
|
const selected = ref([]);
|
|
@@ -95,10 +95,17 @@ export function useListConnector(composableFn, options = {}) {
|
|
|
95
95
|
function remove(row) {
|
|
96
96
|
_deleteTarget.value = row;
|
|
97
97
|
}
|
|
98
|
+
// Apply label overrides: columnLabel function takes priority over columnLabels map
|
|
99
|
+
const resolvedColumns = computed(() => columns.map((col) => ({
|
|
100
|
+
...col,
|
|
101
|
+
label: columnLabel
|
|
102
|
+
? columnLabel(col.key)
|
|
103
|
+
: (columnLabels[col.key] ?? col.label),
|
|
104
|
+
})));
|
|
98
105
|
return {
|
|
99
106
|
// State
|
|
100
107
|
rows,
|
|
101
|
-
columns:
|
|
108
|
+
columns: resolvedColumns,
|
|
102
109
|
loading,
|
|
103
110
|
error,
|
|
104
111
|
// Pagination
|
|
@@ -66,6 +66,10 @@ export async function generateUseAsyncDataComposables(inputDir, outputDir, optio
|
|
|
66
66
|
const sharedHelpersSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/apiHelpers.ts');
|
|
67
67
|
const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
|
|
68
68
|
await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
|
|
69
|
+
// Copy shared pagination.ts
|
|
70
|
+
const sharedPaginationSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/pagination.ts');
|
|
71
|
+
const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
|
|
72
|
+
await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
|
|
69
73
|
mainSpinner.stop('Runtime files copied');
|
|
70
74
|
// 5. Calculate relative import path from composables to APIs
|
|
71
75
|
const relativePath = calculateRelativeImportPath(composablesDir, inputDir);
|
|
@@ -232,10 +232,10 @@ export function useApiAsyncData(key, url, options) {
|
|
|
232
232
|
...paginationState.value,
|
|
233
233
|
hasNextPage: hasNextPage.value,
|
|
234
234
|
hasPrevPage: hasPrevPage.value,
|
|
235
|
+
goToPage,
|
|
236
|
+
nextPage,
|
|
237
|
+
prevPage,
|
|
238
|
+
setPerPage,
|
|
235
239
|
})),
|
|
236
|
-
goToPage,
|
|
237
|
-
nextPage,
|
|
238
|
-
prevPage,
|
|
239
|
-
setPerPage,
|
|
240
240
|
};
|
|
241
241
|
}
|
|
@@ -213,10 +213,10 @@ export function useApiAsyncDataRaw(key, url, options) {
|
|
|
213
213
|
...paginationState.value,
|
|
214
214
|
hasNextPage: hasNextPage.value,
|
|
215
215
|
hasPrevPage: hasPrevPage.value,
|
|
216
|
+
goToPage,
|
|
217
|
+
nextPage,
|
|
218
|
+
prevPage,
|
|
219
|
+
setPerPage,
|
|
216
220
|
})),
|
|
217
|
-
goToPage,
|
|
218
|
-
nextPage,
|
|
219
|
-
prevPage,
|
|
220
|
-
setPerPage,
|
|
221
221
|
};
|
|
222
222
|
}
|
|
@@ -62,6 +62,10 @@ export async function generateUseFetchComposables(inputDir, outputDir, options,
|
|
|
62
62
|
const sharedHelpersSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/apiHelpers.ts');
|
|
63
63
|
const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
|
|
64
64
|
await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
|
|
65
|
+
// Copy shared pagination.ts
|
|
66
|
+
const sharedPaginationSource = path.resolve(__dirname, '../../../src/generators/shared/runtime/pagination.ts');
|
|
67
|
+
const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
|
|
68
|
+
await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
|
|
65
69
|
mainSpinner.stop('Runtime files copied');
|
|
66
70
|
// 5. Calculate relative import path from composables to APIs
|
|
67
71
|
const relativePath = calculateRelativeImportPath(composablesDir, inputDir);
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,8 @@ program
|
|
|
55
55
|
backend: options.backend === 'official' || options.backend === 'heyapi'
|
|
56
56
|
? options.backend
|
|
57
57
|
: undefined,
|
|
58
|
-
|
|
58
|
+
// Only propagate if explicitly passed β undefined means "ask the user"
|
|
59
|
+
createUseAsyncDataConnectors: options.connectors === true ? true : undefined,
|
|
59
60
|
});
|
|
60
61
|
if (config.verbose) {
|
|
61
62
|
console.log('Configuration:', config);
|
package/package.json
CHANGED
|
@@ -122,20 +122,64 @@ function buildZodSchemas(resource: ResourceInfo): string {
|
|
|
122
122
|
return lines.join('\n');
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Build a const array with the column definitions inferred from the resource.
|
|
127
|
+
* Returns an empty string if the resource has no columns.
|
|
128
|
+
*/
|
|
129
|
+
function buildColumns(resource: ResourceInfo): string {
|
|
130
|
+
if (!resource.columns || resource.columns.length === 0) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
const camel = resource.composableName
|
|
134
|
+
.replace(/^use/, '')
|
|
135
|
+
.replace(/Connector$/, '')
|
|
136
|
+
.replace(/^./, (c) => c.toLowerCase());
|
|
137
|
+
const varName = `${camel}Columns`;
|
|
138
|
+
const entries = resource.columns
|
|
139
|
+
.map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
|
|
140
|
+
.join(',\n');
|
|
141
|
+
return `const ${varName} = [\n${entries},\n];`;
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
/**
|
|
126
145
|
* Build the body of the exported connector function.
|
|
127
146
|
*/
|
|
128
147
|
function buildFunctionBody(resource: ResourceInfo): string {
|
|
129
148
|
const pascal = pascalCase(resource.name);
|
|
149
|
+
const hasColumns = resource.columns && resource.columns.length > 0;
|
|
150
|
+
const camel = resource.composableName
|
|
151
|
+
.replace(/^use/, '')
|
|
152
|
+
.replace(/Connector$/, '')
|
|
153
|
+
.replace(/^./, (c) => c.toLowerCase());
|
|
154
|
+
const columnsVar = `${camel}Columns`;
|
|
130
155
|
const subConnectors: string[] = [];
|
|
131
156
|
|
|
157
|
+
// Destructure options param β only what's relevant for this resource
|
|
158
|
+
const optionKeys: string[] = [];
|
|
159
|
+
if (resource.listEndpoint && hasColumns) {
|
|
160
|
+
optionKeys.push('columnLabels', 'columnLabel');
|
|
161
|
+
}
|
|
162
|
+
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
163
|
+
optionKeys.push('createSchema');
|
|
164
|
+
}
|
|
165
|
+
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
166
|
+
optionKeys.push('updateSchema');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const optionsDestructure =
|
|
170
|
+
optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
|
|
171
|
+
|
|
132
172
|
if (resource.listEndpoint) {
|
|
133
173
|
const fn = toAsyncDataName(resource.listEndpoint.operationId);
|
|
134
174
|
// paginated: true tells useListConnector to expose pagination helpers
|
|
135
175
|
// (goToPage, nextPage, prevPage, setPerPage, pagination ref).
|
|
136
176
|
// We set it whenever the spec declares a list endpoint that has a response schema,
|
|
137
177
|
// which is a reliable proxy for "this API returns structured data worth paginating".
|
|
138
|
-
const
|
|
178
|
+
const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
|
|
179
|
+
const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
|
|
180
|
+
const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
|
|
181
|
+
const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
|
|
182
|
+
const opts = allArgs ? `{ ${allArgs} }` : '{}';
|
|
139
183
|
subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
|
|
140
184
|
}
|
|
141
185
|
|
|
@@ -146,7 +190,9 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
146
190
|
|
|
147
191
|
if (resource.createEndpoint) {
|
|
148
192
|
const fn = toAsyncDataName(resource.createEndpoint.operationId);
|
|
149
|
-
const schemaArg = resource.zodSchemas.create
|
|
193
|
+
const schemaArg = resource.zodSchemas.create
|
|
194
|
+
? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
|
|
195
|
+
: '{}';
|
|
150
196
|
subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
|
|
151
197
|
}
|
|
152
198
|
|
|
@@ -163,10 +209,10 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
163
209
|
let schemaArg = '{}';
|
|
164
210
|
if (resource.zodSchemas.update && hasDetail) {
|
|
165
211
|
// Best case: validate AND pre-fill from detail
|
|
166
|
-
schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
|
|
212
|
+
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
|
|
167
213
|
} else if (resource.zodSchemas.update) {
|
|
168
214
|
// Validate, but no detail endpoint to pre-fill from
|
|
169
|
-
schemaArg = `{ schema: ${pascal}UpdateSchema }`;
|
|
215
|
+
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
|
|
170
216
|
} else if (hasDetail) {
|
|
171
217
|
// No Zod schema (no request body in spec), but still pre-fill from detail
|
|
172
218
|
schemaArg = `{ loadWith: detail }`;
|
|
@@ -200,11 +246,14 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
200
246
|
const returnStatement = ` return { ${returnKeys.join(', ')} };`;
|
|
201
247
|
|
|
202
248
|
return [
|
|
203
|
-
`export function ${resource.composableName}() {`,
|
|
249
|
+
`export function ${resource.composableName}(options = {}) {`,
|
|
250
|
+
optionsDestructure.trimEnd(),
|
|
204
251
|
...subConnectors,
|
|
205
252
|
returnStatement,
|
|
206
253
|
`}`,
|
|
207
|
-
]
|
|
254
|
+
]
|
|
255
|
+
.filter((s) => s !== '')
|
|
256
|
+
.join('\n');
|
|
208
257
|
}
|
|
209
258
|
|
|
210
259
|
// βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -220,15 +269,19 @@ export function generateConnectorFile(resource: ResourceInfo, composablesRelDir:
|
|
|
220
269
|
const header = generateFileHeader();
|
|
221
270
|
const imports = buildImports(resource, composablesRelDir);
|
|
222
271
|
const schemas = buildZodSchemas(resource);
|
|
272
|
+
const columns = buildColumns(resource);
|
|
223
273
|
const fn = buildFunctionBody(resource);
|
|
224
274
|
|
|
225
|
-
// Assemble file: header + imports + (optional) Zod blocks + function body.
|
|
275
|
+
// Assemble file: header + imports + (optional) Zod blocks + columns const + function body.
|
|
226
276
|
// Each section ends with its own trailing newline; join with \n adds one blank
|
|
227
277
|
// line between sections, which matches Prettier's output for this structure.
|
|
228
278
|
const parts: string[] = [header, imports];
|
|
229
279
|
if (schemas.trim()) {
|
|
230
280
|
parts.push(schemas);
|
|
231
281
|
}
|
|
282
|
+
if (columns.trim()) {
|
|
283
|
+
parts.push(columns);
|
|
284
|
+
}
|
|
232
285
|
parts.push(fn);
|
|
233
286
|
|
|
234
287
|
return parts.join('\n') + '\n';
|
|
@@ -19,7 +19,15 @@ import { mergeZodErrors } from './zod-error-merger.js';
|
|
|
19
19
|
* @param options { schema, fields, loadWith?, errorConfig? }
|
|
20
20
|
*/
|
|
21
21
|
export function useFormConnector(composableFn, options = {}) {
|
|
22
|
-
const { schema, fields = [], loadWith = null, errorConfig = {} } = options;
|
|
22
|
+
const { schema: baseSchema, schemaOverride, fields = [], loadWith = null, errorConfig = {} } = options;
|
|
23
|
+
|
|
24
|
+
// Resolve the active schema:
|
|
25
|
+
// schemaOverride(base) β extend or refine the generated schema
|
|
26
|
+
// schemaOverride β replace the generated schema entirely
|
|
27
|
+
// baseSchema β the generated schema unchanged (default)
|
|
28
|
+
const schema = schemaOverride
|
|
29
|
+
? (typeof schemaOverride === 'function' ? schemaOverride(baseSchema) : schemaOverride)
|
|
30
|
+
: baseSchema;
|
|
23
31
|
|
|
24
32
|
// ββ Form state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
25
33
|
|
|
@@ -17,7 +17,7 @@ import { ref, computed, shallowRef } from 'vue';
|
|
|
17
17
|
* @param options Configuration for the list connector
|
|
18
18
|
*/
|
|
19
19
|
export function useListConnector(composableFn, options = {}) {
|
|
20
|
-
const { paginated = false, columns = [] } = options;
|
|
20
|
+
const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
|
|
21
21
|
|
|
22
22
|
// ββ Execute the underlying composable ββββββββββββββββββββββββββββββββββββββ
|
|
23
23
|
const composable = composableFn({ paginated });
|
|
@@ -40,19 +40,19 @@ export function useListConnector(composableFn, options = {}) {
|
|
|
40
40
|
const pagination = computed(() => composable.pagination?.value ?? null);
|
|
41
41
|
|
|
42
42
|
function goToPage(page) {
|
|
43
|
-
composable.goToPage?.(page);
|
|
43
|
+
composable.pagination?.value?.goToPage?.(page);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function nextPage() {
|
|
47
|
-
composable.nextPage?.();
|
|
47
|
+
composable.pagination?.value?.nextPage?.();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function prevPage() {
|
|
51
|
-
composable.prevPage?.();
|
|
51
|
+
composable.pagination?.value?.prevPage?.();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function setPerPage(n) {
|
|
55
|
-
composable.setPerPage?.(n);
|
|
55
|
+
composable.pagination?.value?.setPerPage?.(n);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// ββ Row selection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -113,10 +113,20 @@ export function useListConnector(composableFn, options = {}) {
|
|
|
113
113
|
_deleteTarget.value = row;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Apply label overrides: columnLabel function takes priority over columnLabels map
|
|
117
|
+
const resolvedColumns = computed(() =>
|
|
118
|
+
columns.map((col) => ({
|
|
119
|
+
...col,
|
|
120
|
+
label: columnLabel
|
|
121
|
+
? columnLabel(col.key)
|
|
122
|
+
: (columnLabels[col.key] ?? col.label),
|
|
123
|
+
}))
|
|
124
|
+
);
|
|
125
|
+
|
|
116
126
|
return {
|
|
117
127
|
// State
|
|
118
128
|
rows,
|
|
119
|
-
columns:
|
|
129
|
+
columns: resolvedColumns,
|
|
120
130
|
loading,
|
|
121
131
|
error,
|
|
122
132
|
|
|
@@ -104,6 +104,14 @@ export async function generateUseAsyncDataComposables(
|
|
|
104
104
|
);
|
|
105
105
|
const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
|
|
106
106
|
await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
|
|
107
|
+
|
|
108
|
+
// Copy shared pagination.ts
|
|
109
|
+
const sharedPaginationSource = path.resolve(
|
|
110
|
+
__dirname,
|
|
111
|
+
'../../../src/generators/shared/runtime/pagination.ts'
|
|
112
|
+
);
|
|
113
|
+
const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
|
|
114
|
+
await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
|
|
107
115
|
mainSpinner.stop('Runtime files copied');
|
|
108
116
|
|
|
109
117
|
// 5. Calculate relative import path from composables to APIs
|
|
@@ -320,10 +320,10 @@ export function useApiAsyncData<T>(
|
|
|
320
320
|
...paginationState.value,
|
|
321
321
|
hasNextPage: hasNextPage.value,
|
|
322
322
|
hasPrevPage: hasPrevPage.value,
|
|
323
|
+
goToPage,
|
|
324
|
+
nextPage,
|
|
325
|
+
prevPage,
|
|
326
|
+
setPerPage,
|
|
323
327
|
})),
|
|
324
|
-
goToPage,
|
|
325
|
-
nextPage,
|
|
326
|
-
prevPage,
|
|
327
|
-
setPerPage,
|
|
328
328
|
};
|
|
329
329
|
}
|
|
@@ -315,10 +315,10 @@ export function useApiAsyncDataRaw<T>(
|
|
|
315
315
|
...paginationState.value,
|
|
316
316
|
hasNextPage: hasNextPage.value,
|
|
317
317
|
hasPrevPage: hasPrevPage.value,
|
|
318
|
+
goToPage,
|
|
319
|
+
nextPage,
|
|
320
|
+
prevPage,
|
|
321
|
+
setPerPage,
|
|
318
322
|
})),
|
|
319
|
-
goToPage,
|
|
320
|
-
nextPage,
|
|
321
|
-
prevPage,
|
|
322
|
-
setPerPage,
|
|
323
323
|
};
|
|
324
324
|
}
|
|
@@ -91,6 +91,14 @@ export async function generateUseFetchComposables(
|
|
|
91
91
|
);
|
|
92
92
|
const sharedHelpersDest = path.join(sharedRuntimeDir, 'apiHelpers.ts');
|
|
93
93
|
await fs.copyFile(sharedHelpersSource, sharedHelpersDest);
|
|
94
|
+
|
|
95
|
+
// Copy shared pagination.ts
|
|
96
|
+
const sharedPaginationSource = path.resolve(
|
|
97
|
+
__dirname,
|
|
98
|
+
'../../../src/generators/shared/runtime/pagination.ts'
|
|
99
|
+
);
|
|
100
|
+
const sharedPaginationDest = path.join(sharedRuntimeDir, 'pagination.ts');
|
|
101
|
+
await fs.copyFile(sharedPaginationSource, sharedPaginationDest);
|
|
94
102
|
mainSpinner.stop('Runtime files copied');
|
|
95
103
|
|
|
96
104
|
// 5. Calculate relative import path from composables to APIs
|
package/src/index.ts
CHANGED
|
@@ -87,7 +87,8 @@ program
|
|
|
87
87
|
options.backend === 'official' || options.backend === 'heyapi'
|
|
88
88
|
? options.backend
|
|
89
89
|
: undefined,
|
|
90
|
-
|
|
90
|
+
// Only propagate if explicitly passed β undefined means "ask the user"
|
|
91
|
+
createUseAsyncDataConnectors: options.connectors === true ? true : undefined,
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
if (config.verbose) {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Placeholder for TanStack Query composables generator
|
|
3
|
-
* TODO: Implement TanStack Query generation
|
|
4
|
-
*/
|
|
5
|
-
export function generateTanstackQueryComposables(inputDir, outputDir) {
|
|
6
|
-
console.log('\nπ¦ @tanstack/vue-query generator\n');
|
|
7
|
-
console.log(` Input: ${inputDir}`);
|
|
8
|
-
console.log(` Output: ${outputDir}`);
|
|
9
|
-
console.log('\nβ οΈ This generator is not yet implemented.');
|
|
10
|
-
console.log(' Coming soon!\n');
|
|
11
|
-
}
|