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.
- package/.editorconfig +26 -26
- package/.prettierignore +17 -17
- package/CONTRIBUTING.md +291 -291
- package/INSTRUCTIONS.md +327 -327
- package/LICENSE +202 -202
- package/README.md +231 -231
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +1 -1
- package/dist/cli/logo.js +5 -5
- package/dist/cli/messages.d.ts +1 -0
- package/dist/cli/messages.js +2 -0
- package/dist/cli/prompts.d.ts +5 -0
- package/dist/cli/prompts.js +12 -0
- package/dist/cli/types.d.ts +1 -1
- package/dist/generators/components/connector-generator/templates.js +12 -12
- package/dist/generators/use-async-data/templates.js +17 -17
- package/dist/generators/use-fetch/templates.js +14 -14
- package/dist/index.js +39 -27
- package/dist/module/index.js +19 -0
- package/dist/module/types.d.ts +7 -0
- package/docs/API-REFERENCE.md +886 -886
- package/docs/generated-components.md +615 -615
- package/docs/headless-composables-ui.md +569 -569
- package/eslint.config.js +85 -85
- package/package.json +1 -1
- package/src/cli/config.ts +147 -140
- package/src/cli/logger.ts +124 -124
- package/src/cli/logo.ts +25 -25
- package/src/cli/messages.ts +4 -0
- package/src/cli/prompts.ts +14 -1
- package/src/cli/types.ts +50 -50
- package/src/generators/components/connector-generator/generator.ts +138 -138
- package/src/generators/components/connector-generator/templates.ts +254 -254
- package/src/generators/components/connector-generator/types.ts +34 -34
- package/src/generators/components/schema-analyzer/index.ts +44 -44
- package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
- package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
- package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
- package/src/generators/components/schema-analyzer/types.ts +177 -177
- package/src/generators/nuxt-server/generator.ts +272 -272
- package/src/generators/shared/runtime/apiHelpers.ts +535 -535
- package/src/generators/shared/runtime/pagination.ts +323 -323
- package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
- package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
- package/src/generators/shared/runtime/useFormConnector.ts +139 -139
- package/src/generators/shared/runtime/useListConnector.ts +148 -148
- package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
- package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
- package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
- package/src/generators/use-async-data/generator.ts +205 -205
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
- package/src/generators/use-async-data/templates.ts +257 -257
- package/src/generators/use-fetch/generator.ts +170 -170
- package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
- package/src/generators/use-fetch/templates.ts +214 -214
- package/src/index.ts +305 -303
- package/src/module/index.ts +158 -133
- 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
|
+
}
|