nuxt-openapi-hyperfetch 0.3.0-beta → 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.
@@ -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
  }
@@ -141,6 +159,68 @@ function buildColumns(resource: ResourceInfo): string {
141
159
  return `const ${varName} = [\n${entries},\n];`;
142
160
  }
143
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
+
144
224
  /**
145
225
  * Build the body of the exported connector function.
146
226
  */
@@ -154,6 +234,10 @@ function buildFunctionBody(resource: ResourceInfo): string {
154
234
  const columnsVar = `${camel}Columns`;
155
235
  const subConnectors: string[] = [];
156
236
 
237
+ // Derived type names — must match buildOptionsInterface / buildReturnType
238
+ const optionsTypeName = `${pascalCase(resource.composableName)}Options`;
239
+ const returnTypeName = `${pascalCase(resource.composableName)}Return`;
240
+
157
241
  // Destructure options param — only what's relevant for this resource
158
242
  const optionKeys: string[] = [];
159
243
  if (resource.listEndpoint && hasColumns) {
@@ -169,67 +253,71 @@ function buildFunctionBody(resource: ResourceInfo): string {
169
253
  const optionsDestructure =
170
254
  optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
171
255
 
256
+ // ── List / table sub-connector ─────────────────────────────────────────────
172
257
  if (resource.listEndpoint) {
173
258
  const fn = toAsyncDataName(resource.listEndpoint.operationId);
174
- // paginated: true tells useListConnector to expose pagination helpers
175
- // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
176
- // We set it whenever the spec declares a list endpoint that has a response schema,
177
- // which is a reliable proxy for "this API returns structured data worth paginating".
259
+ const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
178
260
  const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
179
261
  const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
180
262
  const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
181
263
  const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
182
264
  const opts = allArgs ? `{ ${allArgs} }` : '{}';
183
- subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
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
+ );
184
282
  }
185
283
 
186
284
  if (resource.detailEndpoint) {
187
285
  const fn = toAsyncDataName(resource.detailEndpoint.operationId);
188
- subConnectors.push(` const detail = useDetailConnector(${fn});`);
286
+ subConnectors.push(` const detail = useDetailConnector(${fn}) as unknown as DetailConnectorReturn<${pascal}>;`);
189
287
  }
190
288
 
191
289
  if (resource.createEndpoint) {
192
290
  const fn = toAsyncDataName(resource.createEndpoint.operationId);
291
+ const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
193
292
  const schemaArg = resource.zodSchemas.create
194
293
  ? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
195
294
  : '{}';
196
- subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
295
+ subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
197
296
  }
198
297
 
199
298
  if (resource.updateEndpoint) {
200
299
  const fn = toAsyncDataName(resource.updateEndpoint.operationId);
201
300
  const hasDetail = !!resource.detailEndpoint;
301
+ const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
202
302
 
203
- // Build the options argument for useFormConnector:
204
- // schema → Zod schema for client-side validation before submission
205
- // loadWith → reference to the detail connector so the form auto-fills
206
- // when detail.item changes (user clicks "Edit" on a row)
207
- //
208
- // Four combinations are possible depending on what the spec provides:
209
303
  let schemaArg = '{}';
210
304
  if (resource.zodSchemas.update && hasDetail) {
211
- // Best case: validate AND pre-fill from detail
212
305
  schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
213
306
  } else if (resource.zodSchemas.update) {
214
- // Validate, but no detail endpoint to pre-fill from
215
307
  schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
216
308
  } else if (hasDetail) {
217
- // No Zod schema (no request body in spec), but still pre-fill from detail
218
309
  schemaArg = `{ loadWith: detail }`;
219
310
  }
220
- subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg});`);
311
+ subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
221
312
  }
222
313
 
223
314
  if (resource.deleteEndpoint) {
224
315
  const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
225
- subConnectors.push(` const deleteAction = useDeleteConnector(${fn});`);
316
+ subConnectors.push(` const deleteAction = useDeleteConnector(${fn}) as unknown as DeleteConnectorReturn<${pascal}>;`);
226
317
  }
227
318
 
228
- // Return object — only include what was built
229
- const returnKeys: string[] = [];
230
- if (resource.listEndpoint) {
231
- returnKeys.push('table');
232
- }
319
+ // Return object — always includes table (undefined when no list + no factory)
320
+ const returnKeys: string[] = ['table'];
233
321
  if (resource.detailEndpoint) {
234
322
  returnKeys.push('detail');
235
323
  }
@@ -243,17 +331,36 @@ function buildFunctionBody(resource: ResourceInfo): string {
243
331
  returnKeys.push('deleteAction');
244
332
  }
245
333
 
246
- const returnStatement = ` return { ${returnKeys.join(', ')} };`;
334
+ const returnStatement = ` return { ${returnKeys.join(', ')} } as ${returnTypeName};`;
247
335
 
248
- return [
249
- `export function ${resource.composableName}(options = {}) {`,
250
- optionsDestructure.trimEnd(),
251
- ...subConnectors,
252
- returnStatement,
253
- `}`,
254
- ]
255
- .filter((s) => s !== '')
256
- .join('\n');
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[] = [];
340
+
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');
257
364
  }
258
365
 
259
366
  // ─── Public API ───────────────────────────────────────────────────────────────
@@ -265,16 +372,21 @@ function buildFunctionBody(resource: ResourceInfo): string {
265
372
  * @param composablesRelDir Relative path from the connector dir to the
266
373
  * useAsyncData composables dir (e.g. '../use-async-data')
267
374
  */
268
- export function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string): string {
375
+ export function generateConnectorFile(
376
+ resource: ResourceInfo,
377
+ composablesRelDir: string,
378
+ sdkRelDir = '../..'
379
+ ): string {
269
380
  const header = generateFileHeader();
270
- const imports = buildImports(resource, composablesRelDir);
381
+ const imports = buildImports(resource, composablesRelDir, sdkRelDir);
271
382
  const schemas = buildZodSchemas(resource);
272
383
  const columns = buildColumns(resource);
384
+ const optionsInterface = buildOptionsInterface(resource);
385
+ const returnType = buildReturnType(resource);
273
386
  const fn = buildFunctionBody(resource);
274
387
 
275
- // Assemble file: header + imports + (optional) Zod blocks + columns const + function body.
276
- // Each section ends with its own trailing newline; join with \n adds one blank
277
- // 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.
278
390
  const parts: string[] = [header, imports];
279
391
  if (schemas.trim()) {
280
392
  parts.push(schemas);
@@ -282,6 +394,8 @@ export function generateConnectorFile(resource: ResourceInfo, composablesRelDir:
282
394
  if (columns.trim()) {
283
395
  parts.push(columns);
284
396
  }
397
+ parts.push(optionsInterface);
398
+ parts.push(returnType);
285
399
  parts.push(fn);
286
400
 
287
401
  return parts.join('\n') + '\n';
@@ -0,0 +1,142 @@
1
+ /**
2
+ * connector-types.ts — Structural return type interfaces for the 4 runtime connectors.
3
+ *
4
+ * Uses locally-defined minimal type aliases for Ref/ComputedRef/ShallowRef so this
5
+ * file compiles in the CLI context (where Vue is not installed) and remains
6
+ * structurally compatible with Vue's actual types in the user's Nuxt project.
7
+ *
8
+ * Copied to the user's project alongside the generated connectors and runtime helpers.
9
+ */
10
+
11
+ // Minimal structural aliases — compatible with Vue's Ref<T>, ComputedRef<T>, ShallowRef<T>.
12
+ // These are intentionally kept as simple as possible to avoid coupling to Vue's internals.
13
+ type Ref<T> = { value: T };
14
+ type ShallowRef<T> = { value: T };
15
+ type ComputedRef<T> = { readonly value: T };
16
+
17
+ // ─── Column / field defs (mirrors schema-analyzer output) ────────────────────
18
+
19
+ export interface ColumnDef {
20
+ key: string;
21
+ label: string;
22
+ type: string;
23
+ }
24
+
25
+ export interface FormFieldDef {
26
+ key: string;
27
+ label: string;
28
+ type: string;
29
+ required: boolean;
30
+ options?: { label: string; value: string }[];
31
+ placeholder?: string;
32
+ hidden?: boolean;
33
+ }
34
+
35
+ // ─── Pagination ───────────────────────────────────────────────────────────────
36
+
37
+ export interface PaginationState {
38
+ page: number;
39
+ perPage: number;
40
+ total: number;
41
+ totalPages: number;
42
+ goToPage: (page: number) => void;
43
+ nextPage: () => void;
44
+ prevPage: () => void;
45
+ setPerPage: (n: number) => void;
46
+ }
47
+
48
+ // ─── ListConnectorReturn ──────────────────────────────────────────────────────
49
+
50
+ export interface ListConnectorReturn<TRow = unknown> {
51
+ // State
52
+ rows: ComputedRef<TRow[]>;
53
+ columns: ComputedRef<ColumnDef[]>;
54
+ loading: ComputedRef<boolean>;
55
+ error: ComputedRef<unknown>;
56
+
57
+ // Pagination
58
+ pagination: ComputedRef<PaginationState | null>;
59
+ goToPage: (page: number) => void;
60
+ nextPage: () => void;
61
+ prevPage: () => void;
62
+ setPerPage: (n: number) => void;
63
+
64
+ // Selection
65
+ selected: Ref<TRow[]>;
66
+ onRowSelect: (row: TRow) => void;
67
+ clearSelection: () => void;
68
+
69
+ // Actions
70
+ refresh: () => void;
71
+
72
+ // CRUD coordination — public methods
73
+ create: () => void;
74
+ update: (row: TRow) => void;
75
+ remove: (row: TRow) => void;
76
+
77
+ // CRUD coordination — internal triggers (watch in the parent component)
78
+ _createTrigger: Ref<number>;
79
+ _updateTarget: ShallowRef<TRow | null>;
80
+ _deleteTarget: ShallowRef<TRow | null>;
81
+ }
82
+
83
+ // ─── DetailConnectorReturn ────────────────────────────────────────────────────
84
+
85
+ export interface DetailConnectorReturn<TItem = unknown> {
86
+ // State
87
+ item: ComputedRef<TItem | null>;
88
+ loading: ComputedRef<boolean>;
89
+ error: ComputedRef<unknown>;
90
+ fields: ComputedRef<FormFieldDef[]>;
91
+
92
+ // Actions
93
+ load: (id: string | number) => Promise<void>;
94
+ clear: () => void;
95
+
96
+ // Internals (advanced use)
97
+ _composable: unknown;
98
+ _currentId: Ref<string | number | null>;
99
+ }
100
+
101
+ // ─── FormConnectorReturn ──────────────────────────────────────────────────────
102
+
103
+ export interface FormConnectorReturn<TInput = Record<string, unknown>> {
104
+ // State
105
+ model: Ref<Partial<TInput>>;
106
+ errors: Ref<Record<string, string[]>>;
107
+ loading: Ref<boolean>;
108
+ submitError: Ref<unknown>;
109
+ submitted: Ref<boolean>;
110
+ isValid: ComputedRef<boolean>;
111
+ hasErrors: ComputedRef<boolean>;
112
+ fields: ComputedRef<FormFieldDef[]>;
113
+
114
+ // Callbacks (developer-assignable)
115
+ onSuccess: Ref<((data: unknown) => void) | null>;
116
+ onError: Ref<((err: unknown) => void) | null>;
117
+
118
+ // Actions
119
+ submit: () => Promise<void>;
120
+ reset: () => void;
121
+ setValues: (data: Partial<TInput>) => void;
122
+ }
123
+
124
+ // ─── DeleteConnectorReturn ────────────────────────────────────────────────────
125
+
126
+ export interface DeleteConnectorReturn<TItem = unknown> {
127
+ // State
128
+ target: Ref<TItem | null>;
129
+ isOpen: Ref<boolean>;
130
+ loading: Ref<boolean>;
131
+ error: Ref<unknown>;
132
+ hasTarget: ComputedRef<boolean>;
133
+
134
+ // Callbacks (developer-assignable)
135
+ onSuccess: Ref<((item: TItem) => void) | null>;
136
+ onError: Ref<((err: unknown) => void) | null>;
137
+
138
+ // Actions
139
+ setTarget: (item: TItem) => void;
140
+ cancel: () => void;
141
+ confirm: () => Promise<void>;
142
+ }
@@ -13,14 +13,16 @@
13
13
  import { ref, computed, shallowRef } from 'vue';
14
14
 
15
15
  /**
16
- * @param composableFn The generated useAsyncData composable, e.g. useAsyncDataGetPets
17
- * @param options Configuration for the list connector
16
+ * @param factory A zero-argument function that calls and returns the underlying
17
+ * useAsyncData composable, e.g. () => useAsyncDataGetPets(params)
18
+ * The factory is called once during connector setup (inside setup()).
19
+ * @param options Configuration for the list connector
18
20
  */
19
- export function useListConnector(composableFn, options = {}) {
21
+ export function useListConnector(factory, options = {}) {
20
22
  const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
21
23
 
22
24
  // ── Execute the underlying composable ──────────────────────────────────────
23
- const composable = composableFn({ paginated });
25
+ const composable = factory();
24
26
 
25
27
  // ── Derived state ──────────────────────────────────────────────────────────
26
28
 
@@ -37,14 +37,39 @@ type MaybeTransformed<T, Options> = Options extends { transform: (...args: any)
37
37
  ? any // With nested paths, type inference is complex, so we use any
38
38
  : T;
39
39
 
40
+ type PickInput = ReadonlyArray<string> | undefined;
41
+
42
+ type HasNestedPath<K extends ReadonlyArray<string>> =
43
+ Extract<K[number], `${string}.${string}`> extends never ? false : true;
44
+
45
+ type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string>
46
+ ? HasNestedPath<K> extends true
47
+ ? any
48
+ : Pick<T, Extract<K[number], keyof T>>
49
+ : T;
50
+
51
+ type InferPick<Options> = Options extends { pick: infer K extends ReadonlyArray<string> }
52
+ ? K
53
+ : undefined;
54
+
55
+ type InferData<T, Options> = Options extends { transform: (...args: any) => infer R }
56
+ ? R
57
+ : PickedData<T, InferPick<Options>>;
58
+
40
59
  /**
41
60
  * Options for useAsyncData API requests with lifecycle callbacks.
42
61
  * Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
43
62
  * Native options like baseURL, method, body, headers, query, lazy, server, immediate, dedupe, etc. are all available.
44
63
  * watch: boolean (true = auto-watch reactive params, false = disable auto-refresh)
45
64
  */
46
- export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> &
47
- Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'watch'> & {
65
+ export type ApiAsyncDataOptions<
66
+ T,
67
+ DataT = T,
68
+ PickT extends PickInput = undefined,
69
+ > = Omit<BaseApiRequestOptions<T>, 'transform' | 'pick'> &
70
+ Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick' | 'watch'> & {
71
+ pick?: PickT;
72
+ transform?: (data: PickedData<T, PickT>) => DataT;
48
73
  /**
49
74
  * Enable automatic refresh when reactive params/url change (default: true).
50
75
  * Set to false to disable auto-refresh entirely.
@@ -61,10 +86,13 @@ export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> &
61
86
  * - Global headers from useApiHeaders or $getApiHeaders
62
87
  * - Watch pattern for reactive parameters
63
88
  */
64
- export function useApiAsyncData<T>(
89
+ export function useApiAsyncData<
90
+ T,
91
+ Options extends ApiAsyncDataOptions<T, any, any> = ApiAsyncDataOptions<T>,
92
+ >(
65
93
  key: string,
66
94
  url: string | (() => string),
67
- options?: ApiAsyncDataOptions<T>
95
+ options?: Options
68
96
  ) {
69
97
  const {
70
98
  method = 'GET',
@@ -295,7 +323,7 @@ export function useApiAsyncData<T>(
295
323
  };
296
324
 
297
325
  // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
298
- const result = useAsyncData<MaybeTransformed<T, ApiAsyncDataOptions<T>>>(computedKey, fetchFn, {
326
+ const result = useAsyncData<InferData<T, Options>>(computedKey, fetchFn, {
299
327
  immediate,
300
328
  lazy,
301
329
  server,
@@ -48,18 +48,35 @@ type MaybeTransformedRaw<T, Options> = Options extends { transform: (...args: an
48
48
  ? RawResponse<any> // With nested paths, type inference is complex
49
49
  : RawResponse<T>;
50
50
 
51
+ type PickInput = ReadonlyArray<string> | undefined;
52
+
53
+ type HasNestedPath<K extends ReadonlyArray<string>> =
54
+ Extract<K[number], `${string}.${string}`> extends never ? false : true;
55
+
56
+ type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string>
57
+ ? HasNestedPath<K> extends true
58
+ ? any
59
+ : Pick<T, Extract<K[number], keyof T>>
60
+ : T;
61
+
51
62
  /**
52
63
  * Options for useAsyncData Raw API requests.
53
64
  * Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
54
65
  * onSuccess receives data AND the full response (headers, status, statusText).
55
66
  */
56
- export type ApiAsyncDataRawOptions<T> = Omit<BaseApiRequestOptions<T>, 'onSuccess'> &
57
- Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'onSuccess'> & {
67
+ export type ApiAsyncDataRawOptions<
68
+ T,
69
+ DataT = T,
70
+ PickT extends PickInput = undefined,
71
+ > = Omit<BaseApiRequestOptions<T>, 'onSuccess' | 'transform' | 'pick'> &
72
+ Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick' | 'onSuccess'> & {
73
+ pick?: PickT;
74
+ transform?: (data: PickedData<T, PickT>) => DataT;
58
75
  /**
59
76
  * Called when the request succeeds — receives both data and the full response object.
60
77
  */
61
78
  onSuccess?: (
62
- data: T,
79
+ data: DataT,
63
80
  response: { headers: Headers; status: number; statusText: string; url: string }
64
81
  ) => void | Promise<void>;
65
82
  };
@@ -76,10 +93,15 @@ export type ApiAsyncDataRawOptions<T> = Omit<BaseApiRequestOptions<T>, 'onSucces
76
93
  * - Global headers from useApiHeaders or $getApiHeaders
77
94
  * - Watch pattern for reactive parameters
78
95
  */
79
- export function useApiAsyncDataRaw<T>(
96
+ export function useApiAsyncDataRaw<
97
+ T,
98
+ DataT = T,
99
+ PickT extends PickInput = undefined,
100
+ Options extends ApiAsyncDataRawOptions<T, DataT, PickT> = ApiAsyncDataRawOptions<T, DataT, PickT>,
101
+ >(
80
102
  key: string,
81
103
  url: string | (() => string),
82
- options?: ApiAsyncDataRawOptions<T>
104
+ options?: Options
83
105
  ) {
84
106
  const {
85
107
  method = 'GET',
@@ -151,7 +173,7 @@ export function useApiAsyncDataRaw<T>(
151
173
  };
152
174
 
153
175
  // Fetch function for useAsyncData
154
- const fetchFn = async (): Promise<RawResponse<T>> => {
176
+ const fetchFn = async (): Promise<RawResponse<DataT>> => {
155
177
  // Get URL value for merging callbacks
156
178
  const finalUrl = typeof url === 'function' ? url() : url;
157
179
 
@@ -253,7 +275,7 @@ export function useApiAsyncDataRaw<T>(
253
275
  }
254
276
 
255
277
  // Construct the raw response object
256
- const rawResponse: RawResponse<T> = {
278
+ const rawResponse: RawResponse<DataT> = {
257
279
  data,
258
280
  headers: response.headers,
259
281
  status: response.status,
@@ -290,7 +312,7 @@ export function useApiAsyncDataRaw<T>(
290
312
  };
291
313
 
292
314
  // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
293
- const result = useAsyncData<MaybeTransformedRaw<T, ApiAsyncDataRawOptions<T>>>(computedKey, fetchFn, {
315
+ const result = useAsyncData<MaybeTransformedRaw<T, Options>>(computedKey, fetchFn, {
294
316
  immediate,
295
317
  lazy,
296
318
  server,