nuxt-openapi-hyperfetch 0.2.7-alpha.1 → 0.2.8-alpha.1

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 (60) hide show
  1. package/.editorconfig +26 -26
  2. package/.prettierignore +17 -17
  3. package/CONTRIBUTING.md +291 -291
  4. package/INSTRUCTIONS.md +327 -327
  5. package/LICENSE +202 -202
  6. package/README.md +231 -231
  7. package/dist/cli/config.d.ts +9 -2
  8. package/dist/cli/config.js +1 -1
  9. package/dist/cli/logo.js +5 -5
  10. package/dist/cli/messages.d.ts +1 -0
  11. package/dist/cli/messages.js +2 -0
  12. package/dist/cli/prompts.d.ts +5 -0
  13. package/dist/cli/prompts.js +12 -0
  14. package/dist/cli/types.d.ts +1 -1
  15. package/dist/generators/components/connector-generator/templates.js +12 -12
  16. package/dist/generators/use-async-data/templates.js +17 -17
  17. package/dist/generators/use-fetch/templates.js +14 -14
  18. package/dist/index.js +39 -27
  19. package/dist/module/index.js +19 -0
  20. package/dist/module/types.d.ts +7 -0
  21. package/docs/API-REFERENCE.md +886 -886
  22. package/docs/generated-components.md +615 -615
  23. package/docs/headless-composables-ui.md +569 -569
  24. package/eslint.config.js +85 -85
  25. package/package.json +1 -1
  26. package/src/cli/config.ts +147 -140
  27. package/src/cli/logger.ts +124 -124
  28. package/src/cli/logo.ts +25 -25
  29. package/src/cli/messages.ts +4 -0
  30. package/src/cli/prompts.ts +14 -1
  31. package/src/cli/types.ts +50 -50
  32. package/src/generators/components/connector-generator/generator.ts +138 -138
  33. package/src/generators/components/connector-generator/templates.ts +254 -254
  34. package/src/generators/components/connector-generator/types.ts +34 -34
  35. package/src/generators/components/schema-analyzer/index.ts +44 -44
  36. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
  37. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
  38. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
  39. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
  40. package/src/generators/components/schema-analyzer/types.ts +177 -177
  41. package/src/generators/nuxt-server/generator.ts +272 -272
  42. package/src/generators/shared/runtime/apiHelpers.ts +535 -535
  43. package/src/generators/shared/runtime/pagination.ts +323 -323
  44. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
  45. package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
  46. package/src/generators/shared/runtime/useFormConnector.ts +139 -139
  47. package/src/generators/shared/runtime/useListConnector.ts +148 -148
  48. package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
  49. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
  50. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
  51. package/src/generators/use-async-data/generator.ts +205 -205
  52. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
  53. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
  54. package/src/generators/use-async-data/templates.ts +257 -257
  55. package/src/generators/use-fetch/generator.ts +170 -170
  56. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
  57. package/src/generators/use-fetch/templates.ts +214 -214
  58. package/src/index.ts +305 -303
  59. package/src/module/index.ts +158 -133
  60. package/src/module/types.ts +39 -31
@@ -1,329 +1,329 @@
1
- // @ts-nocheck
2
- /**
3
- * Nuxt Runtime Helper - This file is copied to the generated output
4
- * It requires Nuxt 3 to be installed in the target project
5
- */
6
- import { watch, ref, computed } from 'vue';
7
- import type { UseFetchOptions } from '#app';
8
- import {
9
- getGlobalHeaders,
10
- getGlobalBaseUrl,
11
- applyPick,
12
- applyRequestModifications,
13
- mergeCallbacks,
14
- type RequestContext,
15
- type ModifiedRequestContext,
16
- type FinishContext,
17
- type ApiRequestOptions as BaseApiRequestOptions,
18
- } from '../../shared/runtime/apiHelpers.js';
19
- import {
20
- getGlobalApiPagination,
21
- buildPaginationRequest,
22
- extractPaginationMetaFromBody,
23
- extractPaginationMetaFromHeaders,
24
- unwrapDataKey,
25
- type PaginationState,
26
- } from '../../shared/runtime/pagination.js';
27
-
28
- /**
29
- * Helper type to infer transformed data type
30
- * If transform is provided, infer its return type
31
- * If pick is provided, return partial object (type inference for nested paths is complex)
32
- * Otherwise, return original type
33
- */
34
- type MaybeTransformed<T, Options> = Options extends { transform: (...args: any) => infer R }
35
- ? R
36
- : Options extends { pick: ReadonlyArray<any> }
37
- ? any // With nested paths, type inference is complex, so we use any
38
- : T;
39
-
40
- /**
41
- * Options for useAsyncData API requests with lifecycle callbacks.
42
- * Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
43
- * Native options like baseURL, method, body, headers, query, lazy, server, immediate, dedupe, etc. are all available.
44
- * watch: boolean (true = auto-watch reactive params, false = disable auto-refresh)
45
- */
46
- export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> &
47
- Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'watch'> & {
48
- /**
49
- * Enable automatic refresh when reactive params/url change (default: true).
50
- * Set to false to disable auto-refresh entirely.
51
- */
52
- watch?: boolean;
53
- };
54
-
55
- /**
56
- * Generic wrapper for API calls using Nuxt's useAsyncData
57
- * Supports:
58
- * - Lifecycle callbacks (onRequest, onSuccess, onError, onFinish)
59
- * - Request modification via onRequest return value
60
- * - Transform and pick operations
61
- * - Global headers from useApiHeaders or $getApiHeaders
62
- * - Watch pattern for reactive parameters
63
- */
64
- export function useApiAsyncData<T>(
65
- key: string,
66
- url: string | (() => string),
67
- options?: ApiAsyncDataOptions<T>
68
- ) {
69
- const {
70
- method = 'GET',
71
- body,
72
- headers = {},
73
- params,
74
- baseURL,
75
- cacheKey,
76
- transform,
77
- pick,
78
- onRequest,
79
- onSuccess,
80
- onError,
81
- onFinish,
82
- skipGlobalCallbacks,
83
- immediate = true,
84
- lazy = false,
85
- server = true,
86
- dedupe = 'cancel',
87
- watch: watchOption = true,
88
- paginated,
89
- initialPage,
90
- initialPerPage,
91
- paginationConfig,
92
- ...restOptions
93
- } = options || {};
94
-
95
- // ---------------------------------------------------------------------------
96
- // Pagination setup
97
- // ---------------------------------------------------------------------------
98
- const activePaginationConfig = paginationConfig ?? (paginated ? getGlobalApiPagination() : null);
99
- const page = ref<number>(initialPage ?? activePaginationConfig?.request.defaults.page ?? 1);
100
- const perPage = ref<number>(initialPerPage ?? activePaginationConfig?.request.defaults.perPage ?? 20);
101
-
102
- const paginationState = ref<PaginationState>({
103
- currentPage: page.value,
104
- totalPages: 0,
105
- total: 0,
106
- perPage: perPage.value,
107
- });
108
-
109
- // Resolve base URL once at setup time (not inside fetchFn to avoid warning on every request)
110
- const resolvedBaseURL = baseURL || getGlobalBaseUrl();
111
- if (!resolvedBaseURL) {
112
- console.warn(
113
- '[nuxt-openapi-hyperfetch] No baseURL configured. Set runtimeConfig.public.apiBaseUrl in nuxt.config.ts or pass baseURL in options.'
114
- );
115
- }
116
-
117
- // Create reactive watch sources — use refs/computeds directly so Vue can track them
118
- // watchOption: false disables auto-refresh entirely
119
- const watchSources =
120
- watchOption === false
121
- ? []
122
- : [
123
- ...(typeof url === 'function' ? [url] : []),
124
- ...(body ? (isRef(body) ? [body] : typeof body === 'object' ? [() => body] : []) : []),
125
- ...(params
126
- ? isRef(params)
127
- ? [params]
128
- : typeof params === 'object'
129
- ? [() => params]
130
- : []
131
- : []),
132
- // Add pagination refs so page/perPage changes trigger re-fetch
133
- ...(paginated ? [page, perPage] : []),
134
- ];
135
-
136
- // Build a reactive cache key: composableName + resolved URL + serialized query params
137
- // This ensures distinct params produce distinct keys — preventing cache collisions
138
- const computedKey = () => {
139
- if (cacheKey) return cacheKey;
140
- const resolvedUrl = typeof url === 'function' ? url() : url;
141
- const resolvedParams = toValue(params);
142
- const paramsSuffix =
143
- resolvedParams && typeof resolvedParams === 'object' && Object.keys(resolvedParams).length > 0
144
- ? '-' + JSON.stringify(resolvedParams)
145
- : '';
146
- return `${key}-${resolvedUrl}${paramsSuffix}`;
147
- };
148
-
149
- // Fetch function for useAsyncData
150
- const fetchFn = async () => {
151
- // Get URL value for merging callbacks
152
- const finalUrl = typeof url === 'function' ? url() : url;
153
-
154
- // Merge local and global callbacks
155
- const mergedCallbacks = mergeCallbacks(
156
- finalUrl,
157
- method,
158
- { onRequest, onSuccess, onError, onFinish },
159
- skipGlobalCallbacks
160
- );
161
-
162
- try {
163
- // Get global headers
164
- const globalHeaders = getGlobalHeaders();
165
-
166
- // Prepare request context
167
- const requestContext: RequestContext = {
168
- url: finalUrl,
169
- method: method as any,
170
- headers: { ...globalHeaders, ...headers },
171
- body,
172
- params,
173
- };
174
-
175
- // Execute merged onRequest callback and potentially modify request
176
- const modifiedContext = { ...requestContext };
177
- if (mergedCallbacks.onRequest) {
178
- const result = await mergedCallbacks.onRequest(requestContext);
179
- // If onRequest returns modifications, apply them
180
- if (result && typeof result === 'object') {
181
- const modifications = result as ModifiedRequestContext;
182
- if (modifications.body !== undefined) {
183
- modifiedContext.body = modifications.body;
184
- }
185
- if (modifications.headers !== undefined) {
186
- modifiedContext.headers = {
187
- ...modifiedContext.headers,
188
- ...modifications.headers,
189
- };
190
- }
191
- if (modifications.query !== undefined) {
192
- modifiedContext.params = {
193
- ...modifiedContext.params,
194
- ...modifications.query,
195
- };
196
- }
197
- }
198
- }
199
-
200
- // Make the request with $fetch — toValue() unrefs any Ref/ComputedRef
201
- // For paginated requests with metaSource:'headers', use $fetch.raw for header access
202
- let data: T;
203
- if (paginated && activePaginationConfig) {
204
- // Inject pagination params into the correct location
205
- const paginationPayload = buildPaginationRequest(page.value, perPage.value, activePaginationConfig);
206
- const paginatedQuery = { ...toValue(modifiedContext.params), ...paginationPayload.query };
207
- const paginatedBody = paginationPayload.body
208
- ? { ...(toValue(modifiedContext.body) ?? {}), ...paginationPayload.body }
209
- : toValue(modifiedContext.body);
210
- const paginatedHeaders = paginationPayload.headers
211
- ? { ...modifiedContext.headers, ...paginationPayload.headers }
212
- : modifiedContext.headers;
213
-
214
- if (activePaginationConfig.meta.metaSource === 'headers') {
215
- // Need raw fetch to access response headers
216
- const response = await $fetch.raw<T>(modifiedContext.url, {
217
- method: modifiedContext.method,
218
- headers: paginatedHeaders,
219
- body: paginatedBody,
220
- params: paginatedQuery,
221
- ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
222
- ...restOptions,
223
- });
224
- // Extract pagination meta from headers
225
- const meta = extractPaginationMetaFromHeaders(response.headers, activePaginationConfig);
226
- if (meta.total !== undefined) paginationState.value.total = meta.total;
227
- if (meta.totalPages !== undefined) paginationState.value.totalPages = meta.totalPages;
228
- if (meta.currentPage !== undefined) paginationState.value.currentPage = meta.currentPage;
229
- if (meta.perPage !== undefined) paginationState.value.perPage = meta.perPage;
230
- data = unwrapDataKey<T>(response._data, activePaginationConfig);
231
- } else {
232
- // metaSource: 'body' — extract after receiving the data
233
- const rawData = await $fetch<any>(modifiedContext.url, {
234
- method: modifiedContext.method,
235
- headers: paginatedHeaders,
236
- body: paginatedBody,
237
- params: paginatedQuery,
238
- ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
239
- ...restOptions,
240
- });
241
- const meta = extractPaginationMetaFromBody(rawData, activePaginationConfig);
242
- if (meta.total !== undefined) paginationState.value.total = meta.total;
243
- if (meta.totalPages !== undefined) paginationState.value.totalPages = meta.totalPages;
244
- if (meta.currentPage !== undefined) paginationState.value.currentPage = meta.currentPage;
245
- if (meta.perPage !== undefined) paginationState.value.perPage = meta.perPage;
246
- data = unwrapDataKey<T>(rawData, activePaginationConfig);
247
- }
248
- } else {
249
- data = await $fetch<T>(modifiedContext.url, {
250
- method: modifiedContext.method,
251
- headers: modifiedContext.headers,
252
- body: toValue(modifiedContext.body),
253
- params: toValue(modifiedContext.params),
254
- ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
255
- ...restOptions,
256
- });
257
- }
258
-
259
- // Apply pick if provided
260
- if (pick) {
261
- data = applyPick(data, pick) as T;
262
- }
263
-
264
- // Apply transform if provided
265
- if (transform) {
266
- data = transform(data);
267
- }
268
-
269
- // Call merged onSuccess callback
270
- if (mergedCallbacks.onSuccess) {
271
- await mergedCallbacks.onSuccess(data, {
272
- url: finalUrl,
273
- method,
274
- headers: modifiedContext.headers,
275
- });
276
- }
277
-
278
- return data;
279
- } catch (error: any) {
280
- // Call merged onError callback
281
- if (mergedCallbacks.onError) {
282
- await mergedCallbacks.onError(error, { url: finalUrl, method, headers });
283
- }
284
- throw error;
285
- } finally {
286
- // Call merged onFinish callback
287
- if (mergedCallbacks.onFinish) {
288
- await mergedCallbacks.onFinish({
289
- url: finalUrl,
290
- method,
291
- headers: { ...getGlobalHeaders(), ...headers },
292
- });
293
- }
294
- }
295
- };
296
-
297
- // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
298
- const result = useAsyncData<MaybeTransformed<T, ApiAsyncDataOptions<T>>>(computedKey, fetchFn, {
299
- immediate,
300
- lazy,
301
- server,
302
- dedupe,
303
- watch: watchOption === false ? [] : watchSources,
304
- });
305
-
306
- if (!paginated) return result;
307
-
308
- // Pagination computed helpers
309
- const hasNextPage = computed(() => paginationState.value.currentPage < paginationState.value.totalPages);
310
- const hasPrevPage = computed(() => paginationState.value.currentPage > 1);
311
-
312
- const goToPage = (n: number) => { page.value = n; };
313
- const nextPage = () => { if (hasNextPage.value) goToPage(page.value + 1); };
314
- const prevPage = () => { if (hasPrevPage.value) goToPage(page.value - 1); };
315
- const setPerPage = (n: number) => { perPage.value = n; page.value = 1; };
316
-
317
- return {
318
- ...result,
319
- pagination: computed(() => ({
320
- ...paginationState.value,
321
- hasNextPage: hasNextPage.value,
322
- hasPrevPage: hasPrevPage.value,
323
- })),
324
- goToPage,
325
- nextPage,
326
- prevPage,
327
- setPerPage,
328
- };
329
- }
1
+ // @ts-nocheck
2
+ /**
3
+ * Nuxt Runtime Helper - This file is copied to the generated output
4
+ * It requires Nuxt 3 to be installed in the target project
5
+ */
6
+ import { watch, ref, computed } from 'vue';
7
+ import type { UseFetchOptions } from '#app';
8
+ import {
9
+ getGlobalHeaders,
10
+ getGlobalBaseUrl,
11
+ applyPick,
12
+ applyRequestModifications,
13
+ mergeCallbacks,
14
+ type RequestContext,
15
+ type ModifiedRequestContext,
16
+ type FinishContext,
17
+ type ApiRequestOptions as BaseApiRequestOptions,
18
+ } from '../../shared/runtime/apiHelpers.js';
19
+ import {
20
+ getGlobalApiPagination,
21
+ buildPaginationRequest,
22
+ extractPaginationMetaFromBody,
23
+ extractPaginationMetaFromHeaders,
24
+ unwrapDataKey,
25
+ type PaginationState,
26
+ } from '../../shared/runtime/pagination.js';
27
+
28
+ /**
29
+ * Helper type to infer transformed data type
30
+ * If transform is provided, infer its return type
31
+ * If pick is provided, return partial object (type inference for nested paths is complex)
32
+ * Otherwise, return original type
33
+ */
34
+ type MaybeTransformed<T, Options> = Options extends { transform: (...args: any) => infer R }
35
+ ? R
36
+ : Options extends { pick: ReadonlyArray<any> }
37
+ ? any // With nested paths, type inference is complex, so we use any
38
+ : T;
39
+
40
+ /**
41
+ * Options for useAsyncData API requests with lifecycle callbacks.
42
+ * Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
43
+ * Native options like baseURL, method, body, headers, query, lazy, server, immediate, dedupe, etc. are all available.
44
+ * watch: boolean (true = auto-watch reactive params, false = disable auto-refresh)
45
+ */
46
+ export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> &
47
+ Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'watch'> & {
48
+ /**
49
+ * Enable automatic refresh when reactive params/url change (default: true).
50
+ * Set to false to disable auto-refresh entirely.
51
+ */
52
+ watch?: boolean;
53
+ };
54
+
55
+ /**
56
+ * Generic wrapper for API calls using Nuxt's useAsyncData
57
+ * Supports:
58
+ * - Lifecycle callbacks (onRequest, onSuccess, onError, onFinish)
59
+ * - Request modification via onRequest return value
60
+ * - Transform and pick operations
61
+ * - Global headers from useApiHeaders or $getApiHeaders
62
+ * - Watch pattern for reactive parameters
63
+ */
64
+ export function useApiAsyncData<T>(
65
+ key: string,
66
+ url: string | (() => string),
67
+ options?: ApiAsyncDataOptions<T>
68
+ ) {
69
+ const {
70
+ method = 'GET',
71
+ body,
72
+ headers = {},
73
+ params,
74
+ baseURL,
75
+ cacheKey,
76
+ transform,
77
+ pick,
78
+ onRequest,
79
+ onSuccess,
80
+ onError,
81
+ onFinish,
82
+ skipGlobalCallbacks,
83
+ immediate = true,
84
+ lazy = false,
85
+ server = true,
86
+ dedupe = 'cancel',
87
+ watch: watchOption = true,
88
+ paginated,
89
+ initialPage,
90
+ initialPerPage,
91
+ paginationConfig,
92
+ ...restOptions
93
+ } = options || {};
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Pagination setup
97
+ // ---------------------------------------------------------------------------
98
+ const activePaginationConfig = paginationConfig ?? (paginated ? getGlobalApiPagination() : null);
99
+ const page = ref<number>(initialPage ?? activePaginationConfig?.request.defaults.page ?? 1);
100
+ const perPage = ref<number>(initialPerPage ?? activePaginationConfig?.request.defaults.perPage ?? 20);
101
+
102
+ const paginationState = ref<PaginationState>({
103
+ currentPage: page.value,
104
+ totalPages: 0,
105
+ total: 0,
106
+ perPage: perPage.value,
107
+ });
108
+
109
+ // Resolve base URL once at setup time (not inside fetchFn to avoid warning on every request)
110
+ const resolvedBaseURL = baseURL || getGlobalBaseUrl();
111
+ if (!resolvedBaseURL) {
112
+ console.warn(
113
+ '[nuxt-openapi-hyperfetch] No baseURL configured. Set runtimeConfig.public.apiBaseUrl in nuxt.config.ts or pass baseURL in options.'
114
+ );
115
+ }
116
+
117
+ // Create reactive watch sources — use refs/computeds directly so Vue can track them
118
+ // watchOption: false disables auto-refresh entirely
119
+ const watchSources =
120
+ watchOption === false
121
+ ? []
122
+ : [
123
+ ...(typeof url === 'function' ? [url] : []),
124
+ ...(body ? (isRef(body) ? [body] : typeof body === 'object' ? [() => body] : []) : []),
125
+ ...(params
126
+ ? isRef(params)
127
+ ? [params]
128
+ : typeof params === 'object'
129
+ ? [() => params]
130
+ : []
131
+ : []),
132
+ // Add pagination refs so page/perPage changes trigger re-fetch
133
+ ...(paginated ? [page, perPage] : []),
134
+ ];
135
+
136
+ // Build a reactive cache key: composableName + resolved URL + serialized query params
137
+ // This ensures distinct params produce distinct keys — preventing cache collisions
138
+ const computedKey = () => {
139
+ if (cacheKey) return cacheKey;
140
+ const resolvedUrl = typeof url === 'function' ? url() : url;
141
+ const resolvedParams = toValue(params);
142
+ const paramsSuffix =
143
+ resolvedParams && typeof resolvedParams === 'object' && Object.keys(resolvedParams).length > 0
144
+ ? '-' + JSON.stringify(resolvedParams)
145
+ : '';
146
+ return `${key}-${resolvedUrl}${paramsSuffix}`;
147
+ };
148
+
149
+ // Fetch function for useAsyncData
150
+ const fetchFn = async () => {
151
+ // Get URL value for merging callbacks
152
+ const finalUrl = typeof url === 'function' ? url() : url;
153
+
154
+ // Merge local and global callbacks
155
+ const mergedCallbacks = mergeCallbacks(
156
+ finalUrl,
157
+ method,
158
+ { onRequest, onSuccess, onError, onFinish },
159
+ skipGlobalCallbacks
160
+ );
161
+
162
+ try {
163
+ // Get global headers
164
+ const globalHeaders = getGlobalHeaders();
165
+
166
+ // Prepare request context
167
+ const requestContext: RequestContext = {
168
+ url: finalUrl,
169
+ method: method as any,
170
+ headers: { ...globalHeaders, ...headers },
171
+ body,
172
+ params,
173
+ };
174
+
175
+ // Execute merged onRequest callback and potentially modify request
176
+ const modifiedContext = { ...requestContext };
177
+ if (mergedCallbacks.onRequest) {
178
+ const result = await mergedCallbacks.onRequest(requestContext);
179
+ // If onRequest returns modifications, apply them
180
+ if (result && typeof result === 'object') {
181
+ const modifications = result as ModifiedRequestContext;
182
+ if (modifications.body !== undefined) {
183
+ modifiedContext.body = modifications.body;
184
+ }
185
+ if (modifications.headers !== undefined) {
186
+ modifiedContext.headers = {
187
+ ...modifiedContext.headers,
188
+ ...modifications.headers,
189
+ };
190
+ }
191
+ if (modifications.query !== undefined) {
192
+ modifiedContext.params = {
193
+ ...modifiedContext.params,
194
+ ...modifications.query,
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ // Make the request with $fetch — toValue() unrefs any Ref/ComputedRef
201
+ // For paginated requests with metaSource:'headers', use $fetch.raw for header access
202
+ let data: T;
203
+ if (paginated && activePaginationConfig) {
204
+ // Inject pagination params into the correct location
205
+ const paginationPayload = buildPaginationRequest(page.value, perPage.value, activePaginationConfig);
206
+ const paginatedQuery = { ...toValue(modifiedContext.params), ...paginationPayload.query };
207
+ const paginatedBody = paginationPayload.body
208
+ ? { ...(toValue(modifiedContext.body) ?? {}), ...paginationPayload.body }
209
+ : toValue(modifiedContext.body);
210
+ const paginatedHeaders = paginationPayload.headers
211
+ ? { ...modifiedContext.headers, ...paginationPayload.headers }
212
+ : modifiedContext.headers;
213
+
214
+ if (activePaginationConfig.meta.metaSource === 'headers') {
215
+ // Need raw fetch to access response headers
216
+ const response = await $fetch.raw<T>(modifiedContext.url, {
217
+ method: modifiedContext.method,
218
+ headers: paginatedHeaders,
219
+ body: paginatedBody,
220
+ params: paginatedQuery,
221
+ ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
222
+ ...restOptions,
223
+ });
224
+ // Extract pagination meta from headers
225
+ const meta = extractPaginationMetaFromHeaders(response.headers, activePaginationConfig);
226
+ if (meta.total !== undefined) paginationState.value.total = meta.total;
227
+ if (meta.totalPages !== undefined) paginationState.value.totalPages = meta.totalPages;
228
+ if (meta.currentPage !== undefined) paginationState.value.currentPage = meta.currentPage;
229
+ if (meta.perPage !== undefined) paginationState.value.perPage = meta.perPage;
230
+ data = unwrapDataKey<T>(response._data, activePaginationConfig);
231
+ } else {
232
+ // metaSource: 'body' — extract after receiving the data
233
+ const rawData = await $fetch<any>(modifiedContext.url, {
234
+ method: modifiedContext.method,
235
+ headers: paginatedHeaders,
236
+ body: paginatedBody,
237
+ params: paginatedQuery,
238
+ ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
239
+ ...restOptions,
240
+ });
241
+ const meta = extractPaginationMetaFromBody(rawData, activePaginationConfig);
242
+ if (meta.total !== undefined) paginationState.value.total = meta.total;
243
+ if (meta.totalPages !== undefined) paginationState.value.totalPages = meta.totalPages;
244
+ if (meta.currentPage !== undefined) paginationState.value.currentPage = meta.currentPage;
245
+ if (meta.perPage !== undefined) paginationState.value.perPage = meta.perPage;
246
+ data = unwrapDataKey<T>(rawData, activePaginationConfig);
247
+ }
248
+ } else {
249
+ data = await $fetch<T>(modifiedContext.url, {
250
+ method: modifiedContext.method,
251
+ headers: modifiedContext.headers,
252
+ body: toValue(modifiedContext.body),
253
+ params: toValue(modifiedContext.params),
254
+ ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
255
+ ...restOptions,
256
+ });
257
+ }
258
+
259
+ // Apply pick if provided
260
+ if (pick) {
261
+ data = applyPick(data, pick) as T;
262
+ }
263
+
264
+ // Apply transform if provided
265
+ if (transform) {
266
+ data = transform(data);
267
+ }
268
+
269
+ // Call merged onSuccess callback
270
+ if (mergedCallbacks.onSuccess) {
271
+ await mergedCallbacks.onSuccess(data, {
272
+ url: finalUrl,
273
+ method,
274
+ headers: modifiedContext.headers,
275
+ });
276
+ }
277
+
278
+ return data;
279
+ } catch (error: any) {
280
+ // Call merged onError callback
281
+ if (mergedCallbacks.onError) {
282
+ await mergedCallbacks.onError(error, { url: finalUrl, method, headers });
283
+ }
284
+ throw error;
285
+ } finally {
286
+ // Call merged onFinish callback
287
+ if (mergedCallbacks.onFinish) {
288
+ await mergedCallbacks.onFinish({
289
+ url: finalUrl,
290
+ method,
291
+ headers: { ...getGlobalHeaders(), ...headers },
292
+ });
293
+ }
294
+ }
295
+ };
296
+
297
+ // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
298
+ const result = useAsyncData<MaybeTransformed<T, ApiAsyncDataOptions<T>>>(computedKey, fetchFn, {
299
+ immediate,
300
+ lazy,
301
+ server,
302
+ dedupe,
303
+ watch: watchOption === false ? [] : watchSources,
304
+ });
305
+
306
+ if (!paginated) return result;
307
+
308
+ // Pagination computed helpers
309
+ const hasNextPage = computed(() => paginationState.value.currentPage < paginationState.value.totalPages);
310
+ const hasPrevPage = computed(() => paginationState.value.currentPage > 1);
311
+
312
+ const goToPage = (n: number) => { page.value = n; };
313
+ const nextPage = () => { if (hasNextPage.value) goToPage(page.value + 1); };
314
+ const prevPage = () => { if (hasPrevPage.value) goToPage(page.value - 1); };
315
+ const setPerPage = (n: number) => { perPage.value = n; page.value = 1; };
316
+
317
+ return {
318
+ ...result,
319
+ pagination: computed(() => ({
320
+ ...paginationState.value,
321
+ hasNextPage: hasNextPage.value,
322
+ hasPrevPage: hasPrevPage.value,
323
+ })),
324
+ goToPage,
325
+ nextPage,
326
+ prevPage,
327
+ setPerPage,
328
+ };
329
+ }