@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/package.json +1 -1
- package/src/index.js +715 -0
- package/src/openAPITools/StreamController.js +62 -0
- package/src/openAPITools/functionGenerator.js +979 -0
- package/src/openAPITools/index.js +40 -0
- package/src/openAPITools/moduleGenerator.js +125 -0
- package/src/openAPITools/streamClient.js +276 -0
- package/src/openAPITools/streamDetector.js +204 -0
- package/src/openAPITools/typeGenerator.js +48 -0
- package/src/openAPITools/types/index.js +19 -0
- package/src/openAPITools/types/utils.js +30 -0
- package/src/openAPITools/utils.js +282 -0
- package/src/unwrap.js +30 -0
- package/src/utils.js +58 -0
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
|
+
}
|