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,323 +1,323 @@
|
|
|
1
|
-
// @ts-nocheck - This file runs in user's Nuxt project with different TypeScript config
|
|
2
|
-
/**
|
|
3
|
-
* Pagination Helpers - Used by both useFetch and useAsyncData wrappers
|
|
4
|
-
* Handles global pagination config, request param injection, and response meta extraction.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Types
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Describes how to read pagination metadata from the backend response.
|
|
13
|
-
*
|
|
14
|
-
* - `metaSource: 'headers'` — metadata comes from HTTP response headers
|
|
15
|
-
* - `metaSource: 'body'` — metadata is nested inside the response JSON
|
|
16
|
-
*/
|
|
17
|
-
export interface PaginationMetaConfig {
|
|
18
|
-
/**
|
|
19
|
-
* Where to read pagination metadata from.
|
|
20
|
-
* - 'headers': from HTTP response headers (requires raw fetch)
|
|
21
|
-
* - 'body': from the response JSON body (default)
|
|
22
|
-
*/
|
|
23
|
-
metaSource: 'headers' | 'body';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Field names (header names or dot-notation body paths) for each pagination value.
|
|
27
|
-
*
|
|
28
|
-
* Examples for headers: 'X-Total-Count', 'X-Total-Pages', 'X-Page', 'X-Per-Page'
|
|
29
|
-
* Examples for body: 'meta.total', 'pagination.totalPages', 'page', 'perPage'
|
|
30
|
-
*/
|
|
31
|
-
fields: {
|
|
32
|
-
/** Total number of items across all pages */
|
|
33
|
-
total: string;
|
|
34
|
-
/** Total number of pages */
|
|
35
|
-
totalPages: string;
|
|
36
|
-
/** Current page number */
|
|
37
|
-
currentPage: string;
|
|
38
|
-
/** Number of items per page */
|
|
39
|
-
perPage: string;
|
|
40
|
-
/**
|
|
41
|
-
* If the actual data array lives inside a nested key (e.g. response.data),
|
|
42
|
-
* set this to that key name so the composable unwraps it automatically.
|
|
43
|
-
* Leave undefined if the response root IS the array.
|
|
44
|
-
*/
|
|
45
|
-
dataKey?: string;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Describes how to send pagination parameters to the backend.
|
|
51
|
-
*/
|
|
52
|
-
export interface PaginationRequestConfig {
|
|
53
|
-
/**
|
|
54
|
-
* Where to attach the pagination parameters.
|
|
55
|
-
* - 'query': as URL query string params (GET default, recommended)
|
|
56
|
-
* - 'body': merged into request body JSON (for POST-as-search APIs)
|
|
57
|
-
* - 'headers': as request headers
|
|
58
|
-
*/
|
|
59
|
-
sendAs: 'query' | 'body' | 'headers';
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Mapping from our internal names to your backend's parameter names.
|
|
63
|
-
* @example { page: 'page', perPage: 'limit' } // ?page=1&limit=20
|
|
64
|
-
* @example { page: 'p', perPage: 'per_page' } // ?p=1&per_page=20
|
|
65
|
-
* @example { page: 'offset', perPage: 'count' } // ?offset=1&count=20
|
|
66
|
-
*/
|
|
67
|
-
params: {
|
|
68
|
-
page: string;
|
|
69
|
-
perPage: string;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/** Default values applied when none are passed by the composable caller */
|
|
73
|
-
defaults: {
|
|
74
|
-
page: number;
|
|
75
|
-
perPage: number;
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Full pagination configuration object.
|
|
81
|
-
* Combine `meta` (how to read the response) and `request` (how to send params).
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* // Headers-based API (e.g. many REST frameworks)
|
|
85
|
-
* {
|
|
86
|
-
* meta: {
|
|
87
|
-
* metaSource: 'headers',
|
|
88
|
-
* fields: { total: 'X-Total-Count', totalPages: 'X-Total-Pages', currentPage: 'X-Page', perPage: 'X-Per-Page' }
|
|
89
|
-
* },
|
|
90
|
-
* request: {
|
|
91
|
-
* sendAs: 'query',
|
|
92
|
-
* params: { page: 'page', perPage: 'limit' },
|
|
93
|
-
* defaults: { page: 1, perPage: 20 }
|
|
94
|
-
* }
|
|
95
|
-
* }
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* // Body-based API (e.g. Laravel paginate())
|
|
99
|
-
* {
|
|
100
|
-
* meta: {
|
|
101
|
-
* metaSource: 'body',
|
|
102
|
-
* fields: { total: 'meta.total', totalPages: 'meta.last_page', currentPage: 'meta.current_page', perPage: 'meta.per_page', dataKey: 'data' }
|
|
103
|
-
* },
|
|
104
|
-
* request: {
|
|
105
|
-
* sendAs: 'query',
|
|
106
|
-
* params: { page: 'page', perPage: 'per_page' },
|
|
107
|
-
* defaults: { page: 1, perPage: 15 }
|
|
108
|
-
* }
|
|
109
|
-
* }
|
|
110
|
-
*/
|
|
111
|
-
export interface PaginationConfig {
|
|
112
|
-
meta: PaginationMetaConfig;
|
|
113
|
-
request: PaginationRequestConfig;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Reactive pagination state exposed to the composable caller.
|
|
118
|
-
*/
|
|
119
|
-
export interface PaginationState {
|
|
120
|
-
/** Current page number (reactive) */
|
|
121
|
-
currentPage: number;
|
|
122
|
-
/** Total pages as reported by the backend (reactive) */
|
|
123
|
-
totalPages: number;
|
|
124
|
-
/** Total item count as reported by the backend (reactive) */
|
|
125
|
-
total: number;
|
|
126
|
-
/** Current page size (reactive) */
|
|
127
|
-
perPage: number;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// Internal defaults
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
const DEFAULT_PAGINATION_CONFIG: PaginationConfig = {
|
|
135
|
-
meta: {
|
|
136
|
-
metaSource: 'body',
|
|
137
|
-
fields: {
|
|
138
|
-
total: 'total',
|
|
139
|
-
totalPages: 'totalPages',
|
|
140
|
-
currentPage: 'currentPage',
|
|
141
|
-
perPage: 'perPage',
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
request: {
|
|
145
|
-
sendAs: 'query',
|
|
146
|
-
params: { page: 'page', perPage: 'perPage' },
|
|
147
|
-
defaults: { page: 1, perPage: 20 },
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// Global config store
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
let _globalPaginationConfig: PaginationConfig | null = null;
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Register the global pagination configuration.
|
|
159
|
-
* Call this from your Nuxt plugin (plugins/api-pagination.ts).
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* export default defineNuxtPlugin(() => {
|
|
163
|
-
* provide('setGlobalApiPagination', setGlobalApiPagination);
|
|
164
|
-
* setGlobalApiPagination({ meta: { ... }, request: { ... } });
|
|
165
|
-
* })
|
|
166
|
-
*/
|
|
167
|
-
export function setGlobalApiPagination(config: PaginationConfig): void {
|
|
168
|
-
_globalPaginationConfig = config;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Retrieve the global pagination configuration, falling back to built-in defaults.
|
|
173
|
-
* Called internally by the runtime wrappers.
|
|
174
|
-
*/
|
|
175
|
-
export function getGlobalApiPagination(): PaginationConfig {
|
|
176
|
-
// Try Nuxt plugin provide first (runtime usage inside components)
|
|
177
|
-
try {
|
|
178
|
-
const nuxtApp = useNuxtApp();
|
|
179
|
-
// @ts-ignore
|
|
180
|
-
if (nuxtApp.$getGlobalApiPagination) {
|
|
181
|
-
// @ts-ignore
|
|
182
|
-
const config: PaginationConfig = nuxtApp.$getGlobalApiPagination();
|
|
183
|
-
if (config && typeof config === 'object') return config;
|
|
184
|
-
}
|
|
185
|
-
} catch {
|
|
186
|
-
// outside Nuxt context — fall through
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Then the module-level variable (set via setGlobalApiPagination)
|
|
190
|
-
if (_globalPaginationConfig) return _globalPaginationConfig;
|
|
191
|
-
|
|
192
|
-
return DEFAULT_PAGINATION_CONFIG;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
// Request helpers
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Build the pagination parameters to inject into the outgoing request.
|
|
201
|
-
*
|
|
202
|
-
* Returns an object with at most one of: `query`, `body`, or `headers` populated,
|
|
203
|
-
* depending on `config.request.sendAs`.
|
|
204
|
-
*/
|
|
205
|
-
export function buildPaginationRequest(
|
|
206
|
-
page: number,
|
|
207
|
-
perPage: number,
|
|
208
|
-
config: PaginationConfig
|
|
209
|
-
): { query?: Record<string, any>; body?: Record<string, any>; headers?: Record<string, string> } {
|
|
210
|
-
const { sendAs, params } = config.request;
|
|
211
|
-
const payload: Record<string, any> = {
|
|
212
|
-
[params.page]: page,
|
|
213
|
-
[params.perPage]: perPage,
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
if (sendAs === 'query') return { query: payload };
|
|
217
|
-
if (sendAs === 'body') return { body: payload };
|
|
218
|
-
if (sendAs === 'headers') {
|
|
219
|
-
// Header values must be strings
|
|
220
|
-
return {
|
|
221
|
-
headers: {
|
|
222
|
-
[params.page]: String(page),
|
|
223
|
-
[params.perPage]: String(perPage),
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
return { query: payload };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
// Response meta extraction helpers
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Navigate a dot-notation path on a plain object.
|
|
236
|
-
* Returns `undefined` if any intermediate key is missing.
|
|
237
|
-
*
|
|
238
|
-
* @example resolveDotPath({ meta: { total: 42 } }, 'meta.total') // → 42
|
|
239
|
-
*/
|
|
240
|
-
function resolveDotPath(obj: any, path: string): any {
|
|
241
|
-
if (!obj || typeof obj !== 'object') return undefined;
|
|
242
|
-
const keys = path.split('.');
|
|
243
|
-
let current: any = obj;
|
|
244
|
-
for (const key of keys) {
|
|
245
|
-
if (current == null || typeof current !== 'object') return undefined;
|
|
246
|
-
current = current[key];
|
|
247
|
-
}
|
|
248
|
-
return current;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Extract pagination metadata from a **body** response.
|
|
253
|
-
*
|
|
254
|
-
* @param responseData - The raw JSON returned by the backend
|
|
255
|
-
* @param config - Active pagination config
|
|
256
|
-
* @returns Partial pagination state (only fields that were found)
|
|
257
|
-
*/
|
|
258
|
-
export function extractPaginationMetaFromBody(
|
|
259
|
-
responseData: any,
|
|
260
|
-
config: PaginationConfig
|
|
261
|
-
): Partial<PaginationState> {
|
|
262
|
-
const { fields } = config.meta;
|
|
263
|
-
const result: Partial<PaginationState> = {};
|
|
264
|
-
|
|
265
|
-
const rawTotal = resolveDotPath(responseData, fields.total);
|
|
266
|
-
if (rawTotal !== undefined) result.total = Number(rawTotal);
|
|
267
|
-
|
|
268
|
-
const rawTotalPages = resolveDotPath(responseData, fields.totalPages);
|
|
269
|
-
if (rawTotalPages !== undefined) result.totalPages = Number(rawTotalPages);
|
|
270
|
-
|
|
271
|
-
const rawCurrentPage = resolveDotPath(responseData, fields.currentPage);
|
|
272
|
-
if (rawCurrentPage !== undefined) result.currentPage = Number(rawCurrentPage);
|
|
273
|
-
|
|
274
|
-
const rawPerPage = resolveDotPath(responseData, fields.perPage);
|
|
275
|
-
if (rawPerPage !== undefined) result.perPage = Number(rawPerPage);
|
|
276
|
-
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Extract pagination metadata from HTTP **response headers**.
|
|
282
|
-
*
|
|
283
|
-
* @param headers - The `Headers` object from the fetch response
|
|
284
|
-
* @param config - Active pagination config
|
|
285
|
-
* @returns Partial pagination state (only fields that were found)
|
|
286
|
-
*/
|
|
287
|
-
export function extractPaginationMetaFromHeaders(
|
|
288
|
-
headers: Headers,
|
|
289
|
-
config: PaginationConfig
|
|
290
|
-
): Partial<PaginationState> {
|
|
291
|
-
const { fields } = config.meta;
|
|
292
|
-
const result: Partial<PaginationState> = {};
|
|
293
|
-
|
|
294
|
-
const rawTotal = headers.get(fields.total);
|
|
295
|
-
if (rawTotal !== null) result.total = Number(rawTotal);
|
|
296
|
-
|
|
297
|
-
const rawTotalPages = headers.get(fields.totalPages);
|
|
298
|
-
if (rawTotalPages !== null) result.totalPages = Number(rawTotalPages);
|
|
299
|
-
|
|
300
|
-
const rawCurrentPage = headers.get(fields.currentPage);
|
|
301
|
-
if (rawCurrentPage !== null) result.currentPage = Number(rawCurrentPage);
|
|
302
|
-
|
|
303
|
-
const rawPerPage = headers.get(fields.perPage);
|
|
304
|
-
if (rawPerPage !== null) result.perPage = Number(rawPerPage);
|
|
305
|
-
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* If `config.meta.fields.dataKey` is set, unwrap the actual data array from the body.
|
|
311
|
-
* Otherwise return the body as-is.
|
|
312
|
-
*
|
|
313
|
-
* @example
|
|
314
|
-
* // Laravel paginate() response: { data: [...], meta: { total: 100, ... } }
|
|
315
|
-
* unwrapDataKey(response, config) // → response.data
|
|
316
|
-
*/
|
|
317
|
-
export function unwrapDataKey<T>(responseData: any, config: PaginationConfig): T {
|
|
318
|
-
const key = config.meta.fields.dataKey;
|
|
319
|
-
if (key && responseData && typeof responseData === 'object' && key in responseData) {
|
|
320
|
-
return responseData[key] as T;
|
|
321
|
-
}
|
|
322
|
-
return responseData as T;
|
|
323
|
-
}
|
|
1
|
+
// @ts-nocheck - This file runs in user's Nuxt project with different TypeScript config
|
|
2
|
+
/**
|
|
3
|
+
* Pagination Helpers - Used by both useFetch and useAsyncData wrappers
|
|
4
|
+
* Handles global pagination config, request param injection, and response meta extraction.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Describes how to read pagination metadata from the backend response.
|
|
13
|
+
*
|
|
14
|
+
* - `metaSource: 'headers'` — metadata comes from HTTP response headers
|
|
15
|
+
* - `metaSource: 'body'` — metadata is nested inside the response JSON
|
|
16
|
+
*/
|
|
17
|
+
export interface PaginationMetaConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Where to read pagination metadata from.
|
|
20
|
+
* - 'headers': from HTTP response headers (requires raw fetch)
|
|
21
|
+
* - 'body': from the response JSON body (default)
|
|
22
|
+
*/
|
|
23
|
+
metaSource: 'headers' | 'body';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Field names (header names or dot-notation body paths) for each pagination value.
|
|
27
|
+
*
|
|
28
|
+
* Examples for headers: 'X-Total-Count', 'X-Total-Pages', 'X-Page', 'X-Per-Page'
|
|
29
|
+
* Examples for body: 'meta.total', 'pagination.totalPages', 'page', 'perPage'
|
|
30
|
+
*/
|
|
31
|
+
fields: {
|
|
32
|
+
/** Total number of items across all pages */
|
|
33
|
+
total: string;
|
|
34
|
+
/** Total number of pages */
|
|
35
|
+
totalPages: string;
|
|
36
|
+
/** Current page number */
|
|
37
|
+
currentPage: string;
|
|
38
|
+
/** Number of items per page */
|
|
39
|
+
perPage: string;
|
|
40
|
+
/**
|
|
41
|
+
* If the actual data array lives inside a nested key (e.g. response.data),
|
|
42
|
+
* set this to that key name so the composable unwraps it automatically.
|
|
43
|
+
* Leave undefined if the response root IS the array.
|
|
44
|
+
*/
|
|
45
|
+
dataKey?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Describes how to send pagination parameters to the backend.
|
|
51
|
+
*/
|
|
52
|
+
export interface PaginationRequestConfig {
|
|
53
|
+
/**
|
|
54
|
+
* Where to attach the pagination parameters.
|
|
55
|
+
* - 'query': as URL query string params (GET default, recommended)
|
|
56
|
+
* - 'body': merged into request body JSON (for POST-as-search APIs)
|
|
57
|
+
* - 'headers': as request headers
|
|
58
|
+
*/
|
|
59
|
+
sendAs: 'query' | 'body' | 'headers';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mapping from our internal names to your backend's parameter names.
|
|
63
|
+
* @example { page: 'page', perPage: 'limit' } // ?page=1&limit=20
|
|
64
|
+
* @example { page: 'p', perPage: 'per_page' } // ?p=1&per_page=20
|
|
65
|
+
* @example { page: 'offset', perPage: 'count' } // ?offset=1&count=20
|
|
66
|
+
*/
|
|
67
|
+
params: {
|
|
68
|
+
page: string;
|
|
69
|
+
perPage: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Default values applied when none are passed by the composable caller */
|
|
73
|
+
defaults: {
|
|
74
|
+
page: number;
|
|
75
|
+
perPage: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Full pagination configuration object.
|
|
81
|
+
* Combine `meta` (how to read the response) and `request` (how to send params).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Headers-based API (e.g. many REST frameworks)
|
|
85
|
+
* {
|
|
86
|
+
* meta: {
|
|
87
|
+
* metaSource: 'headers',
|
|
88
|
+
* fields: { total: 'X-Total-Count', totalPages: 'X-Total-Pages', currentPage: 'X-Page', perPage: 'X-Per-Page' }
|
|
89
|
+
* },
|
|
90
|
+
* request: {
|
|
91
|
+
* sendAs: 'query',
|
|
92
|
+
* params: { page: 'page', perPage: 'limit' },
|
|
93
|
+
* defaults: { page: 1, perPage: 20 }
|
|
94
|
+
* }
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Body-based API (e.g. Laravel paginate())
|
|
99
|
+
* {
|
|
100
|
+
* meta: {
|
|
101
|
+
* metaSource: 'body',
|
|
102
|
+
* fields: { total: 'meta.total', totalPages: 'meta.last_page', currentPage: 'meta.current_page', perPage: 'meta.per_page', dataKey: 'data' }
|
|
103
|
+
* },
|
|
104
|
+
* request: {
|
|
105
|
+
* sendAs: 'query',
|
|
106
|
+
* params: { page: 'page', perPage: 'per_page' },
|
|
107
|
+
* defaults: { page: 1, perPage: 15 }
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
*/
|
|
111
|
+
export interface PaginationConfig {
|
|
112
|
+
meta: PaginationMetaConfig;
|
|
113
|
+
request: PaginationRequestConfig;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reactive pagination state exposed to the composable caller.
|
|
118
|
+
*/
|
|
119
|
+
export interface PaginationState {
|
|
120
|
+
/** Current page number (reactive) */
|
|
121
|
+
currentPage: number;
|
|
122
|
+
/** Total pages as reported by the backend (reactive) */
|
|
123
|
+
totalPages: number;
|
|
124
|
+
/** Total item count as reported by the backend (reactive) */
|
|
125
|
+
total: number;
|
|
126
|
+
/** Current page size (reactive) */
|
|
127
|
+
perPage: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Internal defaults
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
const DEFAULT_PAGINATION_CONFIG: PaginationConfig = {
|
|
135
|
+
meta: {
|
|
136
|
+
metaSource: 'body',
|
|
137
|
+
fields: {
|
|
138
|
+
total: 'total',
|
|
139
|
+
totalPages: 'totalPages',
|
|
140
|
+
currentPage: 'currentPage',
|
|
141
|
+
perPage: 'perPage',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
request: {
|
|
145
|
+
sendAs: 'query',
|
|
146
|
+
params: { page: 'page', perPage: 'perPage' },
|
|
147
|
+
defaults: { page: 1, perPage: 20 },
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Global config store
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
let _globalPaginationConfig: PaginationConfig | null = null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register the global pagination configuration.
|
|
159
|
+
* Call this from your Nuxt plugin (plugins/api-pagination.ts).
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* export default defineNuxtPlugin(() => {
|
|
163
|
+
* provide('setGlobalApiPagination', setGlobalApiPagination);
|
|
164
|
+
* setGlobalApiPagination({ meta: { ... }, request: { ... } });
|
|
165
|
+
* })
|
|
166
|
+
*/
|
|
167
|
+
export function setGlobalApiPagination(config: PaginationConfig): void {
|
|
168
|
+
_globalPaginationConfig = config;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Retrieve the global pagination configuration, falling back to built-in defaults.
|
|
173
|
+
* Called internally by the runtime wrappers.
|
|
174
|
+
*/
|
|
175
|
+
export function getGlobalApiPagination(): PaginationConfig {
|
|
176
|
+
// Try Nuxt plugin provide first (runtime usage inside components)
|
|
177
|
+
try {
|
|
178
|
+
const nuxtApp = useNuxtApp();
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
if (nuxtApp.$getGlobalApiPagination) {
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
const config: PaginationConfig = nuxtApp.$getGlobalApiPagination();
|
|
183
|
+
if (config && typeof config === 'object') return config;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// outside Nuxt context — fall through
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Then the module-level variable (set via setGlobalApiPagination)
|
|
190
|
+
if (_globalPaginationConfig) return _globalPaginationConfig;
|
|
191
|
+
|
|
192
|
+
return DEFAULT_PAGINATION_CONFIG;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Request helpers
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build the pagination parameters to inject into the outgoing request.
|
|
201
|
+
*
|
|
202
|
+
* Returns an object with at most one of: `query`, `body`, or `headers` populated,
|
|
203
|
+
* depending on `config.request.sendAs`.
|
|
204
|
+
*/
|
|
205
|
+
export function buildPaginationRequest(
|
|
206
|
+
page: number,
|
|
207
|
+
perPage: number,
|
|
208
|
+
config: PaginationConfig
|
|
209
|
+
): { query?: Record<string, any>; body?: Record<string, any>; headers?: Record<string, string> } {
|
|
210
|
+
const { sendAs, params } = config.request;
|
|
211
|
+
const payload: Record<string, any> = {
|
|
212
|
+
[params.page]: page,
|
|
213
|
+
[params.perPage]: perPage,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (sendAs === 'query') return { query: payload };
|
|
217
|
+
if (sendAs === 'body') return { body: payload };
|
|
218
|
+
if (sendAs === 'headers') {
|
|
219
|
+
// Header values must be strings
|
|
220
|
+
return {
|
|
221
|
+
headers: {
|
|
222
|
+
[params.page]: String(page),
|
|
223
|
+
[params.perPage]: String(perPage),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return { query: payload };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Response meta extraction helpers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Navigate a dot-notation path on a plain object.
|
|
236
|
+
* Returns `undefined` if any intermediate key is missing.
|
|
237
|
+
*
|
|
238
|
+
* @example resolveDotPath({ meta: { total: 42 } }, 'meta.total') // → 42
|
|
239
|
+
*/
|
|
240
|
+
function resolveDotPath(obj: any, path: string): any {
|
|
241
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
242
|
+
const keys = path.split('.');
|
|
243
|
+
let current: any = obj;
|
|
244
|
+
for (const key of keys) {
|
|
245
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
246
|
+
current = current[key];
|
|
247
|
+
}
|
|
248
|
+
return current;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Extract pagination metadata from a **body** response.
|
|
253
|
+
*
|
|
254
|
+
* @param responseData - The raw JSON returned by the backend
|
|
255
|
+
* @param config - Active pagination config
|
|
256
|
+
* @returns Partial pagination state (only fields that were found)
|
|
257
|
+
*/
|
|
258
|
+
export function extractPaginationMetaFromBody(
|
|
259
|
+
responseData: any,
|
|
260
|
+
config: PaginationConfig
|
|
261
|
+
): Partial<PaginationState> {
|
|
262
|
+
const { fields } = config.meta;
|
|
263
|
+
const result: Partial<PaginationState> = {};
|
|
264
|
+
|
|
265
|
+
const rawTotal = resolveDotPath(responseData, fields.total);
|
|
266
|
+
if (rawTotal !== undefined) result.total = Number(rawTotal);
|
|
267
|
+
|
|
268
|
+
const rawTotalPages = resolveDotPath(responseData, fields.totalPages);
|
|
269
|
+
if (rawTotalPages !== undefined) result.totalPages = Number(rawTotalPages);
|
|
270
|
+
|
|
271
|
+
const rawCurrentPage = resolveDotPath(responseData, fields.currentPage);
|
|
272
|
+
if (rawCurrentPage !== undefined) result.currentPage = Number(rawCurrentPage);
|
|
273
|
+
|
|
274
|
+
const rawPerPage = resolveDotPath(responseData, fields.perPage);
|
|
275
|
+
if (rawPerPage !== undefined) result.perPage = Number(rawPerPage);
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Extract pagination metadata from HTTP **response headers**.
|
|
282
|
+
*
|
|
283
|
+
* @param headers - The `Headers` object from the fetch response
|
|
284
|
+
* @param config - Active pagination config
|
|
285
|
+
* @returns Partial pagination state (only fields that were found)
|
|
286
|
+
*/
|
|
287
|
+
export function extractPaginationMetaFromHeaders(
|
|
288
|
+
headers: Headers,
|
|
289
|
+
config: PaginationConfig
|
|
290
|
+
): Partial<PaginationState> {
|
|
291
|
+
const { fields } = config.meta;
|
|
292
|
+
const result: Partial<PaginationState> = {};
|
|
293
|
+
|
|
294
|
+
const rawTotal = headers.get(fields.total);
|
|
295
|
+
if (rawTotal !== null) result.total = Number(rawTotal);
|
|
296
|
+
|
|
297
|
+
const rawTotalPages = headers.get(fields.totalPages);
|
|
298
|
+
if (rawTotalPages !== null) result.totalPages = Number(rawTotalPages);
|
|
299
|
+
|
|
300
|
+
const rawCurrentPage = headers.get(fields.currentPage);
|
|
301
|
+
if (rawCurrentPage !== null) result.currentPage = Number(rawCurrentPage);
|
|
302
|
+
|
|
303
|
+
const rawPerPage = headers.get(fields.perPage);
|
|
304
|
+
if (rawPerPage !== null) result.perPage = Number(rawPerPage);
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* If `config.meta.fields.dataKey` is set, unwrap the actual data array from the body.
|
|
311
|
+
* Otherwise return the body as-is.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* // Laravel paginate() response: { data: [...], meta: { total: 100, ... } }
|
|
315
|
+
* unwrapDataKey(response, config) // → response.data
|
|
316
|
+
*/
|
|
317
|
+
export function unwrapDataKey<T>(responseData: any, config: PaginationConfig): T {
|
|
318
|
+
const key = config.meta.fields.dataKey;
|
|
319
|
+
if (key && responseData && typeof responseData === 'object' && key in responseData) {
|
|
320
|
+
return responseData[key] as T;
|
|
321
|
+
}
|
|
322
|
+
return responseData as T;
|
|
323
|
+
}
|