@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 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 for CRUD actions (client-side)
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, // Process 10 items at a time
198
- concurrent: 3, // Max 3 requests simultaneously
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, // Default: 20 per batch
209
- concurrent: 5 // Default: 5 concurrent
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, // Override: 50 per batch for this call
217
- concurrent: 10 // Override: 10 concurrent for this call
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
- // Batch file upload with progress control
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, // Upload 5 files at a time
224
- concurrent: 2, // Max 2 uploads simultaneously
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 effectiveBatchSize = executeOpts?.batchSize ?? batchSize;
69
- const effectiveConcurrent = executeOpts?.concurrent ?? concurrent;
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
- if (!effectiveBatchSize && !effectiveConcurrent) {
77
- const promises = items.map(processor);
78
- return await Promise.all(promises);
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
- for (const chunk of chunks) {
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 batchResults = await Promise.all(batch.map(processor));
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 chunkResults = await Promise.all(chunk.map(processor));
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 (!opts.disableBatch && executeOpts?.ids && executeOpts.ids.length > 0 && (method.toLowerCase() === "patch" || method.toLowerCase() === "delete")) {
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 (!opts.disableBatch && method.toLowerCase() === "post" && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0) {
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 ApiOptions<T> {
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
- export interface ExecuteOptions {
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>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/types/index.ts"],"names":[],"mappings":"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"}
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enfyra/sdk-nuxt",
3
3
  "configKey": "enfyraSDK",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/sdk-nuxt",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Nuxt SDK for Enfyra CMS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- // Use executeOpts overrides if provided, otherwise fall back to options
121
- const effectiveBatchSize = executeOpts?.batchSize ?? batchSize;
122
- const effectiveConcurrent = executeOpts?.concurrent ?? concurrent;
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 concurrency control
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
- // If no limits, process all at once (current behavior)
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 (const chunk of chunks) {
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 batchResults = await Promise.all(batch.map(processor));
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 chunkResults = await Promise.all(chunk.map(processor));
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
- !opts.disableBatch &&
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
- !opts.disableBatch &&
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,
@@ -11,7 +11,39 @@ export interface ApiError {
11
11
  response?: any;
12
12
  }
13
13
 
14
- export interface ApiOptions<T> {
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
- export interface ExecuteOptions {
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>;