nuxt-openapi-hyperfetch 0.1.7-alpha.1 → 0.2.7-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 (97) hide show
  1. package/CONTRIBUTING.md +291 -292
  2. package/INSTRUCTIONS.md +327 -327
  3. package/LICENSE +202 -202
  4. package/README.md +231 -227
  5. package/dist/cli/logger.d.ts +26 -0
  6. package/dist/cli/logger.js +36 -0
  7. package/dist/cli/logo.js +5 -5
  8. package/dist/generators/components/connector-generator/generator.d.ts +12 -0
  9. package/dist/generators/components/connector-generator/generator.js +116 -0
  10. package/dist/generators/components/connector-generator/templates.d.ts +18 -0
  11. package/dist/generators/components/connector-generator/templates.js +222 -0
  12. package/dist/generators/components/connector-generator/types.d.ts +32 -0
  13. package/dist/generators/components/connector-generator/types.js +7 -0
  14. package/dist/generators/components/schema-analyzer/index.d.ts +17 -0
  15. package/dist/generators/components/schema-analyzer/index.js +20 -0
  16. package/dist/generators/components/schema-analyzer/intent-detector.d.ts +17 -0
  17. package/dist/generators/components/schema-analyzer/intent-detector.js +143 -0
  18. package/dist/generators/components/schema-analyzer/openapi-reader.d.ts +11 -0
  19. package/dist/generators/components/schema-analyzer/openapi-reader.js +76 -0
  20. package/dist/generators/components/schema-analyzer/resource-grouper.d.ts +6 -0
  21. package/dist/generators/components/schema-analyzer/resource-grouper.js +132 -0
  22. package/dist/generators/components/schema-analyzer/schema-field-mapper.d.ts +35 -0
  23. package/dist/generators/components/schema-analyzer/schema-field-mapper.js +220 -0
  24. package/dist/generators/components/schema-analyzer/types.d.ts +156 -0
  25. package/dist/generators/components/schema-analyzer/types.js +7 -0
  26. package/dist/generators/nuxt-server/generator.d.ts +2 -1
  27. package/dist/generators/nuxt-server/generator.js +21 -21
  28. package/dist/generators/shared/runtime/apiHelpers.d.ts +81 -41
  29. package/dist/generators/shared/runtime/apiHelpers.js +97 -104
  30. package/dist/generators/shared/runtime/pagination.d.ts +168 -0
  31. package/dist/generators/shared/runtime/pagination.js +179 -0
  32. package/dist/generators/shared/runtime/useDeleteConnector.d.ts +16 -0
  33. package/dist/generators/shared/runtime/useDeleteConnector.js +93 -0
  34. package/dist/generators/shared/runtime/useDetailConnector.d.ts +14 -0
  35. package/dist/generators/shared/runtime/useDetailConnector.js +50 -0
  36. package/dist/generators/shared/runtime/useFormConnector.d.ts +19 -0
  37. package/dist/generators/shared/runtime/useFormConnector.js +113 -0
  38. package/dist/generators/shared/runtime/useListConnector.d.ts +25 -0
  39. package/dist/generators/shared/runtime/useListConnector.js +125 -0
  40. package/dist/generators/shared/runtime/zod-error-merger.d.ts +23 -0
  41. package/dist/generators/shared/runtime/zod-error-merger.js +106 -0
  42. package/dist/generators/shared/templates/api-callbacks-plugin.js +54 -11
  43. package/dist/generators/shared/templates/api-pagination-plugin.d.ts +51 -0
  44. package/dist/generators/shared/templates/api-pagination-plugin.js +152 -0
  45. package/dist/generators/use-async-data/generator.d.ts +2 -1
  46. package/dist/generators/use-async-data/generator.js +14 -14
  47. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +114 -13
  48. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +88 -10
  49. package/dist/generators/use-async-data/templates.js +17 -17
  50. package/dist/generators/use-fetch/generator.d.ts +2 -1
  51. package/dist/generators/use-fetch/generator.js +12 -12
  52. package/dist/generators/use-fetch/runtime/useApiRequest.js +149 -40
  53. package/dist/generators/use-fetch/templates.js +14 -14
  54. package/dist/index.js +25 -0
  55. package/dist/module/index.d.ts +4 -0
  56. package/dist/module/index.js +93 -0
  57. package/dist/module/types.d.ts +27 -0
  58. package/dist/module/types.js +1 -0
  59. package/docs/API-REFERENCE.md +886 -887
  60. package/docs/generated-components.md +615 -0
  61. package/docs/headless-composables-ui.md +569 -0
  62. package/eslint.config.js +13 -0
  63. package/package.json +29 -2
  64. package/src/cli/config.ts +140 -140
  65. package/src/cli/logger.ts +124 -66
  66. package/src/cli/logo.ts +25 -25
  67. package/src/cli/types.ts +50 -50
  68. package/src/generators/components/connector-generator/generator.ts +138 -0
  69. package/src/generators/components/connector-generator/templates.ts +254 -0
  70. package/src/generators/components/connector-generator/types.ts +34 -0
  71. package/src/generators/components/schema-analyzer/index.ts +44 -0
  72. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -0
  73. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -0
  74. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -0
  75. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -0
  76. package/src/generators/components/schema-analyzer/types.ts +177 -0
  77. package/src/generators/nuxt-server/generator.ts +272 -270
  78. package/src/generators/shared/runtime/apiHelpers.ts +535 -507
  79. package/src/generators/shared/runtime/pagination.ts +323 -0
  80. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -0
  81. package/src/generators/shared/runtime/useDetailConnector.ts +64 -0
  82. package/src/generators/shared/runtime/useFormConnector.ts +139 -0
  83. package/src/generators/shared/runtime/useListConnector.ts +148 -0
  84. package/src/generators/shared/runtime/zod-error-merger.ts +119 -0
  85. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -352
  86. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -0
  87. package/src/generators/use-async-data/generator.ts +205 -204
  88. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -229
  89. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -245
  90. package/src/generators/use-async-data/templates.ts +257 -257
  91. package/src/generators/use-fetch/generator.ts +170 -169
  92. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -234
  93. package/src/generators/use-fetch/templates.ts +214 -214
  94. package/src/index.ts +303 -265
  95. package/src/module/index.ts +133 -0
  96. package/src/module/types.ts +31 -0
  97. package/src/generators/tanstack-query/generator.ts +0 -11
@@ -1,229 +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 } 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
-
20
- /**
21
- * Helper type to infer transformed data type
22
- * If transform is provided, infer its return type
23
- * If pick is provided, return partial object (type inference for nested paths is complex)
24
- * Otherwise, return original type
25
- */
26
- type MaybeTransformed<T, Options> = Options extends { transform: (...args: any) => infer R }
27
- ? R
28
- : Options extends { pick: ReadonlyArray<any> }
29
- ? any // With nested paths, type inference is complex, so we use any
30
- : T;
31
-
32
- /**
33
- * Options for useAsyncData API requests with lifecycle callbacks.
34
- * Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
35
- * Native options like baseURL, method, body, headers, query, lazy, server, immediate, dedupe, etc. are all available.
36
- * watch: boolean (true = auto-watch reactive params, false = disable auto-refresh)
37
- */
38
- export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> &
39
- Omit<UseFetchOptions<T>, 'transform' | 'pick' | 'watch'> & {
40
- /**
41
- * Enable automatic refresh when reactive params/url change (default: true).
42
- * Set to false to disable auto-refresh entirely.
43
- */
44
- watch?: boolean;
45
- };
46
-
47
- /**
48
- * Generic wrapper for API calls using Nuxt's useAsyncData
49
- * Supports:
50
- * - Lifecycle callbacks (onRequest, onSuccess, onError, onFinish)
51
- * - Request modification via onRequest return value
52
- * - Transform and pick operations
53
- * - Global headers from useApiHeaders or $getApiHeaders
54
- * - Watch pattern for reactive parameters
55
- */
56
- export function useApiAsyncData<T>(
57
- key: string,
58
- url: string | (() => string),
59
- options?: ApiAsyncDataOptions<T>
60
- ) {
61
- const {
62
- method = 'GET',
63
- body,
64
- headers = {},
65
- params,
66
- baseURL,
67
- cacheKey,
68
- transform,
69
- pick,
70
- onRequest,
71
- onSuccess,
72
- onError,
73
- onFinish,
74
- skipGlobalCallbacks,
75
- immediate = true,
76
- lazy = false,
77
- server = true,
78
- dedupe = 'cancel',
79
- watch: watchOption = true,
80
- ...restOptions
81
- } = options || {};
82
-
83
- // Resolve base URL once at setup time (not inside fetchFn to avoid warning on every request)
84
- const resolvedBaseURL = baseURL || getGlobalBaseUrl();
85
- if (!resolvedBaseURL) {
86
- console.warn(
87
- '[nuxt-openapi-hyperfetch] No baseURL configured. Set runtimeConfig.public.apiBaseUrl in nuxt.config.ts or pass baseURL in options.'
88
- );
89
- }
90
-
91
- // Create reactive watch sources — use refs/computeds directly so Vue can track them
92
- // watchOption: false disables auto-refresh entirely
93
- const watchSources =
94
- watchOption === false
95
- ? []
96
- : [
97
- ...(typeof url === 'function' ? [url] : []),
98
- ...(body ? (isRef(body) ? [body] : typeof body === 'object' ? [() => body] : []) : []),
99
- ...(params
100
- ? isRef(params)
101
- ? [params]
102
- : typeof params === 'object'
103
- ? [() => params]
104
- : []
105
- : []),
106
- ];
107
-
108
- // Build a reactive cache key: composableName + resolved URL + serialized query params
109
- // This ensures distinct params produce distinct keys preventing cache collisions
110
- const computedKey = () => {
111
- if (cacheKey) return cacheKey;
112
- const resolvedUrl = typeof url === 'function' ? url() : url;
113
- const resolvedParams = toValue(params);
114
- const paramsSuffix =
115
- resolvedParams && typeof resolvedParams === 'object' && Object.keys(resolvedParams).length > 0
116
- ? '-' + JSON.stringify(resolvedParams)
117
- : '';
118
- return `${key}-${resolvedUrl}${paramsSuffix}`;
119
- };
120
-
121
- // Fetch function for useAsyncData
122
- const fetchFn = async () => {
123
- // Get URL value for merging callbacks
124
- const finalUrl = typeof url === 'function' ? url() : url;
125
-
126
- // Merge local and global callbacks
127
- const mergedCallbacks = mergeCallbacks(
128
- finalUrl,
129
- { onRequest, onSuccess, onError, onFinish },
130
- skipGlobalCallbacks
131
- );
132
-
133
- try {
134
- // Get global headers
135
- const globalHeaders = getGlobalHeaders();
136
-
137
- // Prepare request context
138
- const requestContext: RequestContext = {
139
- url: finalUrl,
140
- method: method as any,
141
- headers: { ...globalHeaders, ...headers },
142
- body,
143
- params,
144
- };
145
-
146
- // Execute merged onRequest callback and potentially modify request
147
- const modifiedContext = { ...requestContext };
148
- if (mergedCallbacks.onRequest) {
149
- const result = await mergedCallbacks.onRequest(requestContext);
150
- // If onRequest returns modifications, apply them
151
- if (result && typeof result === 'object') {
152
- const modifications = result as ModifiedRequestContext;
153
- if (modifications.body !== undefined) {
154
- modifiedContext.body = modifications.body;
155
- }
156
- if (modifications.headers !== undefined) {
157
- modifiedContext.headers = {
158
- ...modifiedContext.headers,
159
- ...modifications.headers,
160
- };
161
- }
162
- if (modifications.params !== undefined) {
163
- modifiedContext.params = {
164
- ...modifiedContext.params,
165
- ...modifications.params,
166
- };
167
- }
168
- }
169
- }
170
-
171
- // Make the request with $fetch — toValue() unrefs any Ref/ComputedRef
172
- let data = await $fetch<T>(modifiedContext.url, {
173
- method: modifiedContext.method,
174
- headers: modifiedContext.headers,
175
- body: toValue(modifiedContext.body),
176
- params: toValue(modifiedContext.params),
177
- ...(resolvedBaseURL ? { baseURL: resolvedBaseURL } : {}),
178
- ...restOptions,
179
- });
180
-
181
- // Apply pick if provided
182
- if (pick) {
183
- data = applyPick(data, pick) as T;
184
- }
185
-
186
- // Apply transform if provided
187
- if (transform) {
188
- data = transform(data);
189
- }
190
-
191
- // Call merged onSuccess callback
192
- if (mergedCallbacks.onSuccess) {
193
- await mergedCallbacks.onSuccess(data, {
194
- url: finalUrl,
195
- method,
196
- headers: modifiedContext.headers,
197
- });
198
- }
199
-
200
- return data;
201
- } catch (error: any) {
202
- // Call merged onError callback
203
- if (mergedCallbacks.onError) {
204
- await mergedCallbacks.onError(error, { url: finalUrl, method, headers });
205
- }
206
- throw error;
207
- } finally {
208
- // Call merged onFinish callback
209
- if (mergedCallbacks.onFinish) {
210
- await mergedCallbacks.onFinish({
211
- url: finalUrl,
212
- method,
213
- headers: { ...getGlobalHeaders(), ...headers },
214
- });
215
- }
216
- }
217
- };
218
-
219
- // Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
220
- const result = useAsyncData<MaybeTransformed<T, ApiAsyncDataOptions<T>>>(computedKey, fetchFn, {
221
- immediate,
222
- lazy,
223
- server,
224
- dedupe,
225
- watch: watchOption === false ? [] : watchSources,
226
- });
227
-
228
- return result;
229
- }
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
+ }