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
|
@@ -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);
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { UseFetchOptions } from '#app';
|
|
2
2
|
import { type ApiRequestOptions as BaseApiRequestOptions } from '../../shared/runtime/apiHelpers.js';
|
|
3
|
+
type PickInput = ReadonlyArray<string> | undefined;
|
|
4
|
+
type HasNestedPath<K extends ReadonlyArray<string>> = Extract<K[number], `${string}.${string}`> extends never ? false : true;
|
|
5
|
+
type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string> ? HasNestedPath<K> extends true ? any : Pick<T, Extract<K[number], keyof T>> : T;
|
|
3
6
|
/**
|
|
4
7
|
* Options for useAsyncData API requests with lifecycle callbacks.
|
|
5
8
|
* Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
|
|
6
9
|
* Native options like baseURL, method, body, headers, query, lazy, server, immediate, dedupe, etc. are all available.
|
|
7
10
|
* watch: boolean (true = auto-watch reactive params, false = disable auto-refresh)
|
|
8
11
|
*/
|
|
9
|
-
export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> & Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'watch'> & {
|
|
12
|
+
export type ApiAsyncDataOptions<T, DataT = T, PickT extends PickInput = undefined> = Omit<BaseApiRequestOptions<T>, 'transform' | 'pick'> & Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick' | 'watch'> & {
|
|
13
|
+
pick?: PickT;
|
|
14
|
+
transform?: (data: PickedData<T, PickT>) => DataT;
|
|
10
15
|
/**
|
|
11
16
|
* Enable automatic refresh when reactive params/url change (default: true).
|
|
12
17
|
* Set to false to disable auto-refresh entirely.
|
|
@@ -22,4 +27,5 @@ export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> & Omit<UseFetchOpt
|
|
|
22
27
|
* - Global headers from useApiHeaders or $getApiHeaders
|
|
23
28
|
* - Watch pattern for reactive parameters
|
|
24
29
|
*/
|
|
25
|
-
export declare function useApiAsyncData<T>(key: string, url: string | (() => string), options?:
|
|
30
|
+
export declare function useApiAsyncData<T, Options extends ApiAsyncDataOptions<T, any, any> = ApiAsyncDataOptions<T>>(key: string, url: string | (() => string), options?: Options): any;
|
|
31
|
+
export {};
|
|
@@ -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
|
}
|
|
@@ -10,16 +10,21 @@ export interface RawResponse<T> {
|
|
|
10
10
|
status: number;
|
|
11
11
|
statusText: string;
|
|
12
12
|
}
|
|
13
|
+
type PickInput = ReadonlyArray<string> | undefined;
|
|
14
|
+
type HasNestedPath<K extends ReadonlyArray<string>> = Extract<K[number], `${string}.${string}`> extends never ? false : true;
|
|
15
|
+
type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string> ? HasNestedPath<K> extends true ? any : Pick<T, Extract<K[number], keyof T>> : T;
|
|
13
16
|
/**
|
|
14
17
|
* Options for useAsyncData Raw API requests.
|
|
15
18
|
* Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
|
|
16
19
|
* onSuccess receives data AND the full response (headers, status, statusText).
|
|
17
20
|
*/
|
|
18
|
-
export type ApiAsyncDataRawOptions<T> = Omit<BaseApiRequestOptions<T>, 'onSuccess'> & Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'onSuccess'> & {
|
|
21
|
+
export type ApiAsyncDataRawOptions<T, DataT = T, PickT extends PickInput = undefined> = Omit<BaseApiRequestOptions<T>, 'onSuccess' | 'transform' | 'pick'> & Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick' | 'onSuccess'> & {
|
|
22
|
+
pick?: PickT;
|
|
23
|
+
transform?: (data: PickedData<T, PickT>) => DataT;
|
|
19
24
|
/**
|
|
20
25
|
* Called when the request succeeds — receives both data and the full response object.
|
|
21
26
|
*/
|
|
22
|
-
onSuccess?: (data:
|
|
27
|
+
onSuccess?: (data: DataT, response: {
|
|
23
28
|
headers: Headers;
|
|
24
29
|
status: number;
|
|
25
30
|
statusText: string;
|
|
@@ -38,4 +43,5 @@ export type ApiAsyncDataRawOptions<T> = Omit<BaseApiRequestOptions<T>, 'onSucces
|
|
|
38
43
|
* - Global headers from useApiHeaders or $getApiHeaders
|
|
39
44
|
* - Watch pattern for reactive parameters
|
|
40
45
|
*/
|
|
41
|
-
export declare function useApiAsyncDataRaw<T>(key: string, url: string | (() => string), options?:
|
|
46
|
+
export declare function useApiAsyncDataRaw<T, DataT = T, PickT extends PickInput = undefined, Options extends ApiAsyncDataRawOptions<T, DataT, PickT> = ApiAsyncDataRawOptions<T, DataT, PickT>>(key: string, url: string | (() => string), options?: Options): any;
|
|
47
|
+
export {};
|
|
@@ -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
|
}
|
|
@@ -98,14 +98,16 @@ function generateImports(method, apiImportPath, isRaw) {
|
|
|
98
98
|
function generateFunctionBody(method, isRaw, generateOptions) {
|
|
99
99
|
const hasParams = !!method.requestType;
|
|
100
100
|
const paramsArg = hasParams ? `params: ${method.requestType}` : '';
|
|
101
|
+
const responseType = method.responseType !== 'void' ? method.responseType : 'void';
|
|
101
102
|
// Determine the options type based on isRaw
|
|
102
103
|
const optionsType = isRaw
|
|
103
|
-
? `ApiAsyncDataRawOptions<${
|
|
104
|
-
: `ApiAsyncDataOptions<${
|
|
105
|
-
const
|
|
104
|
+
? `ApiAsyncDataRawOptions<${responseType}, DataT, PickT>`
|
|
105
|
+
: `ApiAsyncDataOptions<${responseType}, DataT, PickT>`;
|
|
106
|
+
const optionsDefaultType = isRaw
|
|
107
|
+
? `ApiAsyncDataRawOptions<${responseType}, DataT, PickT>`
|
|
108
|
+
: `ApiAsyncDataOptions<${responseType}, DataT, PickT>`;
|
|
109
|
+
const optionsArg = `options?: Options`;
|
|
106
110
|
const args = hasParams ? `${paramsArg}, ${optionsArg}` : optionsArg;
|
|
107
|
-
// Determine the response type generic
|
|
108
|
-
const responseTypeGeneric = method.responseType !== 'void' ? `<${method.responseType}>` : '';
|
|
109
111
|
// Generate unique key for useAsyncData
|
|
110
112
|
const composableName = isRaw && method.rawMethodName
|
|
111
113
|
? `useAsyncData${method.rawMethodName.replace(/Raw$/, '')}Raw`
|
|
@@ -116,15 +118,29 @@ function generateFunctionBody(method, isRaw, generateOptions) {
|
|
|
116
118
|
const description = method.description ? `/**\n * ${method.description}\n */\n` : '';
|
|
117
119
|
// Choose the correct wrapper function
|
|
118
120
|
const wrapperFunction = isRaw ? 'useApiAsyncDataRaw' : 'useApiAsyncData';
|
|
121
|
+
const wrapperCall = isRaw
|
|
122
|
+
? `${wrapperFunction}<${responseType}, DataT, PickT, Options>`
|
|
123
|
+
: `${wrapperFunction}<${responseType}, Options>`;
|
|
124
|
+
const returnType = isRaw
|
|
125
|
+
? `ReturnType<typeof ${wrapperFunction}<${responseType}, DataT, PickT, Options>>`
|
|
126
|
+
: `ReturnType<typeof ${wrapperFunction}<${responseType}, Options>>`;
|
|
119
127
|
const pInit = hasParams ? `\n const p = shallowRef(params)` : '';
|
|
120
128
|
const argsExtraction = hasParams
|
|
121
129
|
? ` const _hasKey = typeof args[0] === 'string'\n const params = _hasKey ? args[1] : args[0]\n const options = _hasKey ? { cacheKey: args[0], ...args[2] } : args[1]`
|
|
122
130
|
: ` const _hasKey = typeof args[0] === 'string'\n const options = _hasKey ? { cacheKey: args[0], ...args[1] } : args[0]`;
|
|
123
|
-
return `${description}export function ${composableName}
|
|
124
|
-
|
|
131
|
+
return `${description}export function ${composableName}<
|
|
132
|
+
DataT = ${responseType},
|
|
133
|
+
PickT extends ReadonlyArray<string> | undefined = undefined,
|
|
134
|
+
Options extends ${optionsType} = ${optionsDefaultType}
|
|
135
|
+
>(key: string, ${args}): ${returnType}
|
|
136
|
+
export function ${composableName}<
|
|
137
|
+
DataT = ${responseType},
|
|
138
|
+
PickT extends ReadonlyArray<string> | undefined = undefined,
|
|
139
|
+
Options extends ${optionsType} = ${optionsDefaultType}
|
|
140
|
+
>(${args}): ${returnType}
|
|
125
141
|
export function ${composableName}(...args: any[]) {
|
|
126
142
|
${argsExtraction}${pInit}
|
|
127
|
-
return ${
|
|
143
|
+
return ${wrapperCall}(${key}, ${url}, ${fetchOptions})
|
|
128
144
|
}`;
|
|
129
145
|
}
|
|
130
146
|
/**
|
|
@@ -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);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { UseFetchOptions } from '#app';
|
|
2
2
|
import { type ApiRequestOptions as BaseApiRequestOptions } from '../../shared/runtime/apiHelpers.js';
|
|
3
|
+
type PickInput = ReadonlyArray<string> | undefined;
|
|
4
|
+
type HasNestedPath<K extends ReadonlyArray<string>> = Extract<K[number], `${string}.${string}`> extends never ? false : true;
|
|
5
|
+
type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string> ? HasNestedPath<K> extends true ? any : Pick<T, Extract<K[number], keyof T>> : T;
|
|
3
6
|
/**
|
|
4
7
|
* Options for useFetch API requests with lifecycle callbacks.
|
|
5
8
|
* Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
|
|
6
9
|
* Native options like baseURL, method, body, headers, query, lazy, server, immediate, etc. are all available.
|
|
7
10
|
*/
|
|
8
|
-
export type ApiRequestOptions<T = any> = BaseApiRequestOptions<T> & Omit<UseFetchOptions<T>, 'transform' | 'pick'
|
|
11
|
+
export type ApiRequestOptions<T = any, DataT = T, PickT extends PickInput = undefined> = Omit<BaseApiRequestOptions<T>, 'transform' | 'pick'> & Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick'> & {
|
|
12
|
+
pick?: PickT;
|
|
13
|
+
transform?: (data: PickedData<T, PickT>) => DataT;
|
|
14
|
+
};
|
|
9
15
|
/**
|
|
10
16
|
* Enhanced useFetch wrapper with lifecycle callbacks and request interception
|
|
11
17
|
*
|
|
@@ -42,4 +48,5 @@ export type ApiRequestOptions<T = any> = BaseApiRequestOptions<T> & Omit<UseFetc
|
|
|
42
48
|
* });
|
|
43
49
|
* ```
|
|
44
50
|
*/
|
|
45
|
-
export declare function useApiRequest<T = any, Options extends ApiRequestOptions<T> = ApiRequestOptions<T>>(url: string | (() => string), options?: Options): any;
|
|
51
|
+
export declare function useApiRequest<T = any, Options extends ApiRequestOptions<T, any, any> = ApiRequestOptions<T>>(url: string | (() => string), options?: Options): any;
|
|
52
|
+
export {};
|
|
@@ -87,16 +87,20 @@ function generateImports(method, apiImportPath) {
|
|
|
87
87
|
function generateFunctionBody(method, options) {
|
|
88
88
|
const hasParams = !!method.requestType;
|
|
89
89
|
const paramsArg = hasParams ? `params: ${method.requestType}` : '';
|
|
90
|
-
const
|
|
91
|
-
const
|
|
90
|
+
const responseType = method.responseType !== 'void' ? method.responseType : 'void';
|
|
91
|
+
const optionsType = `ApiRequestOptions<${responseType}, DataT, PickT>`;
|
|
92
|
+
const optionsArg = `options?: Options`;
|
|
92
93
|
const args = hasParams ? `${paramsArg}, ${optionsArg}` : optionsArg;
|
|
93
|
-
const responseTypeGeneric = method.responseType !== 'void' ? `<${method.responseType}>` : '';
|
|
94
94
|
const url = generateUrl(method);
|
|
95
95
|
const fetchOptions = generateFetchOptions(method, options);
|
|
96
96
|
const description = method.description ? `/**\n * ${method.description}\n */\n` : '';
|
|
97
97
|
const pInit = hasParams ? `\n const p = shallowRef(params)` : '';
|
|
98
|
-
return `${description}export const ${method.composableName} =
|
|
99
|
-
|
|
98
|
+
return `${description}export const ${method.composableName} = <
|
|
99
|
+
DataT = ${responseType},
|
|
100
|
+
PickT extends ReadonlyArray<string> | undefined = undefined,
|
|
101
|
+
Options extends ${optionsType} = ApiRequestOptions<${responseType}, DataT, PickT>
|
|
102
|
+
>(${args}) => {${pInit}
|
|
103
|
+
return useApiRequest<${responseType}, Options>(${url}, ${fetchOptions})
|
|
100
104
|
}`;
|
|
101
105
|
}
|
|
102
106
|
/**
|
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
|
@@ -13,6 +13,7 @@ import { type Logger, createClackLogger } from '../../../cli/logger.js';
|
|
|
13
13
|
|
|
14
14
|
// Runtime files that must be copied to the user's project
|
|
15
15
|
const RUNTIME_FILES = [
|
|
16
|
+
'connector-types.ts',
|
|
16
17
|
'useListConnector.ts',
|
|
17
18
|
'useDetailConnector.ts',
|
|
18
19
|
'useFormConnector.ts',
|
|
@@ -15,7 +15,6 @@ function generateFileHeader(): string {
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/* eslint-disable */
|
|
18
|
-
// @ts-nocheck
|
|
19
18
|
`;
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -42,18 +41,37 @@ function toFileName(composableName: string): string {
|
|
|
42
41
|
/**
|
|
43
42
|
* Build all `import` lines for a resource connector.
|
|
44
43
|
*/
|
|
45
|
-
function buildImports(resource: ResourceInfo, composablesRelDir: string): string {
|
|
44
|
+
function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelDir: string): string {
|
|
46
45
|
const lines: string[] = [];
|
|
47
46
|
|
|
48
47
|
// zod
|
|
49
48
|
lines.push(`import { z } from 'zod';`);
|
|
50
49
|
lines.push('');
|
|
51
50
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
51
|
+
// connector-types — structural interfaces for return types
|
|
52
|
+
const connectorTypeImports: string[] = ['ListConnectorReturn'];
|
|
53
|
+
if (resource.detailEndpoint) {
|
|
54
|
+
connectorTypeImports.push('DetailConnectorReturn');
|
|
55
|
+
}
|
|
56
|
+
if (resource.createEndpoint || resource.updateEndpoint) {
|
|
57
|
+
connectorTypeImports.push('FormConnectorReturn');
|
|
58
|
+
}
|
|
59
|
+
if (resource.deleteEndpoint) {
|
|
60
|
+
connectorTypeImports.push('DeleteConnectorReturn');
|
|
61
|
+
}
|
|
62
|
+
lines.push(`import type { ${connectorTypeImports.join(', ')} } from '#nxh/runtime/connector-types';`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
// SDK request/response types (for the params overload signature)
|
|
54
66
|
if (resource.listEndpoint) {
|
|
55
|
-
|
|
67
|
+
const requestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
|
|
68
|
+
lines.push(`import type { ${requestTypeName} } from '${sdkRelDir}';`);
|
|
69
|
+
lines.push('');
|
|
56
70
|
}
|
|
71
|
+
|
|
72
|
+
// runtime helpers (Nuxt alias — set up by the Nuxt module)
|
|
73
|
+
// useListConnector is always imported to support the optional factory pattern
|
|
74
|
+
const runtimeHelpers: string[] = ['useListConnector'];
|
|
57
75
|
if (resource.detailEndpoint) {
|
|
58
76
|
runtimeHelpers.push('useDetailConnector');
|
|
59
77
|
}
|
|
@@ -122,68 +140,184 @@ function buildZodSchemas(resource: ResourceInfo): string {
|
|
|
122
140
|
return lines.join('\n');
|
|
123
141
|
}
|
|
124
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Build a const array with the column definitions inferred from the resource.
|
|
145
|
+
* Returns an empty string if the resource has no columns.
|
|
146
|
+
*/
|
|
147
|
+
function buildColumns(resource: ResourceInfo): string {
|
|
148
|
+
if (!resource.columns || resource.columns.length === 0) {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
const camel = resource.composableName
|
|
152
|
+
.replace(/^use/, '')
|
|
153
|
+
.replace(/Connector$/, '')
|
|
154
|
+
.replace(/^./, (c) => c.toLowerCase());
|
|
155
|
+
const varName = `${camel}Columns`;
|
|
156
|
+
const entries = resource.columns
|
|
157
|
+
.map((col) => ` { key: '${col.key}', label: '${col.label}', type: '${col.type}' }`)
|
|
158
|
+
.join(',\n');
|
|
159
|
+
return `const ${varName} = [\n${entries},\n];`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the TypeScript options interface for a connector.
|
|
164
|
+
* Only includes fields relevant to the endpoints present on the resource.
|
|
165
|
+
*/
|
|
166
|
+
function buildOptionsInterface(resource: ResourceInfo): string {
|
|
167
|
+
const typeName = `${pascalCase(resource.composableName)}Options`;
|
|
168
|
+
const hasColumns = resource.columns && resource.columns.length > 0;
|
|
169
|
+
const fields: string[] = [];
|
|
170
|
+
|
|
171
|
+
if (resource.listEndpoint && hasColumns) {
|
|
172
|
+
fields.push(` columnLabels?: Record<string, string>;`);
|
|
173
|
+
fields.push(` columnLabel?: (key: string) => string;`);
|
|
174
|
+
}
|
|
175
|
+
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
176
|
+
fields.push(` createSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
|
|
177
|
+
}
|
|
178
|
+
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
179
|
+
fields.push(` updateSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (fields.length === 0) {
|
|
183
|
+
return `type ${typeName} = Record<string, never>;`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [`interface ${typeName} {`, ...fields, `}`].join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build the TypeScript return type for a connector.
|
|
191
|
+
*/
|
|
192
|
+
function buildReturnType(resource: ResourceInfo): string {
|
|
193
|
+
const pascal = pascalCase(resource.name);
|
|
194
|
+
const typeName = `${pascalCase(resource.composableName)}Return`;
|
|
195
|
+
const fields: string[] = [];
|
|
196
|
+
|
|
197
|
+
// table is always present in the return type:
|
|
198
|
+
// - if listEndpoint exists → ListConnectorReturn<T> (always defined)
|
|
199
|
+
// - if no listEndpoint → ListConnectorReturn<unknown> | undefined (only when factory passed)
|
|
200
|
+
if (resource.listEndpoint) {
|
|
201
|
+
fields.push(` table: ListConnectorReturn<${pascal}>;`);
|
|
202
|
+
} else {
|
|
203
|
+
fields.push(` table: ListConnectorReturn<unknown> | undefined;`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (resource.detailEndpoint) {
|
|
207
|
+
fields.push(` detail: DetailConnectorReturn<${pascal}>;`);
|
|
208
|
+
}
|
|
209
|
+
if (resource.createEndpoint) {
|
|
210
|
+
const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
|
|
211
|
+
fields.push(` createForm: FormConnectorReturn<${inputType}>;`);
|
|
212
|
+
}
|
|
213
|
+
if (resource.updateEndpoint) {
|
|
214
|
+
const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
|
|
215
|
+
fields.push(` updateForm: FormConnectorReturn<${inputType}>;`);
|
|
216
|
+
}
|
|
217
|
+
if (resource.deleteEndpoint) {
|
|
218
|
+
fields.push(` deleteAction: DeleteConnectorReturn<${pascal}>;`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return [`type ${typeName} = {`, ...fields, `};`].join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
125
224
|
/**
|
|
126
225
|
* Build the body of the exported connector function.
|
|
127
226
|
*/
|
|
128
227
|
function buildFunctionBody(resource: ResourceInfo): string {
|
|
129
228
|
const pascal = pascalCase(resource.name);
|
|
229
|
+
const hasColumns = resource.columns && resource.columns.length > 0;
|
|
230
|
+
const camel = resource.composableName
|
|
231
|
+
.replace(/^use/, '')
|
|
232
|
+
.replace(/Connector$/, '')
|
|
233
|
+
.replace(/^./, (c) => c.toLowerCase());
|
|
234
|
+
const columnsVar = `${camel}Columns`;
|
|
130
235
|
const subConnectors: string[] = [];
|
|
131
236
|
|
|
237
|
+
// Derived type names — must match buildOptionsInterface / buildReturnType
|
|
238
|
+
const optionsTypeName = `${pascalCase(resource.composableName)}Options`;
|
|
239
|
+
const returnTypeName = `${pascalCase(resource.composableName)}Return`;
|
|
240
|
+
|
|
241
|
+
// Destructure options param — only what's relevant for this resource
|
|
242
|
+
const optionKeys: string[] = [];
|
|
243
|
+
if (resource.listEndpoint && hasColumns) {
|
|
244
|
+
optionKeys.push('columnLabels', 'columnLabel');
|
|
245
|
+
}
|
|
246
|
+
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
247
|
+
optionKeys.push('createSchema');
|
|
248
|
+
}
|
|
249
|
+
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
250
|
+
optionKeys.push('updateSchema');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const optionsDestructure =
|
|
254
|
+
optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
|
|
255
|
+
|
|
256
|
+
// ── List / table sub-connector ─────────────────────────────────────────────
|
|
132
257
|
if (resource.listEndpoint) {
|
|
133
258
|
const fn = toAsyncDataName(resource.listEndpoint.operationId);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
259
|
+
const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
|
|
260
|
+
const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
|
|
261
|
+
const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
|
|
262
|
+
const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
|
|
263
|
+
const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
|
|
264
|
+
const opts = allArgs ? `{ ${allArgs} }` : '{}';
|
|
265
|
+
|
|
266
|
+
// Factory: if the first arg is a function the user provided their own composable;
|
|
267
|
+
// otherwise build a default factory from the plain params object.
|
|
268
|
+
subConnectors.push(
|
|
269
|
+
` const isFactory = typeof paramsOrSource === 'function';`,
|
|
270
|
+
` const listFactory = isFactory`,
|
|
271
|
+
` ? (paramsOrSource as () => unknown)`,
|
|
272
|
+
` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName});`,
|
|
273
|
+
` const table = useListConnector(listFactory, ${opts}) as unknown as ListConnectorReturn<${pascal}>;`
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
// No list endpoint — support optional factory for developer-provided list
|
|
277
|
+
subConnectors.push(
|
|
278
|
+
` const table = paramsOrSource`,
|
|
279
|
+
` ? (useListConnector(paramsOrSource as () => unknown, {}) as unknown as ListConnectorReturn<unknown>)`,
|
|
280
|
+
` : undefined;`
|
|
281
|
+
);
|
|
140
282
|
}
|
|
141
283
|
|
|
142
284
|
if (resource.detailEndpoint) {
|
|
143
285
|
const fn = toAsyncDataName(resource.detailEndpoint.operationId);
|
|
144
|
-
subConnectors.push(` const detail = useDetailConnector(${fn})
|
|
286
|
+
subConnectors.push(` const detail = useDetailConnector(${fn}) as unknown as DetailConnectorReturn<${pascal}>;`);
|
|
145
287
|
}
|
|
146
288
|
|
|
147
289
|
if (resource.createEndpoint) {
|
|
148
290
|
const fn = toAsyncDataName(resource.createEndpoint.operationId);
|
|
149
|
-
const
|
|
150
|
-
|
|
291
|
+
const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
|
|
292
|
+
const schemaArg = resource.zodSchemas.create
|
|
293
|
+
? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
|
|
294
|
+
: '{}';
|
|
295
|
+
subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
|
|
151
296
|
}
|
|
152
297
|
|
|
153
298
|
if (resource.updateEndpoint) {
|
|
154
299
|
const fn = toAsyncDataName(resource.updateEndpoint.operationId);
|
|
155
300
|
const hasDetail = !!resource.detailEndpoint;
|
|
301
|
+
const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
|
|
156
302
|
|
|
157
|
-
// Build the options argument for useFormConnector:
|
|
158
|
-
// schema → Zod schema for client-side validation before submission
|
|
159
|
-
// loadWith → reference to the detail connector so the form auto-fills
|
|
160
|
-
// when detail.item changes (user clicks "Edit" on a row)
|
|
161
|
-
//
|
|
162
|
-
// Four combinations are possible depending on what the spec provides:
|
|
163
303
|
let schemaArg = '{}';
|
|
164
304
|
if (resource.zodSchemas.update && hasDetail) {
|
|
165
|
-
|
|
166
|
-
schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
|
|
305
|
+
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
|
|
167
306
|
} else if (resource.zodSchemas.update) {
|
|
168
|
-
|
|
169
|
-
schemaArg = `{ schema: ${pascal}UpdateSchema }`;
|
|
307
|
+
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
|
|
170
308
|
} else if (hasDetail) {
|
|
171
|
-
// No Zod schema (no request body in spec), but still pre-fill from detail
|
|
172
309
|
schemaArg = `{ loadWith: detail }`;
|
|
173
310
|
}
|
|
174
|
-
subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg})
|
|
311
|
+
subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
|
|
175
312
|
}
|
|
176
313
|
|
|
177
314
|
if (resource.deleteEndpoint) {
|
|
178
315
|
const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
|
|
179
|
-
subConnectors.push(` const deleteAction = useDeleteConnector(${fn})
|
|
316
|
+
subConnectors.push(` const deleteAction = useDeleteConnector(${fn}) as unknown as DeleteConnectorReturn<${pascal}>;`);
|
|
180
317
|
}
|
|
181
318
|
|
|
182
|
-
// Return object —
|
|
183
|
-
const returnKeys: string[] = [];
|
|
184
|
-
if (resource.listEndpoint) {
|
|
185
|
-
returnKeys.push('table');
|
|
186
|
-
}
|
|
319
|
+
// Return object — always includes table (undefined when no list + no factory)
|
|
320
|
+
const returnKeys: string[] = ['table'];
|
|
187
321
|
if (resource.detailEndpoint) {
|
|
188
322
|
returnKeys.push('detail');
|
|
189
323
|
}
|
|
@@ -197,14 +331,36 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
197
331
|
returnKeys.push('deleteAction');
|
|
198
332
|
}
|
|
199
333
|
|
|
200
|
-
const returnStatement = ` return { ${returnKeys.join(', ')} };`;
|
|
334
|
+
const returnStatement = ` return { ${returnKeys.join(', ')} } as ${returnTypeName};`;
|
|
335
|
+
|
|
336
|
+
// ── Function signature ─────────────────────────────────────────────────────
|
|
337
|
+
// Resources WITH a list endpoint: two overloads (factory | params).
|
|
338
|
+
// Resources WITHOUT a list endpoint: single signature with optional factory.
|
|
339
|
+
const lines: string[] = [];
|
|
201
340
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
341
|
+
if (resource.listEndpoint) {
|
|
342
|
+
const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
|
|
343
|
+
lines.push(
|
|
344
|
+
`export function ${resource.composableName}(source: () => unknown, options?: ${optionsTypeName}): ${returnTypeName};`,
|
|
345
|
+
`export function ${resource.composableName}(params?: ${listRequestTypeName}, options?: ${optionsTypeName}): ${returnTypeName};`,
|
|
346
|
+
`export function ${resource.composableName}(paramsOrSource?: ${listRequestTypeName} | (() => unknown), options: ${optionsTypeName} = {}): ${returnTypeName} {`
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
lines.push(
|
|
350
|
+
`export function ${resource.composableName}(source?: () => unknown, options: ${optionsTypeName} = {}): ${returnTypeName} {`
|
|
351
|
+
);
|
|
352
|
+
// Alias so the body can use paramsOrSource uniformly
|
|
353
|
+
lines.push(` const paramsOrSource = source;`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (optionsDestructure.trim()) {
|
|
357
|
+
lines.push(optionsDestructure.trimEnd());
|
|
358
|
+
}
|
|
359
|
+
lines.push(...subConnectors);
|
|
360
|
+
lines.push(returnStatement);
|
|
361
|
+
lines.push(`}`);
|
|
362
|
+
|
|
363
|
+
return lines.join('\n');
|
|
208
364
|
}
|
|
209
365
|
|
|
210
366
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -216,19 +372,30 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
216
372
|
* @param composablesRelDir Relative path from the connector dir to the
|
|
217
373
|
* useAsyncData composables dir (e.g. '../use-async-data')
|
|
218
374
|
*/
|
|
219
|
-
export function generateConnectorFile(
|
|
375
|
+
export function generateConnectorFile(
|
|
376
|
+
resource: ResourceInfo,
|
|
377
|
+
composablesRelDir: string,
|
|
378
|
+
sdkRelDir = '../..'
|
|
379
|
+
): string {
|
|
220
380
|
const header = generateFileHeader();
|
|
221
|
-
const imports = buildImports(resource, composablesRelDir);
|
|
381
|
+
const imports = buildImports(resource, composablesRelDir, sdkRelDir);
|
|
222
382
|
const schemas = buildZodSchemas(resource);
|
|
383
|
+
const columns = buildColumns(resource);
|
|
384
|
+
const optionsInterface = buildOptionsInterface(resource);
|
|
385
|
+
const returnType = buildReturnType(resource);
|
|
223
386
|
const fn = buildFunctionBody(resource);
|
|
224
387
|
|
|
225
|
-
// Assemble file: header + imports + (optional) Zod blocks +
|
|
226
|
-
//
|
|
227
|
-
// line between sections, which matches Prettier's output for this structure.
|
|
388
|
+
// Assemble file: header + imports + (optional) Zod blocks + columns const +
|
|
389
|
+
// options interface + return type + function body.
|
|
228
390
|
const parts: string[] = [header, imports];
|
|
229
391
|
if (schemas.trim()) {
|
|
230
392
|
parts.push(schemas);
|
|
231
393
|
}
|
|
394
|
+
if (columns.trim()) {
|
|
395
|
+
parts.push(columns);
|
|
396
|
+
}
|
|
397
|
+
parts.push(optionsInterface);
|
|
398
|
+
parts.push(returnType);
|
|
232
399
|
parts.push(fn);
|
|
233
400
|
|
|
234
401
|
return parts.join('\n') + '\n';
|