@enfyra/sdk-nuxt 0.2.2 → 0.2.3
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/README.md +134 -17
- package/dist/composables/useEnfyraApi.mjs +141 -14
- package/dist/index.d.ts +53 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/module.cjs +10 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -0
- package/package.json +1 -1
- package/src/composables/useEnfyraApi.ts +190 -32
- package/src/module.ts +14 -0
- package/src/types/index.ts +72 -6
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Nuxt SDK for Enfyra CMS - A powerful composable-based API client with full SSR s
|
|
|
7
7
|
✅ **SSR & Client-Side Support** - Automatic server-side rendering with `useFetch` or client-side with `$fetch`
|
|
8
8
|
✅ **Authentication Integration** - Built-in auth composables with automatic header forwarding
|
|
9
9
|
✅ **TypeScript Support** - Full type safety with auto-generated declarations
|
|
10
|
-
✅ **Batch Operations** - Efficient bulk operations
|
|
10
|
+
✅ **Batch Operations** - Efficient bulk operations with real-time progress tracking (client-side)
|
|
11
11
|
✅ **Error Handling** - Automatic error management with console logging
|
|
12
12
|
✅ **Reactive State** - Built-in loading, error, and data states
|
|
13
13
|
✅ **Caching Support** - Optional cache keys for SSR mode optimization
|
|
@@ -144,22 +144,34 @@ await execute({
|
|
|
144
144
|
- `headers?: Record<string, string>` - Custom headers
|
|
145
145
|
- `errorContext?: string` - Error context for logging
|
|
146
146
|
- `onError?: (error: ApiError, context?: string) => void` - Custom error handler
|
|
147
|
-
- `batchSize?: number` - Batch size for chunking large operations (default: no limit)
|
|
148
|
-
- `concurrent?: number` - Maximum concurrent requests (default: no limit)
|
|
149
147
|
- `key?: string` - Cache key (SSR mode, optional)
|
|
150
148
|
- `default?: () => T` - Default value (SSR mode only)
|
|
151
149
|
|
|
150
|
+
**Batch Options (only available for PATCH, DELETE, and POST methods):**
|
|
151
|
+
- `batchSize?: number` - Batch size for chunking large operations (default: no limit)
|
|
152
|
+
- `concurrent?: number` - Maximum concurrent requests (default: no limit)
|
|
153
|
+
- `onProgress?: (progress: BatchProgress) => void` - Real-time progress callback for batch operations
|
|
154
|
+
|
|
155
|
+
> 🎯 **TypeScript Smart:** Batch options (`batchSize`, `concurrent`, `onProgress`) are only available in TypeScript IntelliSense when using methods that support batch operations (PATCH, DELETE, POST). For GET and PUT methods, these options won't appear in autocomplete.
|
|
156
|
+
|
|
152
157
|
**⚠️ Important: Return Types Differ**
|
|
153
158
|
- **SSR Mode**: Returns `useFetch` result `{ data, pending, error, refresh }`
|
|
154
159
|
- **Client Mode**: Returns custom result `{ data, pending, error, execute }`
|
|
155
160
|
|
|
156
161
|
**Execute Options (Client mode only):**
|
|
162
|
+
|
|
163
|
+
**Basic Options:**
|
|
157
164
|
- `id?: string | number` - Single resource ID
|
|
165
|
+
- `body?: any` - Override request body
|
|
166
|
+
|
|
167
|
+
**Batch Options (only when using `ids` or `files`):**
|
|
158
168
|
- `ids?: (string | number)[]` - Batch operation IDs (PATCH/DELETE)
|
|
159
169
|
- `files?: FormData[]` - Array of FormData objects for batch upload (POST)
|
|
160
|
-
- `body?: any` - Override request body
|
|
161
170
|
- `batchSize?: number` - Override batch size for this execution
|
|
162
171
|
- `concurrent?: number` - Override concurrent limit for this execution
|
|
172
|
+
- `onProgress?: (progress: BatchProgress) => void` - Override progress callback for this execution
|
|
173
|
+
|
|
174
|
+
> 🎯 **TypeScript Smart:** Batch execute options (`batchSize`, `concurrent`, `onProgress`) are only available when you provide `ids` or `files` parameters, ensuring type safety.
|
|
163
175
|
|
|
164
176
|
### `useEnfyraAuth()`
|
|
165
177
|
|
|
@@ -184,6 +196,7 @@ await fetchUser() // Refresh user data
|
|
|
184
196
|
|
|
185
197
|
```typescript
|
|
186
198
|
// Basic batch delete - unlimited parallel requests
|
|
199
|
+
// 🎯 Note: Batch options only appear in IntelliSense for DELETE method
|
|
187
200
|
const { execute: deleteItems } = useEnfyraApi('/items', {
|
|
188
201
|
method: 'delete',
|
|
189
202
|
errorContext: 'Delete Items'
|
|
@@ -191,38 +204,82 @@ const { execute: deleteItems } = useEnfyraApi('/items', {
|
|
|
191
204
|
|
|
192
205
|
await deleteItems({ ids: ['1', '2', '3'] });
|
|
193
206
|
|
|
194
|
-
// Advanced batch operations with concurrency control
|
|
207
|
+
// Advanced batch operations with concurrency control
|
|
208
|
+
// 🎯 TypeScript shows batch options (batchSize, concurrent, onProgress) for DELETE method
|
|
195
209
|
const { execute: deleteMany } = useEnfyraApi('/users', {
|
|
196
210
|
method: 'delete',
|
|
197
|
-
batchSize: 10, //
|
|
198
|
-
concurrent: 3, //
|
|
211
|
+
batchSize: 10, // ✅ Available: DELETE method supports batching
|
|
212
|
+
concurrent: 3, // ✅ Available: DELETE method supports batching
|
|
213
|
+
onProgress: (progress) => { // ✅ Available: DELETE method supports batching
|
|
214
|
+
console.log(`Deleting: ${progress.completed}/${progress.total}`);
|
|
215
|
+
},
|
|
199
216
|
onError: (error, context) => toast.error(`${context}: ${error.message}`)
|
|
200
217
|
});
|
|
201
218
|
|
|
219
|
+
// GET method example - batch options NOT available
|
|
220
|
+
// 🎯 TypeScript won't show batch options for GET method
|
|
221
|
+
const { execute: getUsers } = useEnfyraApi('/users', {
|
|
222
|
+
method: 'get',
|
|
223
|
+
// batchSize: 10, // ❌ Not available: GET doesn't support batching
|
|
224
|
+
// concurrent: 3, // ❌ Not available: GET doesn't support batching
|
|
225
|
+
// onProgress: () => {} // ❌ Not available: GET doesn't support batching
|
|
226
|
+
errorContext: 'Fetch Users'
|
|
227
|
+
});
|
|
228
|
+
|
|
202
229
|
// Delete 100 users in controlled batches
|
|
203
230
|
await deleteMany({ ids: Array.from({length: 100}, (_, i) => `user-${i}`) });
|
|
204
231
|
|
|
205
232
|
// Override batch settings per execution
|
|
233
|
+
// 🎯 TypeScript shows batch options for PATCH method
|
|
206
234
|
const { execute: updateUsers } = useEnfyraApi('/users', {
|
|
207
235
|
method: 'patch',
|
|
208
|
-
batchSize: 20, //
|
|
209
|
-
concurrent: 5 //
|
|
236
|
+
batchSize: 20, // ✅ Available: PATCH method supports batching
|
|
237
|
+
concurrent: 5 // ✅ Available: PATCH method supports batching
|
|
210
238
|
});
|
|
211
239
|
|
|
212
240
|
// This execution uses different settings
|
|
241
|
+
// 🎯 Batch options in execute() only available when using `ids` or `files`
|
|
213
242
|
await updateUsers({
|
|
214
|
-
ids: largeUserList,
|
|
243
|
+
ids: largeUserList, // ✅ Triggers batch mode
|
|
215
244
|
body: { status: 'active' },
|
|
216
|
-
batchSize: 50, //
|
|
217
|
-
concurrent: 10
|
|
245
|
+
batchSize: 50, // ✅ Available: Using `ids` parameter
|
|
246
|
+
concurrent: 10, // ✅ Available: Using `ids` parameter
|
|
247
|
+
onProgress: (progress) => { // ✅ Available: Using `ids` parameter
|
|
248
|
+
console.log(`Updating: ${progress.completed}/${progress.total}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Single operation - batch options NOT available in execute
|
|
253
|
+
await updateUsers({
|
|
254
|
+
id: 'single-user-id', // ❌ Single operation, no batch options
|
|
255
|
+
body: { status: 'active' }
|
|
256
|
+
// batchSize: 50, // ❌ Not available: Not using `ids` or `files`
|
|
257
|
+
// concurrent: 10, // ❌ Not available: Not using `ids` or `files`
|
|
258
|
+
// onProgress: () => {} // ❌ Not available: Not using `ids` or `files`
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Batch file upload with real-time progress tracking
|
|
262
|
+
const progressState = ref({
|
|
263
|
+
progress: 0,
|
|
264
|
+
completed: 0,
|
|
265
|
+
total: 0,
|
|
266
|
+
failed: 0,
|
|
267
|
+
estimatedTimeRemaining: 0,
|
|
268
|
+
operationsPerSecond: 0
|
|
218
269
|
});
|
|
219
270
|
|
|
220
|
-
//
|
|
271
|
+
// 🎯 TypeScript shows batch options for POST method (supports file uploads)
|
|
221
272
|
const { execute: uploadFiles } = useEnfyraApi('/file_definition', {
|
|
222
273
|
method: 'post',
|
|
223
|
-
batchSize: 5, //
|
|
224
|
-
concurrent: 2, //
|
|
225
|
-
errorContext: 'Upload Files'
|
|
274
|
+
batchSize: 5, // ✅ Available: POST method supports batching for files
|
|
275
|
+
concurrent: 2, // ✅ Available: POST method supports batching for files
|
|
276
|
+
errorContext: 'Upload Files',
|
|
277
|
+
onProgress: (progress) => { // ✅ Available: POST method supports batching
|
|
278
|
+
progressState.value = progress;
|
|
279
|
+
console.log(`Progress: ${progress.progress}% (${progress.completed}/${progress.total})`);
|
|
280
|
+
console.log(`ETA: ${Math.round((progress.estimatedTimeRemaining || 0) / 1000)}s`);
|
|
281
|
+
console.log(`Speed: ${progress.operationsPerSecond?.toFixed(1)} ops/sec`);
|
|
282
|
+
}
|
|
226
283
|
});
|
|
227
284
|
|
|
228
285
|
// Convert files to FormData array (matches enfyra_app pattern)
|
|
@@ -236,6 +293,66 @@ const formDataArray = selectedFiles.map(file => {
|
|
|
236
293
|
await uploadFiles({
|
|
237
294
|
files: formDataArray // Array of FormData objects
|
|
238
295
|
});
|
|
296
|
+
|
|
297
|
+
// Real-time progress tracking with detailed results
|
|
298
|
+
const { execute: processData } = useEnfyraApi('/process', {
|
|
299
|
+
method: 'post',
|
|
300
|
+
batchSize: 10,
|
|
301
|
+
concurrent: 3,
|
|
302
|
+
onProgress: (progress) => {
|
|
303
|
+
// Display progress bar
|
|
304
|
+
updateProgressBar(progress.progress);
|
|
305
|
+
|
|
306
|
+
// Show detailed metrics
|
|
307
|
+
console.log('Batch Progress:', {
|
|
308
|
+
percentage: progress.progress,
|
|
309
|
+
completed: progress.completed,
|
|
310
|
+
total: progress.total,
|
|
311
|
+
failed: progress.failed,
|
|
312
|
+
currentBatch: progress.currentBatch,
|
|
313
|
+
totalBatches: progress.totalBatches,
|
|
314
|
+
averageTime: progress.averageTime,
|
|
315
|
+
estimatedTimeRemaining: progress.estimatedTimeRemaining,
|
|
316
|
+
operationsPerSecond: progress.operationsPerSecond
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Handle individual results
|
|
320
|
+
progress.results.forEach(result => {
|
|
321
|
+
if (result.status === 'failed') {
|
|
322
|
+
console.error(`Item ${result.index} failed:`, result.error);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await processData({
|
|
329
|
+
ids: largeDataSet,
|
|
330
|
+
body: processingOptions
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Real-time Progress Interface
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
interface BatchProgress {
|
|
338
|
+
progress: number; // 0-100 percentage
|
|
339
|
+
completed: number; // Number of completed operations
|
|
340
|
+
total: number; // Total number of operations
|
|
341
|
+
failed: number; // Number of failed operations
|
|
342
|
+
inProgress: number; // Operations currently running
|
|
343
|
+
estimatedTimeRemaining?: number; // Milliseconds remaining
|
|
344
|
+
averageTime?: number; // Average time per operation (ms)
|
|
345
|
+
currentBatch: number; // Current batch being processed
|
|
346
|
+
totalBatches: number; // Total number of batches
|
|
347
|
+
operationsPerSecond?: number; // Processing speed
|
|
348
|
+
results: Array<{ // Detailed results
|
|
349
|
+
index: number;
|
|
350
|
+
status: 'completed' | 'failed';
|
|
351
|
+
result?: any;
|
|
352
|
+
error?: ApiError;
|
|
353
|
+
duration?: number;
|
|
354
|
+
}>;
|
|
355
|
+
}
|
|
239
356
|
```
|
|
240
357
|
|
|
241
358
|
### TypeScript Integration
|
|
@@ -310,7 +427,7 @@ export default defineNuxtConfig({
|
|
|
310
427
|
// Required: Main API URL
|
|
311
428
|
apiUrl: process.env.ENFYRA_API_URL || "http://localhost:1105",
|
|
312
429
|
|
|
313
|
-
// Optional: API path prefix (default: '')
|
|
430
|
+
// Optional: API path prefix (default: '/api')
|
|
314
431
|
apiPrefix: '/api/v1',
|
|
315
432
|
|
|
316
433
|
// Required: App URL for SSR requests
|
|
@@ -16,7 +16,7 @@ function handleError(error, context, customHandler) {
|
|
|
16
16
|
return apiError;
|
|
17
17
|
}
|
|
18
18
|
export function useEnfyraApi(path, opts = {}) {
|
|
19
|
-
const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent } = opts;
|
|
19
|
+
const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent, onProgress } = opts;
|
|
20
20
|
if (ssr) {
|
|
21
21
|
const config = useRuntimeConfig().public.enfyraSDK;
|
|
22
22
|
const basePath = (typeof path === "function" ? path() : path).replace(/^\/?api\/?/, "").replace(/^\/+/, "");
|
|
@@ -65,38 +65,165 @@ export function useEnfyraApi(path, opts = {}) {
|
|
|
65
65
|
const basePath = (typeof path === "function" ? path() : path).replace(/^\/?api\/?/, "").replace(/^\/+/, "");
|
|
66
66
|
const finalBody = executeOpts?.body || unref(body);
|
|
67
67
|
const finalQuery = unref(query);
|
|
68
|
-
const
|
|
69
|
-
const
|
|
68
|
+
const isBatchOperation = !opts.disableBatch && (executeOpts?.ids && executeOpts.ids.length > 0 && (method.toLowerCase() === "patch" || method.toLowerCase() === "delete") || method.toLowerCase() === "post" && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0);
|
|
69
|
+
const effectiveBatchSize = isBatchOperation ? executeOpts?.batchSize ?? batchSize : void 0;
|
|
70
|
+
const effectiveConcurrent = isBatchOperation ? executeOpts?.concurrent ?? concurrent : void 0;
|
|
71
|
+
const effectiveOnProgress = isBatchOperation ? executeOpts?.onProgress ?? onProgress : void 0;
|
|
70
72
|
const buildPath = (...segments) => {
|
|
71
73
|
return segments.filter(Boolean).join("/");
|
|
72
74
|
};
|
|
73
75
|
const fullBaseURL = apiUrl + (apiPrefix || "");
|
|
74
76
|
async function processBatch(items, processor) {
|
|
75
77
|
const results = [];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const progressResults = [];
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
let completed = 0;
|
|
81
|
+
let failed = 0;
|
|
80
82
|
const chunks = effectiveBatchSize ? Array.from(
|
|
81
83
|
{ length: Math.ceil(items.length / effectiveBatchSize) },
|
|
82
84
|
(_, i) => items.slice(i * effectiveBatchSize, i * effectiveBatchSize + effectiveBatchSize)
|
|
83
85
|
) : [items];
|
|
84
|
-
|
|
86
|
+
const totalBatches = chunks.length;
|
|
87
|
+
let currentBatch = 0;
|
|
88
|
+
const updateProgress = (inProgress = 0) => {
|
|
89
|
+
if (effectiveOnProgress) {
|
|
90
|
+
const elapsed = Date.now() - startTime;
|
|
91
|
+
const progress = items.length > 0 ? Math.round(completed / items.length * 100) : 0;
|
|
92
|
+
const averageTime = completed > 0 ? elapsed / completed : void 0;
|
|
93
|
+
const operationsPerSecond = completed > 0 ? completed / elapsed * 1e3 : void 0;
|
|
94
|
+
const estimatedTimeRemaining = averageTime && items.length > completed ? Math.round(averageTime * (items.length - completed)) : void 0;
|
|
95
|
+
const progressData = {
|
|
96
|
+
progress,
|
|
97
|
+
completed,
|
|
98
|
+
total: items.length,
|
|
99
|
+
failed,
|
|
100
|
+
inProgress,
|
|
101
|
+
estimatedTimeRemaining,
|
|
102
|
+
averageTime,
|
|
103
|
+
currentBatch: currentBatch + 1,
|
|
104
|
+
totalBatches,
|
|
105
|
+
operationsPerSecond,
|
|
106
|
+
results: [...progressResults]
|
|
107
|
+
};
|
|
108
|
+
effectiveOnProgress(progressData);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
updateProgress(0);
|
|
112
|
+
if (!effectiveBatchSize && !effectiveConcurrent) {
|
|
113
|
+
updateProgress(items.length);
|
|
114
|
+
const promises = items.map(async (item, index) => {
|
|
115
|
+
const itemStartTime = Date.now();
|
|
116
|
+
try {
|
|
117
|
+
const result = await processor(item, index);
|
|
118
|
+
const duration = Date.now() - itemStartTime;
|
|
119
|
+
completed++;
|
|
120
|
+
progressResults.push({
|
|
121
|
+
index,
|
|
122
|
+
status: "completed",
|
|
123
|
+
result,
|
|
124
|
+
duration
|
|
125
|
+
});
|
|
126
|
+
updateProgress(items.length - completed);
|
|
127
|
+
return result;
|
|
128
|
+
} catch (error2) {
|
|
129
|
+
const duration = Date.now() - itemStartTime;
|
|
130
|
+
failed++;
|
|
131
|
+
completed++;
|
|
132
|
+
progressResults.push({
|
|
133
|
+
index,
|
|
134
|
+
status: "failed",
|
|
135
|
+
error: error2,
|
|
136
|
+
duration
|
|
137
|
+
});
|
|
138
|
+
updateProgress(items.length - completed);
|
|
139
|
+
throw error2;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const results2 = await Promise.all(promises);
|
|
143
|
+
updateProgress(0);
|
|
144
|
+
return results2;
|
|
145
|
+
}
|
|
146
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
147
|
+
currentBatch = chunkIndex;
|
|
148
|
+
const chunk = chunks[chunkIndex];
|
|
85
149
|
if (effectiveConcurrent && chunk.length > effectiveConcurrent) {
|
|
86
150
|
for (let i = 0; i < chunk.length; i += effectiveConcurrent) {
|
|
87
151
|
const batch = chunk.slice(i, i + effectiveConcurrent);
|
|
88
|
-
const
|
|
152
|
+
const baseIndex = chunkIndex * (effectiveBatchSize || items.length) + i;
|
|
153
|
+
updateProgress(batch.length);
|
|
154
|
+
const batchPromises = batch.map(async (item, batchItemIndex) => {
|
|
155
|
+
const globalIndex = baseIndex + batchItemIndex;
|
|
156
|
+
const itemStartTime = Date.now();
|
|
157
|
+
try {
|
|
158
|
+
const result = await processor(item, globalIndex);
|
|
159
|
+
const duration = Date.now() - itemStartTime;
|
|
160
|
+
completed++;
|
|
161
|
+
progressResults.push({
|
|
162
|
+
index: globalIndex,
|
|
163
|
+
status: "completed",
|
|
164
|
+
result,
|
|
165
|
+
duration
|
|
166
|
+
});
|
|
167
|
+
updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
|
|
168
|
+
return result;
|
|
169
|
+
} catch (error2) {
|
|
170
|
+
const duration = Date.now() - itemStartTime;
|
|
171
|
+
failed++;
|
|
172
|
+
completed++;
|
|
173
|
+
progressResults.push({
|
|
174
|
+
index: globalIndex,
|
|
175
|
+
status: "failed",
|
|
176
|
+
error: error2,
|
|
177
|
+
duration
|
|
178
|
+
});
|
|
179
|
+
updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
|
|
180
|
+
throw error2;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const batchResults = await Promise.all(batchPromises);
|
|
89
184
|
results.push(...batchResults);
|
|
90
185
|
}
|
|
91
186
|
} else {
|
|
92
|
-
const
|
|
187
|
+
const baseIndex = chunkIndex * (effectiveBatchSize || items.length);
|
|
188
|
+
updateProgress(chunk.length);
|
|
189
|
+
const chunkPromises = chunk.map(async (item, chunkItemIndex) => {
|
|
190
|
+
const globalIndex = baseIndex + chunkItemIndex;
|
|
191
|
+
const itemStartTime = Date.now();
|
|
192
|
+
try {
|
|
193
|
+
const result = await processor(item, globalIndex);
|
|
194
|
+
const duration = Date.now() - itemStartTime;
|
|
195
|
+
completed++;
|
|
196
|
+
progressResults.push({
|
|
197
|
+
index: globalIndex,
|
|
198
|
+
status: "completed",
|
|
199
|
+
result,
|
|
200
|
+
duration
|
|
201
|
+
});
|
|
202
|
+
updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
|
|
203
|
+
return result;
|
|
204
|
+
} catch (error2) {
|
|
205
|
+
const duration = Date.now() - itemStartTime;
|
|
206
|
+
failed++;
|
|
207
|
+
completed++;
|
|
208
|
+
progressResults.push({
|
|
209
|
+
index: globalIndex,
|
|
210
|
+
status: "failed",
|
|
211
|
+
error: error2,
|
|
212
|
+
duration
|
|
213
|
+
});
|
|
214
|
+
updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
|
|
215
|
+
throw error2;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
93
219
|
results.push(...chunkResults);
|
|
94
220
|
}
|
|
95
221
|
}
|
|
222
|
+
updateProgress(0);
|
|
96
223
|
return results;
|
|
97
224
|
}
|
|
98
|
-
if (
|
|
99
|
-
const responses = await processBatch(executeOpts.ids, async (id) => {
|
|
225
|
+
if (isBatchOperation && executeOpts?.ids && executeOpts.ids.length > 0) {
|
|
226
|
+
const responses = await processBatch(executeOpts.ids, async (id, index) => {
|
|
100
227
|
const finalPath2 = buildPath(basePath, id);
|
|
101
228
|
return $fetch(finalPath2, {
|
|
102
229
|
baseURL: fullBaseURL,
|
|
@@ -109,8 +236,8 @@ export function useEnfyraApi(path, opts = {}) {
|
|
|
109
236
|
data.value = responses;
|
|
110
237
|
return responses;
|
|
111
238
|
}
|
|
112
|
-
if (
|
|
113
|
-
const responses = await processBatch(executeOpts.files, async (fileObj) => {
|
|
239
|
+
if (isBatchOperation && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0) {
|
|
240
|
+
const responses = await processBatch(executeOpts.files, async (fileObj, index) => {
|
|
114
241
|
return $fetch(basePath, {
|
|
115
242
|
baseURL: fullBaseURL,
|
|
116
243
|
method,
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,37 @@ export interface ApiError {
|
|
|
9
9
|
data?: any;
|
|
10
10
|
response?: any;
|
|
11
11
|
}
|
|
12
|
-
export interface
|
|
12
|
+
export interface BatchProgress {
|
|
13
|
+
/** Current progress percentage (0-100) */
|
|
14
|
+
progress: number;
|
|
15
|
+
/** Number of completed operations */
|
|
16
|
+
completed: number;
|
|
17
|
+
/** Total number of operations */
|
|
18
|
+
total: number;
|
|
19
|
+
/** Number of failed operations */
|
|
20
|
+
failed: number;
|
|
21
|
+
/** Number of operations currently in progress */
|
|
22
|
+
inProgress: number;
|
|
23
|
+
/** Estimated time remaining in milliseconds */
|
|
24
|
+
estimatedTimeRemaining?: number;
|
|
25
|
+
/** Average time per operation in milliseconds */
|
|
26
|
+
averageTime?: number;
|
|
27
|
+
/** Current batch being processed */
|
|
28
|
+
currentBatch: number;
|
|
29
|
+
/** Total number of batches */
|
|
30
|
+
totalBatches: number;
|
|
31
|
+
/** Processing speed (operations per second) */
|
|
32
|
+
operationsPerSecond?: number;
|
|
33
|
+
/** Detailed results array for completed operations */
|
|
34
|
+
results: Array<{
|
|
35
|
+
index: number;
|
|
36
|
+
status: 'completed' | 'failed';
|
|
37
|
+
result?: any;
|
|
38
|
+
error?: ApiError;
|
|
39
|
+
duration?: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
interface BaseApiOptions<T> {
|
|
13
43
|
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
14
44
|
body?: any;
|
|
15
45
|
query?: Record<string, any>;
|
|
@@ -17,16 +47,28 @@ export interface ApiOptions<T> {
|
|
|
17
47
|
errorContext?: string;
|
|
18
48
|
onError?: (error: ApiError, context?: string) => void;
|
|
19
49
|
disableBatch?: boolean;
|
|
20
|
-
/** Batch size for chunking large operations (default: no limit) */
|
|
21
|
-
batchSize?: number;
|
|
22
|
-
/** Maximum concurrent requests (default: no limit) */
|
|
23
|
-
concurrent?: number;
|
|
24
50
|
default?: () => T;
|
|
25
51
|
/** Enable SSR with useFetch instead of $fetch */
|
|
26
52
|
ssr?: boolean;
|
|
27
53
|
/** Unique key for useFetch caching */
|
|
28
54
|
key?: string;
|
|
29
55
|
}
|
|
56
|
+
interface BatchApiOptions {
|
|
57
|
+
/** Batch size for chunking large operations (default: no limit) - Only available for batch operations */
|
|
58
|
+
batchSize?: number;
|
|
59
|
+
/** Maximum concurrent requests (default: no limit) - Only available for batch operations */
|
|
60
|
+
concurrent?: number;
|
|
61
|
+
/** Real-time progress callback for batch operations - Only available for batch operations */
|
|
62
|
+
onProgress?: (progress: BatchProgress) => void;
|
|
63
|
+
}
|
|
64
|
+
type ConditionalBatchOptions<T> = T extends {
|
|
65
|
+
method?: 'patch' | 'delete' | 'PATCH' | 'DELETE';
|
|
66
|
+
} ? BatchApiOptions : T extends {
|
|
67
|
+
method?: 'post' | 'POST';
|
|
68
|
+
} ? BatchApiOptions : T extends {
|
|
69
|
+
method?: undefined;
|
|
70
|
+
} ? Partial<BatchApiOptions> : {};
|
|
71
|
+
export type ApiOptions<T> = BaseApiOptions<T> & ConditionalBatchOptions<BaseApiOptions<T>>;
|
|
30
72
|
export interface BackendError {
|
|
31
73
|
success: false;
|
|
32
74
|
message: string;
|
|
@@ -50,9 +92,11 @@ export interface UseEnfyraApiSSRReturn<T> extends AsyncData<T | null, ApiError>
|
|
|
50
92
|
error: Ref<ApiError | null>;
|
|
51
93
|
refresh: () => Promise<void>;
|
|
52
94
|
}
|
|
53
|
-
|
|
95
|
+
interface BaseExecuteOptions {
|
|
54
96
|
body?: any;
|
|
55
97
|
id?: string | number;
|
|
98
|
+
}
|
|
99
|
+
interface BatchExecuteOptions {
|
|
56
100
|
ids?: (string | number)[];
|
|
57
101
|
/** Array of FormData objects for batch upload */
|
|
58
102
|
files?: FormData[];
|
|
@@ -60,7 +104,10 @@ export interface ExecuteOptions {
|
|
|
60
104
|
batchSize?: number;
|
|
61
105
|
/** Override concurrent limit for this specific execution */
|
|
62
106
|
concurrent?: number;
|
|
107
|
+
/** Override progress callback for this specific execution */
|
|
108
|
+
onProgress?: (progress: BatchProgress) => void;
|
|
63
109
|
}
|
|
110
|
+
export type ExecuteOptions = BaseExecuteOptions & BatchExecuteOptions;
|
|
64
111
|
export interface UseEnfyraApiClientReturn<T> {
|
|
65
112
|
data: Ref<T | null>;
|
|
66
113
|
error: Ref<ApiError | null>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/dist/module.cjs
CHANGED
|
@@ -15,6 +15,16 @@ const module$1 = kit.defineNuxtModule({
|
|
|
15
15
|
},
|
|
16
16
|
setup(options, nuxt) {
|
|
17
17
|
const { resolve } = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('module.cjs', document.baseURI).href)));
|
|
18
|
+
if (!options.apiUrl || !options.appUrl) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`[Enfyra SDK Nuxt] Missing required configuration:
|
|
21
|
+
${!options.apiUrl ? "- apiUrl is required\n" : ""}${!options.appUrl ? "- appUrl is required\n" : ""}Please configure both in your nuxt.config.ts:
|
|
22
|
+
enfyraSDK: {
|
|
23
|
+
apiUrl: 'https://your-api-url',
|
|
24
|
+
appUrl: 'https://your-app-url'
|
|
25
|
+
}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
18
28
|
nuxt.options.runtimeConfig.public.enfyraSDK = {
|
|
19
29
|
...nuxt.options.runtimeConfig.public.enfyraSDK,
|
|
20
30
|
...options
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -12,6 +12,16 @@ const module = defineNuxtModule({
|
|
|
12
12
|
},
|
|
13
13
|
setup(options, nuxt) {
|
|
14
14
|
const { resolve } = createResolver(import.meta.url);
|
|
15
|
+
if (!options.apiUrl || !options.appUrl) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[Enfyra SDK Nuxt] Missing required configuration:
|
|
18
|
+
${!options.apiUrl ? "- apiUrl is required\n" : ""}${!options.appUrl ? "- appUrl is required\n" : ""}Please configure both in your nuxt.config.ts:
|
|
19
|
+
enfyraSDK: {
|
|
20
|
+
apiUrl: 'https://your-api-url',
|
|
21
|
+
appUrl: 'https://your-app-url'
|
|
22
|
+
}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
15
25
|
nuxt.options.runtimeConfig.public.enfyraSDK = {
|
|
16
26
|
...nuxt.options.runtimeConfig.public.enfyraSDK,
|
|
17
27
|
...options
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ExecuteOptions,
|
|
7
7
|
UseEnfyraApiSSRReturn,
|
|
8
8
|
UseEnfyraApiClientReturn,
|
|
9
|
+
BatchProgress,
|
|
9
10
|
} from "../types";
|
|
10
11
|
import { $fetch } from "../utils/http";
|
|
11
12
|
import { useRuntimeConfig, useFetch, useRequestHeaders } from "#imports";
|
|
@@ -47,7 +48,7 @@ export function useEnfyraApi<T = any>(
|
|
|
47
48
|
path: (() => string) | string,
|
|
48
49
|
opts: ApiOptions<T> = {}
|
|
49
50
|
): UseEnfyraApiSSRReturn<T> | UseEnfyraApiClientReturn<T> {
|
|
50
|
-
const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent } = opts;
|
|
51
|
+
const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent, onProgress } = opts;
|
|
51
52
|
|
|
52
53
|
// SSR mode - use useFetch
|
|
53
54
|
if (ssr) {
|
|
@@ -117,9 +118,19 @@ export function useEnfyraApi<T = any>(
|
|
|
117
118
|
const finalBody = executeOpts?.body || unref(body);
|
|
118
119
|
const finalQuery = unref(query);
|
|
119
120
|
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
|
|
121
|
+
// Check if this is actually a batch operation
|
|
122
|
+
const isBatchOperation = (
|
|
123
|
+
!opts.disableBatch &&
|
|
124
|
+
(
|
|
125
|
+
(executeOpts?.ids && executeOpts.ids.length > 0 && (method.toLowerCase() === "patch" || method.toLowerCase() === "delete")) ||
|
|
126
|
+
(method.toLowerCase() === "post" && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0)
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Use batch options only if this is actually a batch operation
|
|
131
|
+
const effectiveBatchSize = isBatchOperation ? (executeOpts?.batchSize ?? batchSize) : undefined;
|
|
132
|
+
const effectiveConcurrent = isBatchOperation ? (executeOpts?.concurrent ?? concurrent) : undefined;
|
|
133
|
+
const effectiveOnProgress = isBatchOperation ? (executeOpts?.onProgress ?? onProgress) : undefined;
|
|
123
134
|
|
|
124
135
|
// Helper function to build clean path
|
|
125
136
|
const buildPath = (...segments: (string | number)[]): string => {
|
|
@@ -129,52 +140,205 @@ export function useEnfyraApi<T = any>(
|
|
|
129
140
|
// Build full base URL with prefix
|
|
130
141
|
const fullBaseURL = apiUrl + (apiPrefix || "");
|
|
131
142
|
|
|
132
|
-
// Helper function for batch processing with chunking and
|
|
143
|
+
// Helper function for batch processing with chunking, concurrency control, and real-time progress tracking
|
|
133
144
|
async function processBatch<T>(
|
|
134
145
|
items: any[],
|
|
135
|
-
processor: (item: any) => Promise<T>
|
|
146
|
+
processor: (item: any, index: number) => Promise<T>
|
|
136
147
|
): Promise<T[]> {
|
|
137
148
|
const results: T[] = [];
|
|
149
|
+
const progressResults: BatchProgress['results'] = [];
|
|
150
|
+
const startTime = Date.now();
|
|
151
|
+
let completed = 0;
|
|
152
|
+
let failed = 0;
|
|
138
153
|
|
|
139
|
-
//
|
|
140
|
-
if (!effectiveBatchSize && !effectiveConcurrent) {
|
|
141
|
-
const promises = items.map(processor);
|
|
142
|
-
return await Promise.all(promises);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Chunk items by batchSize
|
|
154
|
+
// Calculate batch structure
|
|
146
155
|
const chunks = effectiveBatchSize ?
|
|
147
156
|
Array.from({ length: Math.ceil(items.length / effectiveBatchSize) }, (_, i) =>
|
|
148
157
|
items.slice(i * effectiveBatchSize, i * effectiveBatchSize + effectiveBatchSize)
|
|
149
158
|
) : [items];
|
|
159
|
+
|
|
160
|
+
const totalBatches = chunks.length;
|
|
161
|
+
let currentBatch = 0;
|
|
162
|
+
|
|
163
|
+
// Initialize progress tracking
|
|
164
|
+
const updateProgress = (inProgress: number = 0) => {
|
|
165
|
+
if (effectiveOnProgress) {
|
|
166
|
+
const elapsed = Date.now() - startTime;
|
|
167
|
+
const progress = items.length > 0 ? Math.round((completed / items.length) * 100) : 0;
|
|
168
|
+
const averageTime = completed > 0 ? elapsed / completed : undefined;
|
|
169
|
+
const operationsPerSecond = completed > 0 ? (completed / elapsed) * 1000 : undefined;
|
|
170
|
+
const estimatedTimeRemaining = averageTime && items.length > completed
|
|
171
|
+
? Math.round(averageTime * (items.length - completed))
|
|
172
|
+
: undefined;
|
|
173
|
+
|
|
174
|
+
const progressData: BatchProgress = {
|
|
175
|
+
progress,
|
|
176
|
+
completed,
|
|
177
|
+
total: items.length,
|
|
178
|
+
failed,
|
|
179
|
+
inProgress,
|
|
180
|
+
estimatedTimeRemaining,
|
|
181
|
+
averageTime,
|
|
182
|
+
currentBatch: currentBatch + 1,
|
|
183
|
+
totalBatches,
|
|
184
|
+
operationsPerSecond,
|
|
185
|
+
results: [...progressResults]
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
effectiveOnProgress(progressData);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Initial progress update
|
|
193
|
+
updateProgress(0);
|
|
194
|
+
|
|
195
|
+
// If no limits, process all at once with progress tracking
|
|
196
|
+
if (!effectiveBatchSize && !effectiveConcurrent) {
|
|
197
|
+
updateProgress(items.length);
|
|
198
|
+
|
|
199
|
+
const promises = items.map(async (item, index) => {
|
|
200
|
+
const itemStartTime = Date.now();
|
|
201
|
+
try {
|
|
202
|
+
const result = await processor(item, index);
|
|
203
|
+
const duration = Date.now() - itemStartTime;
|
|
204
|
+
|
|
205
|
+
completed++;
|
|
206
|
+
progressResults.push({
|
|
207
|
+
index,
|
|
208
|
+
status: 'completed',
|
|
209
|
+
result,
|
|
210
|
+
duration
|
|
211
|
+
});
|
|
212
|
+
updateProgress(items.length - completed);
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const duration = Date.now() - itemStartTime;
|
|
217
|
+
failed++;
|
|
218
|
+
completed++;
|
|
219
|
+
|
|
220
|
+
progressResults.push({
|
|
221
|
+
index,
|
|
222
|
+
status: 'failed',
|
|
223
|
+
error: error as ApiError,
|
|
224
|
+
duration
|
|
225
|
+
});
|
|
226
|
+
updateProgress(items.length - completed);
|
|
227
|
+
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const results = await Promise.all(promises);
|
|
233
|
+
updateProgress(0);
|
|
234
|
+
return results;
|
|
235
|
+
}
|
|
150
236
|
|
|
151
|
-
// Process each chunk
|
|
152
|
-
for (
|
|
237
|
+
// Process each chunk with progress tracking
|
|
238
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
239
|
+
currentBatch = chunkIndex;
|
|
240
|
+
const chunk = chunks[chunkIndex];
|
|
241
|
+
|
|
153
242
|
if (effectiveConcurrent && chunk.length > effectiveConcurrent) {
|
|
154
243
|
// Process chunk with concurrency limit
|
|
155
244
|
for (let i = 0; i < chunk.length; i += effectiveConcurrent) {
|
|
156
245
|
const batch = chunk.slice(i, i + effectiveConcurrent);
|
|
157
|
-
const
|
|
246
|
+
const baseIndex = chunkIndex * (effectiveBatchSize || items.length) + i;
|
|
247
|
+
|
|
248
|
+
updateProgress(batch.length);
|
|
249
|
+
|
|
250
|
+
const batchPromises = batch.map(async (item, batchItemIndex) => {
|
|
251
|
+
const globalIndex = baseIndex + batchItemIndex;
|
|
252
|
+
const itemStartTime = Date.now();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const result = await processor(item, globalIndex);
|
|
256
|
+
const duration = Date.now() - itemStartTime;
|
|
257
|
+
|
|
258
|
+
completed++;
|
|
259
|
+
progressResults.push({
|
|
260
|
+
index: globalIndex,
|
|
261
|
+
status: 'completed',
|
|
262
|
+
result,
|
|
263
|
+
duration
|
|
264
|
+
});
|
|
265
|
+
updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const duration = Date.now() - itemStartTime;
|
|
270
|
+
failed++;
|
|
271
|
+
completed++;
|
|
272
|
+
|
|
273
|
+
progressResults.push({
|
|
274
|
+
index: globalIndex,
|
|
275
|
+
status: 'failed',
|
|
276
|
+
error: error as ApiError,
|
|
277
|
+
duration
|
|
278
|
+
});
|
|
279
|
+
updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
|
|
280
|
+
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const batchResults = await Promise.all(batchPromises);
|
|
158
286
|
results.push(...batchResults);
|
|
159
287
|
}
|
|
160
288
|
} else {
|
|
161
289
|
// Process entire chunk at once
|
|
162
|
-
const
|
|
290
|
+
const baseIndex = chunkIndex * (effectiveBatchSize || items.length);
|
|
291
|
+
|
|
292
|
+
updateProgress(chunk.length);
|
|
293
|
+
|
|
294
|
+
const chunkPromises = chunk.map(async (item, chunkItemIndex) => {
|
|
295
|
+
const globalIndex = baseIndex + chunkItemIndex;
|
|
296
|
+
const itemStartTime = Date.now();
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const result = await processor(item, globalIndex);
|
|
300
|
+
const duration = Date.now() - itemStartTime;
|
|
301
|
+
|
|
302
|
+
completed++;
|
|
303
|
+
progressResults.push({
|
|
304
|
+
index: globalIndex,
|
|
305
|
+
status: 'completed',
|
|
306
|
+
result,
|
|
307
|
+
duration
|
|
308
|
+
});
|
|
309
|
+
updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
|
|
310
|
+
|
|
311
|
+
return result;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const duration = Date.now() - itemStartTime;
|
|
314
|
+
failed++;
|
|
315
|
+
completed++;
|
|
316
|
+
|
|
317
|
+
progressResults.push({
|
|
318
|
+
index: globalIndex,
|
|
319
|
+
status: 'failed',
|
|
320
|
+
error: error as ApiError,
|
|
321
|
+
duration
|
|
322
|
+
});
|
|
323
|
+
updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
|
|
324
|
+
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
163
330
|
results.push(...chunkResults);
|
|
164
331
|
}
|
|
165
332
|
}
|
|
166
333
|
|
|
334
|
+
// Final progress update
|
|
335
|
+
updateProgress(0);
|
|
167
336
|
return results;
|
|
168
337
|
}
|
|
169
338
|
|
|
170
339
|
// Batch operation with multiple IDs (only for patch and delete)
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
executeOpts?.ids &&
|
|
174
|
-
executeOpts.ids.length > 0 &&
|
|
175
|
-
(method.toLowerCase() === "patch" || method.toLowerCase() === "delete")
|
|
176
|
-
) {
|
|
177
|
-
const responses = await processBatch(executeOpts.ids, async (id) => {
|
|
340
|
+
if (isBatchOperation && executeOpts?.ids && executeOpts.ids.length > 0) {
|
|
341
|
+
const responses = await processBatch(executeOpts.ids, async (id, index) => {
|
|
178
342
|
const finalPath = buildPath(basePath, id);
|
|
179
343
|
return $fetch<T>(finalPath, {
|
|
180
344
|
baseURL: fullBaseURL,
|
|
@@ -190,14 +354,8 @@ export function useEnfyraApi<T = any>(
|
|
|
190
354
|
}
|
|
191
355
|
|
|
192
356
|
// Batch operation with files array for POST method
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
method.toLowerCase() === "post" &&
|
|
196
|
-
executeOpts?.files &&
|
|
197
|
-
Array.isArray(executeOpts.files) &&
|
|
198
|
-
executeOpts.files.length > 0
|
|
199
|
-
) {
|
|
200
|
-
const responses = await processBatch(executeOpts.files, async (fileObj: any) => {
|
|
357
|
+
if (isBatchOperation && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0) {
|
|
358
|
+
const responses = await processBatch(executeOpts.files, async (fileObj: any, index) => {
|
|
201
359
|
return $fetch<T>(basePath, {
|
|
202
360
|
baseURL: fullBaseURL,
|
|
203
361
|
method: method as any,
|
package/src/module.ts
CHANGED
|
@@ -18,6 +18,20 @@ export default defineNuxtModule({
|
|
|
18
18
|
setup(options, nuxt) {
|
|
19
19
|
const { resolve } = createResolver(import.meta.url);
|
|
20
20
|
|
|
21
|
+
// Validate required configuration
|
|
22
|
+
if (!options.apiUrl || !options.appUrl) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`[Enfyra SDK Nuxt] Missing required configuration:\n` +
|
|
25
|
+
`${!options.apiUrl ? '- apiUrl is required\n' : ''}` +
|
|
26
|
+
`${!options.appUrl ? '- appUrl is required\n' : ''}` +
|
|
27
|
+
`Please configure both in your nuxt.config.ts:\n` +
|
|
28
|
+
`enfyraSDK: {\n` +
|
|
29
|
+
` apiUrl: 'https://your-api-url',\n` +
|
|
30
|
+
` appUrl: 'https://your-app-url'\n` +
|
|
31
|
+
`}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
// Make module options available at runtime
|
|
22
36
|
nuxt.options.runtimeConfig.public.enfyraSDK = {
|
|
23
37
|
...nuxt.options.runtimeConfig.public.enfyraSDK,
|
package/src/types/index.ts
CHANGED
|
@@ -11,7 +11,39 @@ export interface ApiError {
|
|
|
11
11
|
response?: any;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface BatchProgress {
|
|
15
|
+
/** Current progress percentage (0-100) */
|
|
16
|
+
progress: number;
|
|
17
|
+
/** Number of completed operations */
|
|
18
|
+
completed: number;
|
|
19
|
+
/** Total number of operations */
|
|
20
|
+
total: number;
|
|
21
|
+
/** Number of failed operations */
|
|
22
|
+
failed: number;
|
|
23
|
+
/** Number of operations currently in progress */
|
|
24
|
+
inProgress: number;
|
|
25
|
+
/** Estimated time remaining in milliseconds */
|
|
26
|
+
estimatedTimeRemaining?: number;
|
|
27
|
+
/** Average time per operation in milliseconds */
|
|
28
|
+
averageTime?: number;
|
|
29
|
+
/** Current batch being processed */
|
|
30
|
+
currentBatch: number;
|
|
31
|
+
/** Total number of batches */
|
|
32
|
+
totalBatches: number;
|
|
33
|
+
/** Processing speed (operations per second) */
|
|
34
|
+
operationsPerSecond?: number;
|
|
35
|
+
/** Detailed results array for completed operations */
|
|
36
|
+
results: Array<{
|
|
37
|
+
index: number;
|
|
38
|
+
status: 'completed' | 'failed';
|
|
39
|
+
result?: any;
|
|
40
|
+
error?: ApiError;
|
|
41
|
+
duration?: number;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Base options available for all operations
|
|
46
|
+
interface BaseApiOptions<T> {
|
|
15
47
|
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
16
48
|
body?: any;
|
|
17
49
|
query?: Record<string, any>;
|
|
@@ -19,10 +51,6 @@ export interface ApiOptions<T> {
|
|
|
19
51
|
errorContext?: string;
|
|
20
52
|
onError?: (error: ApiError, context?: string) => void;
|
|
21
53
|
disableBatch?: boolean;
|
|
22
|
-
/** Batch size for chunking large operations (default: no limit) */
|
|
23
|
-
batchSize?: number;
|
|
24
|
-
/** Maximum concurrent requests (default: no limit) */
|
|
25
|
-
concurrent?: number;
|
|
26
54
|
default?: () => T;
|
|
27
55
|
/** Enable SSR with useFetch instead of $fetch */
|
|
28
56
|
ssr?: boolean;
|
|
@@ -30,6 +58,28 @@ export interface ApiOptions<T> {
|
|
|
30
58
|
key?: string;
|
|
31
59
|
}
|
|
32
60
|
|
|
61
|
+
// Batch-specific options (only available for batch operations)
|
|
62
|
+
interface BatchApiOptions {
|
|
63
|
+
/** Batch size for chunking large operations (default: no limit) - Only available for batch operations */
|
|
64
|
+
batchSize?: number;
|
|
65
|
+
/** Maximum concurrent requests (default: no limit) - Only available for batch operations */
|
|
66
|
+
concurrent?: number;
|
|
67
|
+
/** Real-time progress callback for batch operations - Only available for batch operations */
|
|
68
|
+
onProgress?: (progress: BatchProgress) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Conditional type that adds batch options only for batch-capable methods
|
|
72
|
+
type ConditionalBatchOptions<T> = T extends { method?: 'patch' | 'delete' | 'PATCH' | 'DELETE' }
|
|
73
|
+
? BatchApiOptions
|
|
74
|
+
: T extends { method?: 'post' | 'POST' }
|
|
75
|
+
? BatchApiOptions // POST supports file batch uploads
|
|
76
|
+
: T extends { method?: undefined } // Default method is 'get', but could be overridden at execution
|
|
77
|
+
? Partial<BatchApiOptions> // Allow batch options but make them optional since method could change
|
|
78
|
+
: {};
|
|
79
|
+
|
|
80
|
+
// Main ApiOptions interface with conditional batch support
|
|
81
|
+
export type ApiOptions<T> = BaseApiOptions<T> & ConditionalBatchOptions<BaseApiOptions<T>>;
|
|
82
|
+
|
|
33
83
|
export interface BackendError {
|
|
34
84
|
success: false;
|
|
35
85
|
message: string;
|
|
@@ -59,9 +109,14 @@ export interface UseEnfyraApiSSRReturn<T> extends AsyncData<T | null, ApiError>
|
|
|
59
109
|
}
|
|
60
110
|
|
|
61
111
|
// Execute options interface
|
|
62
|
-
|
|
112
|
+
// Base execute options available for all operations
|
|
113
|
+
interface BaseExecuteOptions {
|
|
63
114
|
body?: any;
|
|
64
115
|
id?: string | number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Batch execute options (only available when doing batch operations)
|
|
119
|
+
interface BatchExecuteOptions {
|
|
65
120
|
ids?: (string | number)[];
|
|
66
121
|
/** Array of FormData objects for batch upload */
|
|
67
122
|
files?: FormData[];
|
|
@@ -69,8 +124,19 @@ export interface ExecuteOptions {
|
|
|
69
124
|
batchSize?: number;
|
|
70
125
|
/** Override concurrent limit for this specific execution */
|
|
71
126
|
concurrent?: number;
|
|
127
|
+
/** Override progress callback for this specific execution */
|
|
128
|
+
onProgress?: (progress: BatchProgress) => void;
|
|
72
129
|
}
|
|
73
130
|
|
|
131
|
+
// Conditional execute options based on what's being executed
|
|
132
|
+
type ConditionalExecuteOptions<T> = T extends { ids: any }
|
|
133
|
+
? BatchExecuteOptions // If ids provided, enable all batch options
|
|
134
|
+
: T extends { files: any }
|
|
135
|
+
? BatchExecuteOptions // If files provided, enable all batch options
|
|
136
|
+
: BaseExecuteOptions & Partial<BatchExecuteOptions>; // Otherwise base options + optional batch options
|
|
137
|
+
|
|
138
|
+
export type ExecuteOptions = BaseExecuteOptions & BatchExecuteOptions;
|
|
139
|
+
|
|
74
140
|
// Client Mode return type
|
|
75
141
|
export interface UseEnfyraApiClientReturn<T> {
|
|
76
142
|
data: Ref<T | null>;
|