nuxt-openapi-hyperfetch 0.2.8-alpha.1 → 0.3.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +84 -6
  2. package/dist/generators/components/connector-generator/generator.js +1 -0
  3. package/dist/generators/components/connector-generator/templates.d.ts +1 -1
  4. package/dist/generators/components/connector-generator/templates.js +175 -44
  5. package/dist/generators/shared/runtime/connector-types.d.ts +104 -0
  6. package/dist/generators/shared/runtime/connector-types.js +10 -0
  7. package/dist/generators/shared/runtime/useFormConnector.js +8 -1
  8. package/dist/generators/shared/runtime/useListConnector.d.ts +5 -3
  9. package/dist/generators/shared/runtime/useListConnector.js +19 -10
  10. package/dist/generators/use-async-data/generator.js +4 -0
  11. package/dist/generators/use-async-data/runtime/useApiAsyncData.d.ts +8 -2
  12. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
  13. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.d.ts +9 -3
  14. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
  15. package/dist/generators/use-async-data/templates.js +24 -8
  16. package/dist/generators/use-fetch/generator.js +4 -0
  17. package/dist/generators/use-fetch/runtime/useApiRequest.d.ts +9 -2
  18. package/dist/generators/use-fetch/templates.js +9 -5
  19. package/dist/index.js +2 -1
  20. package/package.json +1 -1
  21. package/src/generators/components/connector-generator/generator.ts +1 -0
  22. package/src/generators/components/connector-generator/templates.ts +211 -44
  23. package/src/generators/shared/runtime/connector-types.ts +142 -0
  24. package/src/generators/shared/runtime/useFormConnector.ts +9 -1
  25. package/src/generators/shared/runtime/useListConnector.ts +22 -10
  26. package/src/generators/use-async-data/generator.ts +8 -0
  27. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +37 -9
  28. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +34 -12
  29. package/src/generators/use-async-data/templates.ts +24 -9
  30. package/src/generators/use-fetch/generator.ts +8 -0
  31. package/src/generators/use-fetch/runtime/useApiRequest.ts +34 -4
  32. package/src/generators/use-fetch/templates.ts +9 -6
  33. package/src/index.ts +2 -1
  34. package/dist/generators/tanstack-query/generator.d.ts +0 -5
  35. package/dist/generators/tanstack-query/generator.js +0 -11
@@ -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?: ApiAsyncDataOptions<T>): any;
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: T, response: {
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?: ApiAsyncDataRawOptions<T>): any;
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<${method.responseType}>`
104
- : `ApiAsyncDataOptions<${method.responseType}>`;
105
- const optionsArg = `options?: ${optionsType}`;
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}(key: string, ${args})
124
- export function ${composableName}(${args})
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 ${wrapperFunction}${responseTypeGeneric}(${key}, ${url}, ${fetchOptions})
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 optionsType = `ApiRequestOptions<${method.responseType}>`;
91
- const optionsArg = `options?: ${optionsType}`;
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} = (${args}) => {${pInit}
99
- return useApiRequest${responseTypeGeneric}(${url}, ${fetchOptions})
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
- createUseAsyncDataConnectors: options.connectors,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-openapi-hyperfetch",
3
- "version": "0.2.8-alpha.1",
3
+ "version": "0.3.1-beta",
4
4
  "description": "Nuxt useFetch, useAsyncData and Nuxt server OpenAPI generator",
5
5
  "type": "module",
6
6
  "author": "",
@@ -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
- // runtime helpers (Nuxt alias set up by the Nuxt module)
53
- const runtimeHelpers: string[] = [];
51
+ // connector-typesstructural 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
- runtimeHelpers.push('useListConnector');
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
- // paginated: true tells useListConnector to expose pagination helpers
135
- // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
136
- // We set it whenever the spec declares a list endpoint that has a response schema,
137
- // which is a reliable proxy for "this API returns structured data worth paginating".
138
- const opts = resource.listEndpoint.responseSchema ? '{ paginated: true }' : '{}';
139
- subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
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 schemaArg = resource.zodSchemas.create ? `{ schema: ${pascal}CreateSchema }` : '{}';
150
- subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
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
- // Best case: validate AND pre-fill from detail
166
- schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
305
+ schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
167
306
  } else if (resource.zodSchemas.update) {
168
- // Validate, but no detail endpoint to pre-fill from
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 — only include what was built
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
- return [
203
- `export function ${resource.composableName}() {`,
204
- ...subConnectors,
205
- returnStatement,
206
- `}`,
207
- ].join('\n');
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(resource: ResourceInfo, composablesRelDir: string): string {
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 + function body.
226
- // Each section ends with its own trailing newline; join with \n adds one blank
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';