@bagelink/sdk 1.20.54 → 1.20.58

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/src/index.js ADDED
@@ -0,0 +1,715 @@
1
+ import ax from 'axios';
2
+ import { ref } from 'vue';
3
+ import { createSSEStream, createSSEStreamPost } from './openAPITools/streamClient';
4
+ export * from './openAPITools';
5
+ export { default as openAPI } from './openAPITools';
6
+ export { createSSEStream, createSSEStreamPost, StreamController } from './openAPITools/streamClient';
7
+ export { unwrap } from './unwrap';
8
+ /**
9
+ * Enhanced API response that adds metadata to the response data
10
+ * - For arrays: acts as array with metadata properties
11
+ * - For objects: object with metadata properties (no array wrapping!)
12
+ * @template TData - The response data type
13
+ */
14
+ export class ApiResponse extends Array {
15
+ /** Raw axios response */
16
+ $raw;
17
+ /** Total number of items (from x-total header) */
18
+ total;
19
+ /** Number of items per page (from x-limit header) */
20
+ limit;
21
+ /** Number of items skipped (from x-offset header) */
22
+ offset;
23
+ /** Current page number (calculated: Math.floor(offset / limit) + 1) */
24
+ page;
25
+ /** Total number of pages (calculated: Math.ceil(total / limit)) */
26
+ totalPages;
27
+ /** Rate limit remaining (from x-ratelimit-remaining header) */
28
+ rateLimit;
29
+ /** Rate limit reset timestamp (from x-ratelimit-reset header) */
30
+ rateLimitReset;
31
+ /** All response metadata */
32
+ $metadata;
33
+ constructor(data, response) {
34
+ // Must call super first when extending Array
35
+ super();
36
+ // For arrays, add items and metadata
37
+ if (Array.isArray(data)) {
38
+ this.push(...data);
39
+ if (response) {
40
+ this.$raw = response;
41
+ this.$metadata = ApiResponse.parseMetadataStatic(response);
42
+ this.total = this.$metadata.total;
43
+ this.limit = this.$metadata.limit;
44
+ this.offset = this.$metadata.offset;
45
+ this.page = this.$metadata.page;
46
+ this.totalPages = this.$metadata.totalPages;
47
+ this.rateLimit = this.$metadata.rateLimit;
48
+ this.rateLimitReset = this.$metadata.rateLimitReset;
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Create an ApiResponse from data and response
54
+ * Factory method to handle both arrays and objects
55
+ */
56
+ static create(data, response) {
57
+ // Primitive payloads cannot carry response metadata. Return them as-is
58
+ // instead of boxing them into an object that loses the actual value.
59
+ if (data === null || typeof data !== 'object') {
60
+ return data;
61
+ }
62
+ // For single objects, attach metadata directly
63
+ if (!Array.isArray(data)) {
64
+ const obj = Object.assign(Object.create(null), data);
65
+ obj.$raw = response;
66
+ obj.$metadata = ApiResponse.parseMetadataStatic(response);
67
+ obj.total = obj.$metadata.total;
68
+ obj.limit = obj.$metadata.limit;
69
+ obj.offset = obj.$metadata.offset;
70
+ obj.page = obj.$metadata.page;
71
+ obj.totalPages = obj.$metadata.totalPages;
72
+ obj.rateLimit = obj.$metadata.rateLimit;
73
+ obj.rateLimitReset = obj.$metadata.rateLimitReset;
74
+ obj.getHeader = (name) => response?.headers[name.toLowerCase()];
75
+ obj.hasPagination = obj.total !== undefined || obj.offset !== undefined;
76
+ obj.hasNextPage = obj.total !== undefined && obj.limit !== undefined && obj.offset !== undefined
77
+ ? obj.offset + obj.limit < obj.total
78
+ : false;
79
+ obj.hasPrevPage = obj.offset !== undefined ? obj.offset > 0 : false;
80
+ return obj;
81
+ }
82
+ // For arrays, create ApiResponse instance
83
+ const instance = new ApiResponse(data, response);
84
+ return instance;
85
+ }
86
+ static parseMetadataStatic(response) {
87
+ const { headers } = response;
88
+ const metadata = {};
89
+ const get = (name) => {
90
+ if (typeof headers.get === 'function')
91
+ return headers.get(name);
92
+ const lower = name.toLowerCase();
93
+ const key = Object.keys(headers).find(k => k.toLowerCase() === lower);
94
+ return key ? headers[key] : undefined;
95
+ };
96
+ if (get('x-total')) {
97
+ metadata.total = Number(get('x-total'));
98
+ }
99
+ if (get('x-limit')) {
100
+ metadata.limit = Number(get('x-limit'));
101
+ }
102
+ if (get('x-offset')) {
103
+ metadata.offset = Number(get('x-offset'));
104
+ }
105
+ if (metadata.total !== undefined && metadata.limit) {
106
+ metadata.totalPages = Math.ceil(metadata.total / metadata.limit);
107
+ }
108
+ if (metadata.offset !== undefined && metadata.limit) {
109
+ metadata.page = Math.floor(metadata.offset / metadata.limit) + 1;
110
+ }
111
+ if (headers['x-ratelimit-remaining'] || headers['x-rate-limit-remaining']) {
112
+ metadata.rateLimit = Number(headers['x-ratelimit-remaining'] || headers['x-rate-limit-remaining']);
113
+ }
114
+ if (headers['x-ratelimit-reset'] || headers['x-rate-limit-reset']) {
115
+ metadata.rateLimitReset = Number(headers['x-ratelimit-reset'] || headers['x-rate-limit-reset']);
116
+ }
117
+ Object.keys(headers).forEach((key) => {
118
+ if (key.startsWith('x-') && !metadata[key]) {
119
+ metadata[key] = headers[key];
120
+ }
121
+ });
122
+ return metadata;
123
+ }
124
+ getHeader(name) {
125
+ return this.$raw.headers[name.toLowerCase()];
126
+ }
127
+ get hasPagination() {
128
+ return this.total !== undefined || this.offset !== undefined;
129
+ }
130
+ get hasNextPage() {
131
+ if (this.total === undefined || this.limit === undefined || this.offset === undefined)
132
+ return false;
133
+ return this.offset + this.limit < this.total;
134
+ }
135
+ get hasPrevPage() {
136
+ if (this.offset === undefined)
137
+ return false;
138
+ return this.offset > 0;
139
+ }
140
+ }
141
+ /**
142
+ * Parse FastAPI/Pydantic validation errors into human-readable format
143
+ */
144
+ export function parseApiError(error) {
145
+ const axiosError = error;
146
+ if (!axiosError.response?.data) {
147
+ return {
148
+ message: axiosError.message || 'An unexpected error occurred',
149
+ raw: error
150
+ };
151
+ }
152
+ const { detail } = axiosError.response.data;
153
+ if (typeof detail === 'string') {
154
+ return {
155
+ message: detail,
156
+ raw: error
157
+ };
158
+ }
159
+ if (Array.isArray(detail)) {
160
+ const fields = {};
161
+ const messages = [];
162
+ for (const err of detail) {
163
+ const fieldPath = err.loc.slice(1).join('.');
164
+ const message = err.msg;
165
+ if (fieldPath) {
166
+ if (!fields[fieldPath]) {
167
+ fields[fieldPath] = [];
168
+ }
169
+ fields[fieldPath].push(message);
170
+ }
171
+ messages.push(fieldPath ? `${fieldPath}: ${message}` : message);
172
+ }
173
+ return {
174
+ message: messages.join('; '),
175
+ fields,
176
+ raw: error
177
+ };
178
+ }
179
+ return {
180
+ message: 'An error occurred',
181
+ raw: error
182
+ };
183
+ }
184
+ /**
185
+ * Format field errors for display
186
+ */
187
+ export function formatFieldErrors(fields) {
188
+ return Object.entries(fields)
189
+ .map(([field, errors]) => `${field}: ${errors.join(', ')}`)
190
+ .join('\n');
191
+ }
192
+ function joinURL(baseURL, path) {
193
+ if (/^https?:\/\//i.test(path))
194
+ return path;
195
+ if (!baseURL)
196
+ return path;
197
+ return `${baseURL.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
198
+ }
199
+ async function getTransportHeaders(config, options) {
200
+ const optionHeaders = typeof options.headers === 'function'
201
+ ? await options.headers()
202
+ : (options.headers ?? {});
203
+ const configuredHeaders = config.axiosConfig?.headers;
204
+ const headers = typeof configuredHeaders?.toJSON === 'function'
205
+ ? configuredHeaders.toJSON()
206
+ : (configuredHeaders ?? {});
207
+ const token = await config.auth?.getToken?.();
208
+ return {
209
+ ...headers,
210
+ ...(token ? { Authorization: `${config.auth?.tokenPrefix ?? 'Bearer'} ${token}` } : {}),
211
+ ...optionHeaders,
212
+ };
213
+ }
214
+ /**
215
+ * Create an isolated transport. Multiple generated clients can safely use
216
+ * different base URLs, credentials, auth providers, and interceptors.
217
+ */
218
+ export function createApiTransport(config = {}) {
219
+ const configRef = ref(null);
220
+ const lastResponse = ref(null);
221
+ const axios = ax.create({
222
+ baseURL: config.baseURL,
223
+ withCredentials: config.withCredentials ?? config.axiosConfig?.withCredentials ?? true,
224
+ paramsSerializer: { indexes: null },
225
+ ...config.axiosConfig,
226
+ });
227
+ const streamOptions = async (options = {}) => {
228
+ const currentConfig = configRef.value ?? {};
229
+ return {
230
+ ...options,
231
+ withCredentials: options.withCredentials ?? currentConfig.withCredentials,
232
+ headers: await getTransportHeaders(currentConfig, options),
233
+ };
234
+ };
235
+ const streamURL = (path) => joinURL(configRef.value?.streamBaseURL ?? axios.defaults.baseURL ?? '', path);
236
+ const transport = {
237
+ axios,
238
+ config: configRef,
239
+ lastResponse,
240
+ getStreamBaseURL: () => configRef.value?.streamBaseURL ?? axios.defaults.baseURL ?? '',
241
+ createSSEStream: (path, options = {}) => createSSEStream(streamURL(path), {
242
+ ...options,
243
+ headers: async () => (await streamOptions(options)).headers,
244
+ withCredentials: options.withCredentials ?? configRef.value?.withCredentials,
245
+ }),
246
+ createSSEStreamPost: (path, body, options = {}) => createSSEStreamPost(streamURL(path), body, {
247
+ ...options,
248
+ headers: async () => (await streamOptions(options)).headers,
249
+ withCredentials: options.withCredentials ?? configRef.value?.withCredentials,
250
+ }),
251
+ };
252
+ return configureApiTransport(transport, config);
253
+ }
254
+ /**
255
+ * Reconfigure an existing transport without changing its Axios identity.
256
+ * This supports legacy generated `axios` exports while keeping bindings isolated.
257
+ */
258
+ export function configureApiTransport(transport, config = {}) {
259
+ const previousConfig = transport.config.value ?? {};
260
+ const resolvedConfig = {
261
+ ...previousConfig,
262
+ ...config,
263
+ axiosConfig: {
264
+ ...previousConfig.axiosConfig,
265
+ ...config.axiosConfig,
266
+ },
267
+ withCredentials: config.withCredentials
268
+ ?? config.axiosConfig?.withCredentials
269
+ ?? previousConfig.withCredentials
270
+ ?? true,
271
+ };
272
+ transport.config.value = resolvedConfig;
273
+ transport.axios.defaults.baseURL = resolvedConfig.baseURL;
274
+ transport.axios.defaults.withCredentials = resolvedConfig.withCredentials;
275
+ if (resolvedConfig.axiosConfig) {
276
+ Object.assign(transport.axios.defaults, resolvedConfig.axiosConfig);
277
+ }
278
+ transport.axios.interceptors.request.clear();
279
+ transport.axios.interceptors.response.clear();
280
+ if (resolvedConfig.auth) {
281
+ transport.axios.interceptors.request.use(async (requestConfig) => {
282
+ const token = await resolvedConfig.auth.getToken?.();
283
+ if (token) {
284
+ requestConfig.headers.Authorization = `${resolvedConfig.auth.tokenPrefix ?? 'Bearer'} ${token}`;
285
+ }
286
+ return requestConfig;
287
+ });
288
+ }
289
+ if (resolvedConfig.interceptors?.request) {
290
+ transport.axios.interceptors.request.use(resolvedConfig.interceptors.request);
291
+ }
292
+ transport.axios.interceptors.response.use((response) => {
293
+ transport.lastResponse.value = response;
294
+ return resolvedConfig.interceptors?.response?.(response) ?? response;
295
+ }, async (error) => {
296
+ if (error.response?.status === 401) {
297
+ await resolvedConfig.auth?.onUnauthorized?.();
298
+ }
299
+ resolvedConfig.onError?.(parseApiError(error), error);
300
+ throw error;
301
+ });
302
+ return transport;
303
+ }
304
+ /**
305
+ * Composable for managing API request state
306
+ */
307
+ export function useApiRequest(requestFn, options = {}) {
308
+ const loading = ref(false);
309
+ const error = ref(null);
310
+ const data = ref(null);
311
+ const execute = async () => {
312
+ loading.value = true;
313
+ error.value = null;
314
+ try {
315
+ const result = await requestFn();
316
+ data.value = result;
317
+ return result;
318
+ }
319
+ catch (e) {
320
+ error.value = parseApiError(e);
321
+ return null;
322
+ }
323
+ finally {
324
+ loading.value = false;
325
+ }
326
+ };
327
+ const reset = () => {
328
+ loading.value = false;
329
+ error.value = null;
330
+ data.value = null;
331
+ };
332
+ if (options.immediate) {
333
+ execute();
334
+ }
335
+ return { loading, error, data, execute, reset };
336
+ }
337
+ /**
338
+ * Composable for cancellable API requests
339
+ */
340
+ export function useCancellableRequest(requestFn) {
341
+ const controller = ref(null);
342
+ const loading = ref(false);
343
+ const error = ref(null);
344
+ const data = ref(null);
345
+ const execute = async () => {
346
+ controller.value?.abort();
347
+ controller.value = new AbortController();
348
+ loading.value = true;
349
+ error.value = null;
350
+ try {
351
+ const result = await requestFn(controller.value.signal);
352
+ data.value = result;
353
+ return result;
354
+ }
355
+ catch (e) {
356
+ if (e.name !== 'AbortError' && e.name !== 'CanceledError') {
357
+ error.value = parseApiError(e);
358
+ }
359
+ return null;
360
+ }
361
+ finally {
362
+ loading.value = false;
363
+ }
364
+ };
365
+ const cancel = () => {
366
+ controller.value?.abort();
367
+ loading.value = false;
368
+ };
369
+ const reset = () => {
370
+ cancel();
371
+ error.value = null;
372
+ data.value = null;
373
+ };
374
+ return { loading, error, data, execute, cancel, reset };
375
+ }
376
+ /**
377
+ * Wrap API functions to return ApiResponse with data and metadata
378
+ */
379
+ export function wrapApiForDirectReturn(apiObj) {
380
+ if (typeof apiObj === 'function') {
381
+ return async (...args) => {
382
+ const result = await apiObj(...args);
383
+ if (result && typeof result === 'object' && 'data' in result && 'status' in result) {
384
+ return ApiResponse.create(result.data, result);
385
+ }
386
+ return result;
387
+ };
388
+ }
389
+ if (typeof apiObj === 'object' && apiObj !== null) {
390
+ const wrapped = {};
391
+ for (const key in apiObj) {
392
+ if (key === 'stream') {
393
+ wrapped[key] = apiObj[key];
394
+ }
395
+ else {
396
+ wrapped[key] = wrapApiForDirectReturn(apiObj[key]);
397
+ }
398
+ }
399
+ return wrapped;
400
+ }
401
+ return apiObj;
402
+ }
403
+ const axios = ax.create({
404
+ withCredentials: true,
405
+ });
406
+ class DataRequest {
407
+ data_table;
408
+ bagel;
409
+ itemID;
410
+ _filter = {};
411
+ constructor(table, bagel) {
412
+ this.data_table = table;
413
+ this.bagel = bagel;
414
+ this.itemID = '';
415
+ }
416
+ async post(item) {
417
+ if (!this.data_table) {
418
+ throw new Error('Data table not set');
419
+ }
420
+ const { data } = await axios.post(`/data/${this.data_table}`, item);
421
+ return data;
422
+ }
423
+ filter(filter) {
424
+ this._filter = filter;
425
+ return this;
426
+ }
427
+ async get() {
428
+ if (!this.data_table) {
429
+ throw new Error('Data table not set');
430
+ }
431
+ const filterStr = Object.keys(this._filter).length > 0
432
+ ? `?filter={${Object.entries(this._filter)
433
+ .map(([k, v]) => `${k}:${v}`)
434
+ .join(',')}}`
435
+ : '';
436
+ const url = `/data/${this.data_table}${this.itemID ? `/${this.itemID}` : ''}${filterStr}`;
437
+ try {
438
+ const { data } = await axios.get(url);
439
+ return data;
440
+ }
441
+ catch (err) {
442
+ console.log(err);
443
+ this.bagel.onError?.(err);
444
+ return undefined;
445
+ }
446
+ }
447
+ item(id) {
448
+ this.itemID = id;
449
+ return this;
450
+ }
451
+ async delete() {
452
+ if (!this.data_table) {
453
+ throw new Error('Data table not set');
454
+ }
455
+ const { data } = await axios.delete(`/data/${this.data_table}/${this.itemID}`);
456
+ return data;
457
+ }
458
+ async put(updatedItem) {
459
+ const { data_table, itemID } = this;
460
+ if (!data_table) {
461
+ throw new Error('Data table not set');
462
+ }
463
+ if (!itemID) {
464
+ throw new Error('Item ID not set');
465
+ }
466
+ const { data } = await axios.put(`/data/${data_table}/${itemID}`, updatedItem);
467
+ return data;
468
+ }
469
+ }
470
+ function responses(key) {
471
+ const res = {
472
+ LOGIN_BAD_CREDENTIALS: 'Invalid username or password',
473
+ RESET_PASSWORD_BAD_TOKEN: 'This reset password link is invalid or expired.',
474
+ };
475
+ return res[key] || key;
476
+ }
477
+ class BagelAuth {
478
+ bagel;
479
+ constructor(bagel) {
480
+ this.bagel = bagel;
481
+ }
482
+ user = undefined;
483
+ async validateUser() {
484
+ try {
485
+ const { data: usr } = await axios.get('/users/me', {
486
+ withCredentials: true,
487
+ });
488
+ this.user = usr;
489
+ return usr;
490
+ }
491
+ catch {
492
+ //
493
+ }
494
+ }
495
+ async resetPassword(ctx) {
496
+ return this.bagel.post('auth/reset-password', ctx).catch((err) => {
497
+ throw responses(err.response?.data?.detail);
498
+ });
499
+ }
500
+ async forgotPassword(email) {
501
+ try {
502
+ await this.bagel.post('auth/forgot-password', { email });
503
+ }
504
+ catch (err) {
505
+ throw responses(err?.response?.data?.detail);
506
+ }
507
+ }
508
+ async login(user) {
509
+ const formData = new FormData();
510
+ formData.append('username', user.email || '');
511
+ formData.append('password', user.password || '');
512
+ console.log('Logging in');
513
+ try {
514
+ await axios.post('/auth/cookie/login', formData, {
515
+ headers: {
516
+ 'withCredentials': true,
517
+ 'Content-Type': 'multipart/form-data',
518
+ },
519
+ });
520
+ axios.defaults.withCredentials = true;
521
+ return this.validateUser();
522
+ }
523
+ catch (err) {
524
+ throw responses(err.response?.data?.detail || 'LOGIN_BAD_CREDENTIALS');
525
+ }
526
+ }
527
+ async logout() {
528
+ try {
529
+ await axios.post('/auth/cookie/logout');
530
+ }
531
+ catch (err) {
532
+ this.bagel.onError?.(err);
533
+ console.log(err);
534
+ }
535
+ this.user = undefined;
536
+ }
537
+ async acceptInvite(token, user) {
538
+ await axios.post(`/auth/accept-invite/${token}`, user);
539
+ await this.login(user);
540
+ }
541
+ async register(user, errors) {
542
+ try {
543
+ await axios.post('/auth/register', user);
544
+ return this.login(user);
545
+ }
546
+ catch (err) {
547
+ console.error(err);
548
+ errors.email = err.response.data.detail;
549
+ throw err;
550
+ }
551
+ }
552
+ }
553
+ export class Bagel {
554
+ host;
555
+ fileBaseUrl;
556
+ fileVersion;
557
+ onError;
558
+ constructor({ host, fileBaseUrl, fileVersion, onError }) {
559
+ this.host = host?.replace(/\/$/, '');
560
+ this.fileBaseUrl = fileBaseUrl?.replace(/\/$/, '');
561
+ this.fileVersion = fileVersion || 'files'; // Default to 'files'
562
+ if (!this.host) {
563
+ return this;
564
+ }
565
+ axios.defaults.baseURL = this.host;
566
+ this.onError = onError;
567
+ }
568
+ read_table = undefined;
569
+ data(table) {
570
+ return new DataRequest(table, this);
571
+ }
572
+ auth = new BagelAuth(this);
573
+ _endpointCleaner(endpoint) {
574
+ const url = `${endpoint.replace(/^\//, '').replaceAll(/\/$/g, '')}`;
575
+ return url;
576
+ }
577
+ async api(endpoint, query) {
578
+ if (query) {
579
+ const queryParams = Object.entries(query)
580
+ .map(([key, value]) => `${key}=${value}`)
581
+ .join('&');
582
+ endpoint = `${endpoint}?${queryParams}`;
583
+ }
584
+ return axios
585
+ .get(`/api/${endpoint}`)
586
+ .then(({ data }) => data);
587
+ }
588
+ async get(endpoint, query) {
589
+ this._setAuthorization();
590
+ endpoint = this._endpointCleaner(endpoint);
591
+ if ((/undefined|null/).test(endpoint)) {
592
+ throw new Error(`Invalid endpoint: ${endpoint}`);
593
+ }
594
+ if (query) {
595
+ const queryParams = Object.entries(query)
596
+ .filter(([_, value]) => !!value)
597
+ .map(([key, value]) => `${key}=${value}`)
598
+ .join('&');
599
+ if (queryParams) {
600
+ endpoint = `${endpoint}?${queryParams}`;
601
+ }
602
+ }
603
+ const url = `/${endpoint}`;
604
+ return axios
605
+ .get(url)
606
+ .then(({ data }) => data)
607
+ .catch((err) => {
608
+ this.onError?.(err);
609
+ throw err;
610
+ });
611
+ }
612
+ async delete(endpoint) {
613
+ this._setAuthorization();
614
+ endpoint = this._endpointCleaner(endpoint);
615
+ return axios
616
+ .delete(`/${endpoint}`)
617
+ .then(({ data }) => data)
618
+ .catch((err) => {
619
+ this.onError?.(err);
620
+ throw err;
621
+ });
622
+ }
623
+ async put(endpoint, payload) {
624
+ endpoint = this._endpointCleaner(endpoint);
625
+ this._setAuthorization();
626
+ return axios
627
+ .put(`/${endpoint}`, payload)
628
+ .then(({ data }) => data)
629
+ .catch((err) => {
630
+ this.onError?.(err);
631
+ throw err;
632
+ });
633
+ }
634
+ async patch(endpoint, payload = {}) {
635
+ endpoint = this._endpointCleaner(endpoint);
636
+ return axios
637
+ .patch(`/${endpoint}`, payload)
638
+ .then(({ data }) => data)
639
+ .catch((err) => {
640
+ this.onError?.(err);
641
+ throw err;
642
+ });
643
+ }
644
+ _setAuthorization() {
645
+ const Authorization = localStorage.getItem('access_token');
646
+ if (Authorization) {
647
+ axios.defaults.headers.common.Authorization = `Bearer ${Authorization}`;
648
+ }
649
+ }
650
+ async post(endpoint, payload = {}) {
651
+ this._setAuthorization();
652
+ endpoint = this._endpointCleaner(endpoint);
653
+ return axios
654
+ .post(`/${endpoint}`, payload)
655
+ .then(({ data }) => data)
656
+ .catch((err) => {
657
+ this.onError?.(err);
658
+ throw err;
659
+ });
660
+ }
661
+ async uploadFile(file, options) {
662
+ this._setAuthorization();
663
+ const formData = new FormData();
664
+ formData.append('file', file);
665
+ let url = '/static_files/upload';
666
+ if (options?.topic) {
667
+ url = `/static_files/upload?topic=${options.topic}`;
668
+ }
669
+ // get an indication of the progress
670
+ const { data } = await axios.post(url, formData, {
671
+ headers: {
672
+ 'Content-Type': 'multipart/form-data',
673
+ },
674
+ onUploadProgress: options?.onUploadProgress,
675
+ });
676
+ return data;
677
+ }
678
+ }
679
+ export { formatAPIErrorMessage } from './utils';
680
+ /**
681
+ * Create a cached options getter for select inputs with auto-refresh
682
+ * @param fetchFn - Function that fetches the data
683
+ * @param mapFn - Optional mapper to transform items to { label, value }
684
+ * @returns Async function that returns cached options
685
+ * @example
686
+ * const getUsers = createOptionsGetter(
687
+ * () => api.users.list(),
688
+ * (user) => ({ label: user.name, value: user.id })
689
+ * )
690
+ */
691
+ export function createOptionsGetter(fetchFn, mapFn) {
692
+ let cache = [];
693
+ let lastFetchedAt = null;
694
+ const STALE_TIME = 5 * 60 * 1000; // 5 minutes
695
+ return async () => {
696
+ const now = Date.now();
697
+ const isStale = !lastFetchedAt || (now - lastFetchedAt) > STALE_TIME;
698
+ if (cache.length === 0 || isStale) {
699
+ const items = await fetchFn();
700
+ if (mapFn) {
701
+ cache = items.map(mapFn);
702
+ }
703
+ else {
704
+ // Auto-detect common properties
705
+ cache = items.map((item) => ({
706
+ ...item,
707
+ label: item.label ?? item.name ?? item.title ?? item.full_name ?? item.id,
708
+ value: item.value ?? item.id,
709
+ }));
710
+ }
711
+ lastFetchedAt = now;
712
+ }
713
+ return cache;
714
+ };
715
+ }