@enfyra/sdk-nuxt 0.2.2 → 0.2.4

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`
218
259
  });
219
260
 
220
- // Batch file upload with progress control
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
269
+ });
270
+
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,9 +427,6 @@ 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: '')
314
- apiPrefix: '/api/v1',
315
-
316
430
  // Required: App URL for SSR requests
317
431
  appUrl: process.env.ENFYRA_APP_URL || "http://localhost:3001",
318
432
  },
@@ -1,5 +1,6 @@
1
1
  import { ref, unref, toRaw } from "vue";
2
2
  import { $fetch } from "../utils/http";
3
+ import { ENFYRA_API_PREFIX } from "../constants/config";
3
4
  import { useRuntimeConfig, useFetch, useRequestHeaders } from "#imports";
4
5
  function handleError(error, context, customHandler) {
5
6
  const apiError = {
@@ -16,11 +17,11 @@ function handleError(error, context, customHandler) {
16
17
  return apiError;
17
18
  }
18
19
  export function useEnfyraApi(path, opts = {}) {
19
- const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent } = opts;
20
+ const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent, onProgress } = opts;
20
21
  if (ssr) {
21
22
  const config = useRuntimeConfig().public.enfyraSDK;
22
23
  const basePath = (typeof path === "function" ? path() : path).replace(/^\/?api\/?/, "").replace(/^\/+/, "");
23
- const finalUrl = (config?.appUrl || "") + (config?.apiPrefix || "") + "/" + basePath;
24
+ const finalUrl = (config?.appUrl || "") + (config?.apiPrefix || ENFYRA_API_PREFIX) + "/" + basePath;
24
25
  const clientHeaders = process.client ? {} : useRequestHeaders([
25
26
  "authorization",
26
27
  "cookie",
@@ -65,38 +66,165 @@ export function useEnfyraApi(path, opts = {}) {
65
66
  const basePath = (typeof path === "function" ? path() : path).replace(/^\/?api\/?/, "").replace(/^\/+/, "");
66
67
  const finalBody = executeOpts?.body || unref(body);
67
68
  const finalQuery = unref(query);
68
- const effectiveBatchSize = executeOpts?.batchSize ?? batchSize;
69
- const effectiveConcurrent = executeOpts?.concurrent ?? concurrent;
69
+ 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);
70
+ const effectiveBatchSize = isBatchOperation ? executeOpts?.batchSize ?? batchSize : void 0;
71
+ const effectiveConcurrent = isBatchOperation ? executeOpts?.concurrent ?? concurrent : void 0;
72
+ const effectiveOnProgress = isBatchOperation ? executeOpts?.onProgress ?? onProgress : void 0;
70
73
  const buildPath = (...segments) => {
71
74
  return segments.filter(Boolean).join("/");
72
75
  };
73
- const fullBaseURL = apiUrl + (apiPrefix || "");
76
+ const fullBaseURL = apiUrl + (apiPrefix || ENFYRA_API_PREFIX);
74
77
  async function processBatch(items, processor) {
75
78
  const results = [];
76
- if (!effectiveBatchSize && !effectiveConcurrent) {
77
- const promises = items.map(processor);
78
- return await Promise.all(promises);
79
- }
79
+ const progressResults = [];
80
+ const startTime = Date.now();
81
+ let completed = 0;
82
+ let failed = 0;
80
83
  const chunks = effectiveBatchSize ? Array.from(
81
84
  { length: Math.ceil(items.length / effectiveBatchSize) },
82
85
  (_, i) => items.slice(i * effectiveBatchSize, i * effectiveBatchSize + effectiveBatchSize)
83
86
  ) : [items];
84
- for (const chunk of chunks) {
87
+ const totalBatches = chunks.length;
88
+ let currentBatch = 0;
89
+ const updateProgress = (inProgress = 0) => {
90
+ if (effectiveOnProgress) {
91
+ const elapsed = Date.now() - startTime;
92
+ const progress = items.length > 0 ? Math.round(completed / items.length * 100) : 0;
93
+ const averageTime = completed > 0 ? elapsed / completed : void 0;
94
+ const operationsPerSecond = completed > 0 ? completed / elapsed * 1e3 : void 0;
95
+ const estimatedTimeRemaining = averageTime && items.length > completed ? Math.round(averageTime * (items.length - completed)) : void 0;
96
+ const progressData = {
97
+ progress,
98
+ completed,
99
+ total: items.length,
100
+ failed,
101
+ inProgress,
102
+ estimatedTimeRemaining,
103
+ averageTime,
104
+ currentBatch: currentBatch + 1,
105
+ totalBatches,
106
+ operationsPerSecond,
107
+ results: [...progressResults]
108
+ };
109
+ effectiveOnProgress(progressData);
110
+ }
111
+ };
112
+ updateProgress(0);
113
+ if (!effectiveBatchSize && !effectiveConcurrent) {
114
+ updateProgress(items.length);
115
+ const promises = items.map(async (item, index) => {
116
+ const itemStartTime = Date.now();
117
+ try {
118
+ const result = await processor(item, index);
119
+ const duration = Date.now() - itemStartTime;
120
+ completed++;
121
+ progressResults.push({
122
+ index,
123
+ status: "completed",
124
+ result,
125
+ duration
126
+ });
127
+ updateProgress(items.length - completed);
128
+ return result;
129
+ } catch (error2) {
130
+ const duration = Date.now() - itemStartTime;
131
+ failed++;
132
+ completed++;
133
+ progressResults.push({
134
+ index,
135
+ status: "failed",
136
+ error: error2,
137
+ duration
138
+ });
139
+ updateProgress(items.length - completed);
140
+ throw error2;
141
+ }
142
+ });
143
+ const results2 = await Promise.all(promises);
144
+ updateProgress(0);
145
+ return results2;
146
+ }
147
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
148
+ currentBatch = chunkIndex;
149
+ const chunk = chunks[chunkIndex];
85
150
  if (effectiveConcurrent && chunk.length > effectiveConcurrent) {
86
151
  for (let i = 0; i < chunk.length; i += effectiveConcurrent) {
87
152
  const batch = chunk.slice(i, i + effectiveConcurrent);
88
- const batchResults = await Promise.all(batch.map(processor));
153
+ const baseIndex = chunkIndex * (effectiveBatchSize || items.length) + i;
154
+ updateProgress(batch.length);
155
+ const batchPromises = batch.map(async (item, batchItemIndex) => {
156
+ const globalIndex = baseIndex + batchItemIndex;
157
+ const itemStartTime = Date.now();
158
+ try {
159
+ const result = await processor(item, globalIndex);
160
+ const duration = Date.now() - itemStartTime;
161
+ completed++;
162
+ progressResults.push({
163
+ index: globalIndex,
164
+ status: "completed",
165
+ result,
166
+ duration
167
+ });
168
+ updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
169
+ return result;
170
+ } catch (error2) {
171
+ const duration = Date.now() - itemStartTime;
172
+ failed++;
173
+ completed++;
174
+ progressResults.push({
175
+ index: globalIndex,
176
+ status: "failed",
177
+ error: error2,
178
+ duration
179
+ });
180
+ updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
181
+ throw error2;
182
+ }
183
+ });
184
+ const batchResults = await Promise.all(batchPromises);
89
185
  results.push(...batchResults);
90
186
  }
91
187
  } else {
92
- const chunkResults = await Promise.all(chunk.map(processor));
188
+ const baseIndex = chunkIndex * (effectiveBatchSize || items.length);
189
+ updateProgress(chunk.length);
190
+ const chunkPromises = chunk.map(async (item, chunkItemIndex) => {
191
+ const globalIndex = baseIndex + chunkItemIndex;
192
+ const itemStartTime = Date.now();
193
+ try {
194
+ const result = await processor(item, globalIndex);
195
+ const duration = Date.now() - itemStartTime;
196
+ completed++;
197
+ progressResults.push({
198
+ index: globalIndex,
199
+ status: "completed",
200
+ result,
201
+ duration
202
+ });
203
+ updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
204
+ return result;
205
+ } catch (error2) {
206
+ const duration = Date.now() - itemStartTime;
207
+ failed++;
208
+ completed++;
209
+ progressResults.push({
210
+ index: globalIndex,
211
+ status: "failed",
212
+ error: error2,
213
+ duration
214
+ });
215
+ updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
216
+ throw error2;
217
+ }
218
+ });
219
+ const chunkResults = await Promise.all(chunkPromises);
93
220
  results.push(...chunkResults);
94
221
  }
95
222
  }
223
+ updateProgress(0);
96
224
  return results;
97
225
  }
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) => {
226
+ if (isBatchOperation && executeOpts?.ids && executeOpts.ids.length > 0) {
227
+ const responses = await processBatch(executeOpts.ids, async (id, index) => {
100
228
  const finalPath2 = buildPath(basePath, id);
101
229
  return $fetch(finalPath2, {
102
230
  baseURL: fullBaseURL,
@@ -109,8 +237,8 @@ export function useEnfyraApi(path, opts = {}) {
109
237
  data.value = responses;
110
238
  return responses;
111
239
  }
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) => {
240
+ if (isBatchOperation && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0) {
241
+ const responses = await processBatch(executeOpts.files, async (fileObj, index) => {
114
242
  return $fetch(basePath, {
115
243
  baseURL: fullBaseURL,
116
244
  method,
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hardcoded API prefix for all Enfyra SDK routes
3
+ * This ensures no conflicts with application routes
4
+ */
5
+ export declare const ENFYRA_API_PREFIX = "/enfyra/api";
@@ -0,0 +1 @@
1
+ export const ENFYRA_API_PREFIX = "/enfyra/api";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export interface EnfyraConfig {
2
2
  apiUrl: string;
3
- apiPrefix?: string;
4
3
  defaultHeaders?: Record<string, string>;
5
4
  }
6
5
  export interface ApiError {
@@ -9,7 +8,37 @@ export interface ApiError {
9
8
  data?: any;
10
9
  response?: any;
11
10
  }
12
- export interface ApiOptions<T> {
11
+ export interface BatchProgress {
12
+ /** Current progress percentage (0-100) */
13
+ progress: number;
14
+ /** Number of completed operations */
15
+ completed: number;
16
+ /** Total number of operations */
17
+ total: number;
18
+ /** Number of failed operations */
19
+ failed: number;
20
+ /** Number of operations currently in progress */
21
+ inProgress: number;
22
+ /** Estimated time remaining in milliseconds */
23
+ estimatedTimeRemaining?: number;
24
+ /** Average time per operation in milliseconds */
25
+ averageTime?: number;
26
+ /** Current batch being processed */
27
+ currentBatch: number;
28
+ /** Total number of batches */
29
+ totalBatches: number;
30
+ /** Processing speed (operations per second) */
31
+ operationsPerSecond?: number;
32
+ /** Detailed results array for completed operations */
33
+ results: Array<{
34
+ index: number;
35
+ status: 'completed' | 'failed';
36
+ result?: any;
37
+ error?: ApiError;
38
+ duration?: number;
39
+ }>;
40
+ }
41
+ interface BaseApiOptions<T> {
13
42
  method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
14
43
  body?: any;
15
44
  query?: Record<string, any>;
@@ -17,16 +46,28 @@ export interface ApiOptions<T> {
17
46
  errorContext?: string;
18
47
  onError?: (error: ApiError, context?: string) => void;
19
48
  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
49
  default?: () => T;
25
50
  /** Enable SSR with useFetch instead of $fetch */
26
51
  ssr?: boolean;
27
52
  /** Unique key for useFetch caching */
28
53
  key?: string;
29
54
  }
55
+ interface BatchApiOptions {
56
+ /** Batch size for chunking large operations (default: no limit) - Only available for batch operations */
57
+ batchSize?: number;
58
+ /** Maximum concurrent requests (default: no limit) - Only available for batch operations */
59
+ concurrent?: number;
60
+ /** Real-time progress callback for batch operations - Only available for batch operations */
61
+ onProgress?: (progress: BatchProgress) => void;
62
+ }
63
+ type ConditionalBatchOptions<T> = T extends {
64
+ method?: 'patch' | 'delete' | 'PATCH' | 'DELETE';
65
+ } ? BatchApiOptions : T extends {
66
+ method?: 'post' | 'POST';
67
+ } ? BatchApiOptions : T extends {
68
+ method?: undefined;
69
+ } ? Partial<BatchApiOptions> : {};
70
+ export type ApiOptions<T> = BaseApiOptions<T> & ConditionalBatchOptions<BaseApiOptions<T>>;
30
71
  export interface BackendError {
31
72
  success: false;
32
73
  message: string;
@@ -50,9 +91,11 @@ export interface UseEnfyraApiSSRReturn<T> extends AsyncData<T | null, ApiError>
50
91
  error: Ref<ApiError | null>;
51
92
  refresh: () => Promise<void>;
52
93
  }
53
- export interface ExecuteOptions {
94
+ interface BaseExecuteOptions {
54
95
  body?: any;
55
96
  id?: string | number;
97
+ }
98
+ interface BatchExecuteOptions {
56
99
  ids?: (string | number)[];
57
100
  /** Array of FormData objects for batch upload */
58
101
  files?: FormData[];
@@ -60,7 +103,10 @@ export interface ExecuteOptions {
60
103
  batchSize?: number;
61
104
  /** Override concurrent limit for this specific execution */
62
105
  concurrent?: number;
106
+ /** Override progress callback for this specific execution */
107
+ onProgress?: (progress: BatchProgress) => void;
63
108
  }
109
+ export type ExecuteOptions = BaseExecuteOptions & BatchExecuteOptions;
64
110
  export interface UseEnfyraApiClientReturn<T> {
65
111
  data: Ref<T | null>;
66
112
  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
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const kit = require('@nuxt/kit');
4
+ const config_mjs = require('../dist/constants/config.mjs');
4
5
 
5
6
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
7
  const module$1 = kit.defineNuxtModule({
@@ -10,14 +11,24 @@ const module$1 = kit.defineNuxtModule({
10
11
  },
11
12
  defaults: {
12
13
  apiUrl: "",
13
- apiPrefix: "/api",
14
14
  appUrl: ""
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
- ...options
30
+ ...options,
31
+ apiPrefix: config_mjs.ENFYRA_API_PREFIX
21
32
  };
22
33
  kit.addImportsDir(resolve("./composables"));
23
34
  kit.addServerHandler({
@@ -25,17 +36,17 @@ const module$1 = kit.defineNuxtModule({
25
36
  middleware: true
26
37
  });
27
38
  kit.addServerHandler({
28
- route: "/api/login",
39
+ route: `${config_mjs.ENFYRA_API_PREFIX}/login`,
29
40
  handler: resolve("./runtime/server/api/login.post"),
30
41
  method: "post"
31
42
  });
32
43
  kit.addServerHandler({
33
- route: "/api/logout",
44
+ route: `${config_mjs.ENFYRA_API_PREFIX}/logout`,
34
45
  handler: resolve("./runtime/server/api/logout.post"),
35
46
  method: "post"
36
47
  });
37
48
  kit.addServerHandler({
38
- route: "/api/**",
49
+ route: `${config_mjs.ENFYRA_API_PREFIX}/**`,
39
50
  handler: resolve("./runtime/server/api/all")
40
51
  });
41
52
  }
package/dist/module.d.cts CHANGED
@@ -2,11 +2,9 @@ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  declare const _default: _nuxt_schema.NuxtModule<{
4
4
  apiUrl: string;
5
- apiPrefix: string;
6
5
  appUrl: string;
7
6
  }, {
8
7
  apiUrl: string;
9
- apiPrefix: string;
10
8
  appUrl: string;
11
9
  }, false>;
12
10
 
package/dist/module.d.mts CHANGED
@@ -2,11 +2,9 @@ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  declare const _default: _nuxt_schema.NuxtModule<{
4
4
  apiUrl: string;
5
- apiPrefix: string;
6
5
  appUrl: string;
7
6
  }, {
8
7
  apiUrl: string;
9
- apiPrefix: string;
10
8
  appUrl: string;
11
9
  }, false>;
12
10
 
package/dist/module.d.ts CHANGED
@@ -2,11 +2,9 @@ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  declare const _default: _nuxt_schema.NuxtModule<{
4
4
  apiUrl: string;
5
- apiPrefix: string;
6
5
  appUrl: string;
7
6
  }, {
8
7
  apiUrl: string;
9
- apiPrefix: string;
10
8
  appUrl: string;
11
9
  }, false>;
12
10
 
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.4",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { defineNuxtModule, createResolver, addImportsDir, addServerHandler } from '@nuxt/kit';
2
+ import { ENFYRA_API_PREFIX } from '../dist/constants/config.mjs';
2
3
 
3
4
  const module = defineNuxtModule({
4
5
  meta: {
@@ -7,14 +8,24 @@ const module = defineNuxtModule({
7
8
  },
8
9
  defaults: {
9
10
  apiUrl: "",
10
- apiPrefix: "/api",
11
11
  appUrl: ""
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
- ...options
27
+ ...options,
28
+ apiPrefix: ENFYRA_API_PREFIX
18
29
  };
19
30
  addImportsDir(resolve("./composables"));
20
31
  addServerHandler({
@@ -22,17 +33,17 @@ const module = defineNuxtModule({
22
33
  middleware: true
23
34
  });
24
35
  addServerHandler({
25
- route: "/api/login",
36
+ route: `${ENFYRA_API_PREFIX}/login`,
26
37
  handler: resolve("./runtime/server/api/login.post"),
27
38
  method: "post"
28
39
  });
29
40
  addServerHandler({
30
- route: "/api/logout",
41
+ route: `${ENFYRA_API_PREFIX}/logout`,
31
42
  handler: resolve("./runtime/server/api/logout.post"),
32
43
  method: "post"
33
44
  });
34
45
  addServerHandler({
35
- route: "/api/**",
46
+ route: `${ENFYRA_API_PREFIX}/**`,
36
47
  handler: resolve("./runtime/server/api/all")
37
48
  });
38
49
  }
@@ -1,7 +1,6 @@
1
1
  import { ref } from "vue";
2
2
  const config = ref({
3
3
  apiUrl: "",
4
- apiPrefix: "/api",
5
4
  defaultHeaders: {}
6
5
  });
7
6
  export function useEnfyraConfig() {
@@ -1,8 +1,9 @@
1
1
  import { proxyRequest } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
+ import { ENFYRA_API_PREFIX } from "../../constants/config";
3
4
  export function proxyToAPI(event, customPath) {
4
5
  const config = useRuntimeConfig();
5
- const apiPrefix = config.public.enfyraSDK.apiPrefix;
6
+ const apiPrefix = config.public.enfyraSDK.apiPrefix || ENFYRA_API_PREFIX;
6
7
  const rawPath = customPath || event.path.replace(new RegExp(`^${apiPrefix}`), "");
7
8
  const targetUrl = `${config.public.enfyraSDK.apiUrl}${rawPath}`;
8
9
  const headers = event.context.proxyHeaders || {};
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.4",
4
4
  "description": "Nuxt SDK for Enfyra CMS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -6,8 +6,10 @@ import type {
6
6
  ExecuteOptions,
7
7
  UseEnfyraApiSSRReturn,
8
8
  UseEnfyraApiClientReturn,
9
+ BatchProgress,
9
10
  } from "../types";
10
11
  import { $fetch } from "../utils/http";
12
+ import { ENFYRA_API_PREFIX } from "../constants/config";
11
13
  import { useRuntimeConfig, useFetch, useRequestHeaders } from "#imports";
12
14
 
13
15
  function handleError(
@@ -47,7 +49,7 @@ export function useEnfyraApi<T = any>(
47
49
  path: (() => string) | string,
48
50
  opts: ApiOptions<T> = {}
49
51
  ): UseEnfyraApiSSRReturn<T> | UseEnfyraApiClientReturn<T> {
50
- const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent } = opts;
52
+ const { method = "get", body, query, errorContext, onError, ssr, key, batchSize, concurrent, onProgress } = opts;
51
53
 
52
54
  // SSR mode - use useFetch
53
55
  if (ssr) {
@@ -57,7 +59,7 @@ export function useEnfyraApi<T = any>(
57
59
  .replace(/^\/+/, ""); // Remove leading slashes
58
60
 
59
61
  const finalUrl =
60
- (config?.appUrl || "") + (config?.apiPrefix || "") + "/" + basePath;
62
+ (config?.appUrl || "") + (config?.apiPrefix || ENFYRA_API_PREFIX) + "/" + basePath;
61
63
 
62
64
  // Get headers from client request and filter out connection-specific headers
63
65
  const clientHeaders = process.client
@@ -117,9 +119,19 @@ export function useEnfyraApi<T = any>(
117
119
  const finalBody = executeOpts?.body || unref(body);
118
120
  const finalQuery = unref(query);
119
121
 
120
- // Use executeOpts overrides if provided, otherwise fall back to options
121
- const effectiveBatchSize = executeOpts?.batchSize ?? batchSize;
122
- const effectiveConcurrent = executeOpts?.concurrent ?? concurrent;
122
+ // Check if this is actually a batch operation
123
+ const isBatchOperation = (
124
+ !opts.disableBatch &&
125
+ (
126
+ (executeOpts?.ids && executeOpts.ids.length > 0 && (method.toLowerCase() === "patch" || method.toLowerCase() === "delete")) ||
127
+ (method.toLowerCase() === "post" && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0)
128
+ )
129
+ );
130
+
131
+ // Use batch options only if this is actually a batch operation
132
+ const effectiveBatchSize = isBatchOperation ? (executeOpts?.batchSize ?? batchSize) : undefined;
133
+ const effectiveConcurrent = isBatchOperation ? (executeOpts?.concurrent ?? concurrent) : undefined;
134
+ const effectiveOnProgress = isBatchOperation ? (executeOpts?.onProgress ?? onProgress) : undefined;
123
135
 
124
136
  // Helper function to build clean path
125
137
  const buildPath = (...segments: (string | number)[]): string => {
@@ -127,54 +139,207 @@ export function useEnfyraApi<T = any>(
127
139
  };
128
140
 
129
141
  // Build full base URL with prefix
130
- const fullBaseURL = apiUrl + (apiPrefix || "");
142
+ const fullBaseURL = apiUrl + (apiPrefix || ENFYRA_API_PREFIX);
131
143
 
132
- // Helper function for batch processing with chunking and concurrency control
144
+ // Helper function for batch processing with chunking, concurrency control, and real-time progress tracking
133
145
  async function processBatch<T>(
134
146
  items: any[],
135
- processor: (item: any) => Promise<T>
147
+ processor: (item: any, index: number) => Promise<T>
136
148
  ): Promise<T[]> {
137
149
  const results: T[] = [];
150
+ const progressResults: BatchProgress['results'] = [];
151
+ const startTime = Date.now();
152
+ let completed = 0;
153
+ let failed = 0;
138
154
 
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
155
+ // Calculate batch structure
146
156
  const chunks = effectiveBatchSize ?
147
157
  Array.from({ length: Math.ceil(items.length / effectiveBatchSize) }, (_, i) =>
148
158
  items.slice(i * effectiveBatchSize, i * effectiveBatchSize + effectiveBatchSize)
149
159
  ) : [items];
160
+
161
+ const totalBatches = chunks.length;
162
+ let currentBatch = 0;
163
+
164
+ // Initialize progress tracking
165
+ const updateProgress = (inProgress: number = 0) => {
166
+ if (effectiveOnProgress) {
167
+ const elapsed = Date.now() - startTime;
168
+ const progress = items.length > 0 ? Math.round((completed / items.length) * 100) : 0;
169
+ const averageTime = completed > 0 ? elapsed / completed : undefined;
170
+ const operationsPerSecond = completed > 0 ? (completed / elapsed) * 1000 : undefined;
171
+ const estimatedTimeRemaining = averageTime && items.length > completed
172
+ ? Math.round(averageTime * (items.length - completed))
173
+ : undefined;
174
+
175
+ const progressData: BatchProgress = {
176
+ progress,
177
+ completed,
178
+ total: items.length,
179
+ failed,
180
+ inProgress,
181
+ estimatedTimeRemaining,
182
+ averageTime,
183
+ currentBatch: currentBatch + 1,
184
+ totalBatches,
185
+ operationsPerSecond,
186
+ results: [...progressResults]
187
+ };
188
+
189
+ effectiveOnProgress(progressData);
190
+ }
191
+ };
192
+
193
+ // Initial progress update
194
+ updateProgress(0);
195
+
196
+ // If no limits, process all at once with progress tracking
197
+ if (!effectiveBatchSize && !effectiveConcurrent) {
198
+ updateProgress(items.length);
199
+
200
+ const promises = items.map(async (item, index) => {
201
+ const itemStartTime = Date.now();
202
+ try {
203
+ const result = await processor(item, index);
204
+ const duration = Date.now() - itemStartTime;
205
+
206
+ completed++;
207
+ progressResults.push({
208
+ index,
209
+ status: 'completed',
210
+ result,
211
+ duration
212
+ });
213
+ updateProgress(items.length - completed);
214
+
215
+ return result;
216
+ } catch (error) {
217
+ const duration = Date.now() - itemStartTime;
218
+ failed++;
219
+ completed++;
220
+
221
+ progressResults.push({
222
+ index,
223
+ status: 'failed',
224
+ error: error as ApiError,
225
+ duration
226
+ });
227
+ updateProgress(items.length - completed);
228
+
229
+ throw error;
230
+ }
231
+ });
232
+
233
+ const results = await Promise.all(promises);
234
+ updateProgress(0);
235
+ return results;
236
+ }
150
237
 
151
- // Process each chunk
152
- for (const chunk of chunks) {
238
+ // Process each chunk with progress tracking
239
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
240
+ currentBatch = chunkIndex;
241
+ const chunk = chunks[chunkIndex];
242
+
153
243
  if (effectiveConcurrent && chunk.length > effectiveConcurrent) {
154
244
  // Process chunk with concurrency limit
155
245
  for (let i = 0; i < chunk.length; i += effectiveConcurrent) {
156
246
  const batch = chunk.slice(i, i + effectiveConcurrent);
157
- const batchResults = await Promise.all(batch.map(processor));
247
+ const baseIndex = chunkIndex * (effectiveBatchSize || items.length) + i;
248
+
249
+ updateProgress(batch.length);
250
+
251
+ const batchPromises = batch.map(async (item, batchItemIndex) => {
252
+ const globalIndex = baseIndex + batchItemIndex;
253
+ const itemStartTime = Date.now();
254
+
255
+ try {
256
+ const result = await processor(item, globalIndex);
257
+ const duration = Date.now() - itemStartTime;
258
+
259
+ completed++;
260
+ progressResults.push({
261
+ index: globalIndex,
262
+ status: 'completed',
263
+ result,
264
+ duration
265
+ });
266
+ updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
267
+
268
+ return result;
269
+ } catch (error) {
270
+ const duration = Date.now() - itemStartTime;
271
+ failed++;
272
+ completed++;
273
+
274
+ progressResults.push({
275
+ index: globalIndex,
276
+ status: 'failed',
277
+ error: error as ApiError,
278
+ duration
279
+ });
280
+ updateProgress(Math.max(0, batch.length - (batchItemIndex + 1)));
281
+
282
+ throw error;
283
+ }
284
+ });
285
+
286
+ const batchResults = await Promise.all(batchPromises);
158
287
  results.push(...batchResults);
159
288
  }
160
289
  } else {
161
290
  // Process entire chunk at once
162
- const chunkResults = await Promise.all(chunk.map(processor));
291
+ const baseIndex = chunkIndex * (effectiveBatchSize || items.length);
292
+
293
+ updateProgress(chunk.length);
294
+
295
+ const chunkPromises = chunk.map(async (item, chunkItemIndex) => {
296
+ const globalIndex = baseIndex + chunkItemIndex;
297
+ const itemStartTime = Date.now();
298
+
299
+ try {
300
+ const result = await processor(item, globalIndex);
301
+ const duration = Date.now() - itemStartTime;
302
+
303
+ completed++;
304
+ progressResults.push({
305
+ index: globalIndex,
306
+ status: 'completed',
307
+ result,
308
+ duration
309
+ });
310
+ updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
311
+
312
+ return result;
313
+ } catch (error) {
314
+ const duration = Date.now() - itemStartTime;
315
+ failed++;
316
+ completed++;
317
+
318
+ progressResults.push({
319
+ index: globalIndex,
320
+ status: 'failed',
321
+ error: error as ApiError,
322
+ duration
323
+ });
324
+ updateProgress(Math.max(0, chunk.length - (chunkItemIndex + 1)));
325
+
326
+ throw error;
327
+ }
328
+ });
329
+
330
+ const chunkResults = await Promise.all(chunkPromises);
163
331
  results.push(...chunkResults);
164
332
  }
165
333
  }
166
334
 
335
+ // Final progress update
336
+ updateProgress(0);
167
337
  return results;
168
338
  }
169
339
 
170
340
  // 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) => {
341
+ if (isBatchOperation && executeOpts?.ids && executeOpts.ids.length > 0) {
342
+ const responses = await processBatch(executeOpts.ids, async (id, index) => {
178
343
  const finalPath = buildPath(basePath, id);
179
344
  return $fetch<T>(finalPath, {
180
345
  baseURL: fullBaseURL,
@@ -190,14 +355,8 @@ export function useEnfyraApi<T = any>(
190
355
  }
191
356
 
192
357
  // 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) => {
358
+ if (isBatchOperation && executeOpts?.files && Array.isArray(executeOpts.files) && executeOpts.files.length > 0) {
359
+ const responses = await processBatch(executeOpts.files, async (fileObj: any, index) => {
201
360
  return $fetch<T>(basePath, {
202
361
  baseURL: fullBaseURL,
203
362
  method: method as any,
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hardcoded API prefix for all Enfyra SDK routes
3
+ * This ensures no conflicts with application routes
4
+ */
5
+ export const ENFYRA_API_PREFIX = "/enfyra/api";
package/src/module.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  addServerHandler,
5
5
  addImportsDir,
6
6
  } from "@nuxt/kit";
7
+ import { ENFYRA_API_PREFIX } from "./constants/config";
7
8
 
8
9
  export default defineNuxtModule({
9
10
  meta: {
@@ -12,16 +13,30 @@ export default defineNuxtModule({
12
13
  },
13
14
  defaults: {
14
15
  apiUrl: "",
15
- apiPrefix: "/api",
16
16
  appUrl: "",
17
17
  },
18
18
  setup(options, nuxt) {
19
19
  const { resolve } = createResolver(import.meta.url);
20
20
 
21
- // Make module options available at runtime
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
+
35
+ // Make module options available at runtime with hardcoded apiPrefix
22
36
  nuxt.options.runtimeConfig.public.enfyraSDK = {
23
37
  ...nuxt.options.runtimeConfig.public.enfyraSDK,
24
38
  ...options,
39
+ apiPrefix: ENFYRA_API_PREFIX,
25
40
  };
26
41
 
27
42
  // Auto-import composables
@@ -33,21 +48,21 @@ export default defineNuxtModule({
33
48
  middleware: true,
34
49
  });
35
50
 
36
- // Register server handlers from SDK
51
+ // Register server handlers from SDK with hardcoded prefix
37
52
  addServerHandler({
38
- route: "/api/login",
53
+ route: `${ENFYRA_API_PREFIX}/login`,
39
54
  handler: resolve("./runtime/server/api/login.post"),
40
55
  method: "post",
41
56
  });
42
57
 
43
58
  addServerHandler({
44
- route: "/api/logout",
59
+ route: `${ENFYRA_API_PREFIX}/logout`,
45
60
  handler: resolve("./runtime/server/api/logout.post"),
46
61
  method: "post",
47
62
  });
48
63
 
49
64
  addServerHandler({
50
- route: "/api/**",
65
+ route: `${ENFYRA_API_PREFIX}/**`,
51
66
  handler: resolve("./runtime/server/api/all"),
52
67
  });
53
68
  },
@@ -1,6 +1,5 @@
1
1
  export interface EnfyraConfig {
2
2
  apiUrl: string;
3
- apiPrefix?: string;
4
3
  defaultHeaders?: Record<string, string>;
5
4
  }
6
5
 
@@ -11,7 +10,39 @@ export interface ApiError {
11
10
  response?: any;
12
11
  }
13
12
 
14
- export interface ApiOptions<T> {
13
+ export interface BatchProgress {
14
+ /** Current progress percentage (0-100) */
15
+ progress: number;
16
+ /** Number of completed operations */
17
+ completed: number;
18
+ /** Total number of operations */
19
+ total: number;
20
+ /** Number of failed operations */
21
+ failed: number;
22
+ /** Number of operations currently in progress */
23
+ inProgress: number;
24
+ /** Estimated time remaining in milliseconds */
25
+ estimatedTimeRemaining?: number;
26
+ /** Average time per operation in milliseconds */
27
+ averageTime?: number;
28
+ /** Current batch being processed */
29
+ currentBatch: number;
30
+ /** Total number of batches */
31
+ totalBatches: number;
32
+ /** Processing speed (operations per second) */
33
+ operationsPerSecond?: number;
34
+ /** Detailed results array for completed operations */
35
+ results: Array<{
36
+ index: number;
37
+ status: 'completed' | 'failed';
38
+ result?: any;
39
+ error?: ApiError;
40
+ duration?: number;
41
+ }>;
42
+ }
43
+
44
+ // Base options available for all operations
45
+ interface BaseApiOptions<T> {
15
46
  method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
16
47
  body?: any;
17
48
  query?: Record<string, any>;
@@ -19,10 +50,6 @@ export interface ApiOptions<T> {
19
50
  errorContext?: string;
20
51
  onError?: (error: ApiError, context?: string) => void;
21
52
  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
53
  default?: () => T;
27
54
  /** Enable SSR with useFetch instead of $fetch */
28
55
  ssr?: boolean;
@@ -30,6 +57,28 @@ export interface ApiOptions<T> {
30
57
  key?: string;
31
58
  }
32
59
 
60
+ // Batch-specific options (only available for batch operations)
61
+ interface BatchApiOptions {
62
+ /** Batch size for chunking large operations (default: no limit) - Only available for batch operations */
63
+ batchSize?: number;
64
+ /** Maximum concurrent requests (default: no limit) - Only available for batch operations */
65
+ concurrent?: number;
66
+ /** Real-time progress callback for batch operations - Only available for batch operations */
67
+ onProgress?: (progress: BatchProgress) => void;
68
+ }
69
+
70
+ // Conditional type that adds batch options only for batch-capable methods
71
+ type ConditionalBatchOptions<T> = T extends { method?: 'patch' | 'delete' | 'PATCH' | 'DELETE' }
72
+ ? BatchApiOptions
73
+ : T extends { method?: 'post' | 'POST' }
74
+ ? BatchApiOptions // POST supports file batch uploads
75
+ : T extends { method?: undefined } // Default method is 'get', but could be overridden at execution
76
+ ? Partial<BatchApiOptions> // Allow batch options but make them optional since method could change
77
+ : {};
78
+
79
+ // Main ApiOptions interface with conditional batch support
80
+ export type ApiOptions<T> = BaseApiOptions<T> & ConditionalBatchOptions<BaseApiOptions<T>>;
81
+
33
82
  export interface BackendError {
34
83
  success: false;
35
84
  message: string;
@@ -59,9 +108,14 @@ export interface UseEnfyraApiSSRReturn<T> extends AsyncData<T | null, ApiError>
59
108
  }
60
109
 
61
110
  // Execute options interface
62
- export interface ExecuteOptions {
111
+ // Base execute options available for all operations
112
+ interface BaseExecuteOptions {
63
113
  body?: any;
64
114
  id?: string | number;
115
+ }
116
+
117
+ // Batch execute options (only available when doing batch operations)
118
+ interface BatchExecuteOptions {
65
119
  ids?: (string | number)[];
66
120
  /** Array of FormData objects for batch upload */
67
121
  files?: FormData[];
@@ -69,8 +123,19 @@ export interface ExecuteOptions {
69
123
  batchSize?: number;
70
124
  /** Override concurrent limit for this specific execution */
71
125
  concurrent?: number;
126
+ /** Override progress callback for this specific execution */
127
+ onProgress?: (progress: BatchProgress) => void;
72
128
  }
73
129
 
130
+ // Conditional execute options based on what's being executed
131
+ type ConditionalExecuteOptions<T> = T extends { ids: any }
132
+ ? BatchExecuteOptions // If ids provided, enable all batch options
133
+ : T extends { files: any }
134
+ ? BatchExecuteOptions // If files provided, enable all batch options
135
+ : BaseExecuteOptions & Partial<BatchExecuteOptions>; // Otherwise base options + optional batch options
136
+
137
+ export type ExecuteOptions = BaseExecuteOptions & BatchExecuteOptions;
138
+
74
139
  // Client Mode return type
75
140
  export interface UseEnfyraApiClientReturn<T> {
76
141
  data: Ref<T | null>;
@@ -3,7 +3,6 @@ import type { EnfyraConfig } from '../types';
3
3
 
4
4
  const config = ref<EnfyraConfig>({
5
5
  apiUrl: '',
6
- apiPrefix: '/api',
7
6
  defaultHeaders: {}
8
7
  });
9
8
 
@@ -1,9 +1,10 @@
1
1
  import { H3Event, proxyRequest } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
+ import { ENFYRA_API_PREFIX } from "../../constants/config";
3
4
 
4
5
  export function proxyToAPI(event: H3Event, customPath?: string) {
5
6
  const config = useRuntimeConfig();
6
- const apiPrefix = config.public.enfyraSDK.apiPrefix;
7
+ const apiPrefix = config.public.enfyraSDK.apiPrefix || ENFYRA_API_PREFIX;
7
8
  const rawPath =
8
9
  customPath || event.path.replace(new RegExp(`^${apiPrefix}`), "");
9
10
  const targetUrl = `${config.public.enfyraSDK.apiUrl}${rawPath}`;