nuxt-openapi-hyperfetch 0.2.7-alpha.1 → 0.3.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) 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 +309 -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 +68 -19
  16. package/dist/generators/shared/runtime/useFormConnector.js +8 -1
  17. package/dist/generators/shared/runtime/useListConnector.js +13 -6
  18. package/dist/generators/use-async-data/generator.js +4 -0
  19. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
  20. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
  21. package/dist/generators/use-async-data/templates.js +17 -17
  22. package/dist/generators/use-fetch/generator.js +4 -0
  23. package/dist/generators/use-fetch/templates.js +14 -14
  24. package/dist/index.js +40 -27
  25. package/dist/module/index.js +19 -0
  26. package/dist/module/types.d.ts +7 -0
  27. package/docs/API-REFERENCE.md +886 -886
  28. package/docs/generated-components.md +615 -615
  29. package/docs/headless-composables-ui.md +569 -569
  30. package/eslint.config.js +85 -85
  31. package/package.json +1 -1
  32. package/src/cli/config.ts +147 -140
  33. package/src/cli/logger.ts +124 -124
  34. package/src/cli/logo.ts +25 -25
  35. package/src/cli/messages.ts +4 -0
  36. package/src/cli/prompts.ts +14 -1
  37. package/src/cli/types.ts +50 -50
  38. package/src/generators/components/connector-generator/generator.ts +138 -138
  39. package/src/generators/components/connector-generator/templates.ts +307 -254
  40. package/src/generators/components/connector-generator/types.ts +34 -34
  41. package/src/generators/components/schema-analyzer/index.ts +44 -44
  42. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
  43. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
  44. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
  45. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
  46. package/src/generators/components/schema-analyzer/types.ts +177 -177
  47. package/src/generators/nuxt-server/generator.ts +272 -272
  48. package/src/generators/shared/runtime/apiHelpers.ts +535 -535
  49. package/src/generators/shared/runtime/pagination.ts +323 -323
  50. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
  51. package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
  52. package/src/generators/shared/runtime/useFormConnector.ts +147 -139
  53. package/src/generators/shared/runtime/useListConnector.ts +158 -148
  54. package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
  55. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
  56. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
  57. package/src/generators/use-async-data/generator.ts +213 -205
  58. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
  59. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
  60. package/src/generators/use-async-data/templates.ts +257 -257
  61. package/src/generators/use-fetch/generator.ts +178 -170
  62. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
  63. package/src/generators/use-fetch/templates.ts +214 -214
  64. package/src/index.ts +306 -303
  65. package/src/module/index.ts +158 -133
  66. package/src/module/types.ts +39 -31
  67. package/dist/generators/tanstack-query/generator.d.ts +0 -5
  68. package/dist/generators/tanstack-query/generator.js +0 -11
@@ -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
+ goToPage,
324
+ nextPage,
325
+ prevPage,
326
+ setPerPage,
327
+ })),
328
+ };
329
+ }