@arela/uploader 0.3.0 → 1.0.0
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 +130 -5
- package/docs/API_ENDPOINTS_FOR_DETECTION.md +647 -0
- package/docs/QUICK_REFERENCE_API_DETECTION.md +264 -0
- package/docs/REFACTORING_SUMMARY_DETECT_PEDIMENTOS.md +200 -0
- package/package.json +1 -1
- package/src/commands/WatchCommand.js +47 -10
- package/src/config/config.js +157 -2
- package/src/document-types/support-document.js +4 -5
- package/src/file-detection.js +7 -0
- package/src/index.js +119 -4
- package/src/services/AutoProcessingService.js +146 -36
- package/src/services/DatabaseService.js +341 -517
- package/src/services/upload/ApiUploadService.js +426 -4
- package/src/services/upload/MultiApiUploadService.js +233 -0
- package/src/services/upload/UploadServiceFactory.js +24 -0
- package/src/utils/FileOperations.js +6 -3
- package/src/utils/WatchEventHandler.js +14 -9
- package/.envbackup +0 -37
- package/SUPABASE_UPLOAD_FIX.md +0 -157
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { appConfig } from '../config/config.js';
|
|
1
2
|
import logger from './LoggingService.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* AutoProcessingService - Handles automatic processing workflow for newly detected files
|
|
5
6
|
* Executes the 4-step processing pipeline:
|
|
6
|
-
* 1. Stats collection (stats --stats-only)
|
|
7
|
-
* 2. PDF/Pedimento detection (detect --detect-pdfs)
|
|
8
|
-
* 3. Arela path propagation (detect --propagate-arela-path)
|
|
9
|
-
* 4. RFC-based upload with folder structure (upload --upload-by-rfc --folder-structure)
|
|
7
|
+
* 1. Stats collection (stats --stats-only) - uses sourceApi in cross-tenant mode
|
|
8
|
+
* 2. PDF/Pedimento detection (detect --detect-pdfs) - uses sourceApi in cross-tenant mode
|
|
9
|
+
* 3. Arela path propagation (detect --propagate-arela-path) - uses sourceApi in cross-tenant mode
|
|
10
|
+
* 4. RFC-based upload with folder structure (upload --upload-by-rfc --folder-structure) - uses targetApi in cross-tenant mode
|
|
11
|
+
*
|
|
12
|
+
* In cross-tenant mode (--source-api and --target-api):
|
|
13
|
+
* - Phases 1-3 write/update to the SOURCE API
|
|
14
|
+
* - Phase 4 uploads files to the TARGET API
|
|
15
|
+
*
|
|
16
|
+
* In single API mode (--api):
|
|
17
|
+
* - All phases use the same API
|
|
10
18
|
*/
|
|
11
19
|
export class AutoProcessingService {
|
|
12
20
|
constructor() {
|
|
@@ -54,7 +62,7 @@ export class AutoProcessingService {
|
|
|
54
62
|
pipelineId,
|
|
55
63
|
};
|
|
56
64
|
}
|
|
57
|
-
|
|
65
|
+
|
|
58
66
|
logger.warn(
|
|
59
67
|
'⚠️ Processing pipeline already running, skipping new request',
|
|
60
68
|
);
|
|
@@ -67,7 +75,7 @@ export class AutoProcessingService {
|
|
|
67
75
|
|
|
68
76
|
this.isProcessing = true;
|
|
69
77
|
this.lastProcessedFile = filePath;
|
|
70
|
-
|
|
78
|
+
|
|
71
79
|
// Set automatic timeout to reset processing flag (fail-safe)
|
|
72
80
|
this.#setProcessingTimeout();
|
|
73
81
|
const results = {
|
|
@@ -86,12 +94,33 @@ export class AutoProcessingService {
|
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
try {
|
|
89
|
-
//
|
|
97
|
+
// Determine API target for phases 1-3 (write operations)
|
|
98
|
+
// In cross-tenant mode: use sourceApi for phases 1-3
|
|
99
|
+
// In single API mode: use activeTarget
|
|
100
|
+
const isCrossTenant = appConfig.isCrossTenantMode();
|
|
101
|
+
const sourceApiTarget = isCrossTenant
|
|
102
|
+
? appConfig.api.sourceTarget
|
|
103
|
+
: appConfig.api.activeTarget !== 'default'
|
|
104
|
+
? appConfig.api.activeTarget
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
if (isCrossTenant) {
|
|
108
|
+
logger.debug(
|
|
109
|
+
`🔀 Cross-tenant mode: phases 1-3 will use ${appConfig.api.sourceTarget}, phase 4 will use ${appConfig.api.targetTarget}`,
|
|
110
|
+
);
|
|
111
|
+
} else if (sourceApiTarget) {
|
|
112
|
+
logger.debug(
|
|
113
|
+
`🎯 Single API mode: all phases will use ${sourceApiTarget}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Step 1: Run stats collection (uses sourceApi in cross-tenant mode)
|
|
90
118
|
logger.debug(`📊 [Step 1/4] Stats collection...`);
|
|
91
119
|
results.steps.statsOnly = await this.#executeStatsOnly({
|
|
92
120
|
filePath,
|
|
93
121
|
watchDir,
|
|
94
122
|
batchSize,
|
|
123
|
+
apiTarget: sourceApiTarget,
|
|
95
124
|
});
|
|
96
125
|
|
|
97
126
|
if (results.steps.statsOnly.status === 'failed') {
|
|
@@ -103,10 +132,11 @@ export class AutoProcessingService {
|
|
|
103
132
|
return results;
|
|
104
133
|
}
|
|
105
134
|
|
|
106
|
-
// Step 2: Run PDF detection
|
|
135
|
+
// Step 2: Run PDF detection (uses sourceApi in cross-tenant mode)
|
|
107
136
|
logger.debug(`🔍 [Step 2/4] PDF detection...`);
|
|
108
137
|
results.steps.detectPdfs = await this.#executeDetectPdfs({
|
|
109
138
|
batchSize,
|
|
139
|
+
apiTarget: sourceApiTarget,
|
|
110
140
|
});
|
|
111
141
|
|
|
112
142
|
if (results.steps.detectPdfs.status === 'failed') {
|
|
@@ -122,10 +152,11 @@ export class AutoProcessingService {
|
|
|
122
152
|
return results;
|
|
123
153
|
}
|
|
124
154
|
|
|
125
|
-
// Step 3: Propagate Arela path
|
|
155
|
+
// Step 3: Propagate Arela path (uses sourceApi in cross-tenant mode)
|
|
126
156
|
logger.debug(`🔄 [Step 3/4] Arela path propagation...`);
|
|
127
|
-
results.steps.propagateArelaPath =
|
|
128
|
-
|
|
157
|
+
results.steps.propagateArelaPath = await this.#executePropagateArelaPath({
|
|
158
|
+
apiTarget: sourceApiTarget,
|
|
159
|
+
});
|
|
129
160
|
|
|
130
161
|
if (results.steps.propagateArelaPath.status === 'failed') {
|
|
131
162
|
logger.debug(
|
|
@@ -140,6 +171,8 @@ export class AutoProcessingService {
|
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
// Step 4: Upload by RFC with folder structure
|
|
174
|
+
// In cross-tenant mode: uses sourceApi for reading, targetApi for uploading
|
|
175
|
+
// In single API mode: uses the same API for both
|
|
143
176
|
logger.debug(`📤 [Step 4/4] RFC upload...`);
|
|
144
177
|
results.steps.uploadByRfc = await this.#executeUploadByRfc({
|
|
145
178
|
batchSize,
|
|
@@ -191,17 +224,22 @@ export class AutoProcessingService {
|
|
|
191
224
|
// Import DatabaseService to collect stats directly
|
|
192
225
|
// This bypasses UploadCommand which has watch mode restrictions
|
|
193
226
|
const databaseService = (await import('./DatabaseService.js')).default;
|
|
194
|
-
const FileOperations = (await import('../utils/FileOperations.js'))
|
|
227
|
+
const FileOperations = (await import('../utils/FileOperations.js'))
|
|
228
|
+
.default;
|
|
195
229
|
const fs = (await import('fs')).default;
|
|
196
230
|
const path = (await import('path')).default;
|
|
197
231
|
|
|
198
|
-
logger.debug(
|
|
232
|
+
logger.debug(
|
|
233
|
+
`[AutoProcessingService] Executing stats collection for watch mode`,
|
|
234
|
+
);
|
|
199
235
|
|
|
200
236
|
// Get files from the watch directory
|
|
201
237
|
const { filePath, watchDir } = options;
|
|
202
|
-
|
|
238
|
+
|
|
203
239
|
if (!filePath || !watchDir) {
|
|
204
|
-
throw new Error(
|
|
240
|
+
throw new Error(
|
|
241
|
+
'filePath and watchDir are required for stats collection',
|
|
242
|
+
);
|
|
205
243
|
}
|
|
206
244
|
|
|
207
245
|
// Wait for file to be fully written (with retries)
|
|
@@ -217,14 +255,18 @@ export class AutoProcessingService {
|
|
|
217
255
|
} else {
|
|
218
256
|
attempts++;
|
|
219
257
|
if (attempts < maxAttempts) {
|
|
220
|
-
logger.debug(
|
|
258
|
+
logger.debug(
|
|
259
|
+
`File not yet ready (attempt ${attempts}/${maxAttempts}), waiting...`,
|
|
260
|
+
);
|
|
221
261
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
222
262
|
}
|
|
223
263
|
}
|
|
224
264
|
} catch (error) {
|
|
225
265
|
attempts++;
|
|
226
266
|
if (attempts < maxAttempts) {
|
|
227
|
-
logger.debug(
|
|
267
|
+
logger.debug(
|
|
268
|
+
`Error reading file stats (attempt ${attempts}/${maxAttempts}): ${error.message}, retrying...`,
|
|
269
|
+
);
|
|
228
270
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
229
271
|
} else {
|
|
230
272
|
throw error;
|
|
@@ -233,7 +275,9 @@ export class AutoProcessingService {
|
|
|
233
275
|
}
|
|
234
276
|
|
|
235
277
|
if (!fileStats) {
|
|
236
|
-
throw new Error(
|
|
278
|
+
throw new Error(
|
|
279
|
+
`File not found after ${maxAttempts} retries: ${filePath}`,
|
|
280
|
+
);
|
|
237
281
|
}
|
|
238
282
|
|
|
239
283
|
// Get the parent directory of the detected file
|
|
@@ -257,7 +301,10 @@ export class AutoProcessingService {
|
|
|
257
301
|
// Add all related files (except the one already detected)
|
|
258
302
|
for (const relatedFilePath of relatedFiles) {
|
|
259
303
|
// Skip the file already detected and .DS_Store
|
|
260
|
-
if (
|
|
304
|
+
if (
|
|
305
|
+
relatedFilePath === filePath ||
|
|
306
|
+
relatedFilePath.endsWith('.DS_Store')
|
|
307
|
+
) {
|
|
261
308
|
continue;
|
|
262
309
|
}
|
|
263
310
|
|
|
@@ -271,18 +318,32 @@ export class AutoProcessingService {
|
|
|
271
318
|
});
|
|
272
319
|
}
|
|
273
320
|
} catch (error) {
|
|
274
|
-
logger.debug(
|
|
321
|
+
logger.debug(
|
|
322
|
+
`Could not read stats for related file: ${relatedFilePath}`,
|
|
323
|
+
);
|
|
275
324
|
}
|
|
276
325
|
}
|
|
277
326
|
|
|
278
|
-
logger.debug(
|
|
327
|
+
logger.debug(
|
|
328
|
+
`📊 Found ${fileObjects.length} file(s) to process (1 detected + ${fileObjects.length - 1} related)`,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Insert into database directly, using the specified API target
|
|
332
|
+
const insertOptions = {
|
|
333
|
+
quietMode: true, // Suppress verbose logging when in auto-pipeline
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Pass apiTarget if specified (for cross-tenant or single API mode)
|
|
337
|
+
if (options.apiTarget) {
|
|
338
|
+
insertOptions.apiTarget = options.apiTarget;
|
|
339
|
+
logger.debug(
|
|
340
|
+
`[AutoProcessingService] Stats using API target: ${options.apiTarget}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
279
343
|
|
|
280
|
-
// Insert into database directly
|
|
281
344
|
const result = await databaseService.insertStatsOnlyToUploaderTable(
|
|
282
345
|
fileObjects,
|
|
283
|
-
|
|
284
|
-
quietMode: true, // Suppress verbose logging when in auto-pipeline
|
|
285
|
-
},
|
|
346
|
+
insertOptions,
|
|
286
347
|
);
|
|
287
348
|
|
|
288
349
|
const duration = Date.now() - stepStartTime;
|
|
@@ -314,6 +375,7 @@ export class AutoProcessingService {
|
|
|
314
375
|
* Execute PDF detection (Step 2)
|
|
315
376
|
* @private
|
|
316
377
|
* @param {Object} options - Options for PDF detection
|
|
378
|
+
* @param {string} options.apiTarget - API target to use (for cross-tenant or single API mode)
|
|
317
379
|
* @returns {Promise<Object>} Result of PDF detection
|
|
318
380
|
*/
|
|
319
381
|
async #executeDetectPdfs(options = {}) {
|
|
@@ -326,9 +388,20 @@ export class AutoProcessingService {
|
|
|
326
388
|
`[AutoProcessingService] Executing PDF detection with batch size: ${options.batchSize}`,
|
|
327
389
|
);
|
|
328
390
|
|
|
329
|
-
|
|
391
|
+
// Pass apiTarget if specified
|
|
392
|
+
const detectOptions = {
|
|
330
393
|
batchSize: options.batchSize || 10,
|
|
331
|
-
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (options.apiTarget) {
|
|
397
|
+
detectOptions.apiTarget = options.apiTarget;
|
|
398
|
+
logger.debug(
|
|
399
|
+
`[AutoProcessingService] PDF detection using API target: ${options.apiTarget}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const result =
|
|
404
|
+
await databaseService.detectPedimentosInDatabase(detectOptions);
|
|
332
405
|
|
|
333
406
|
const duration = Date.now() - stepStartTime;
|
|
334
407
|
logger.info(`✅ PDF detection completed in ${duration}ms`);
|
|
@@ -358,9 +431,11 @@ export class AutoProcessingService {
|
|
|
358
431
|
/**
|
|
359
432
|
* Execute arela_path propagation (Step 3)
|
|
360
433
|
* @private
|
|
434
|
+
* @param {Object} options - Options for propagation
|
|
435
|
+
* @param {string} options.apiTarget - API target to use (for cross-tenant or single API mode)
|
|
361
436
|
* @returns {Promise<Object>} Result of arela_path propagation
|
|
362
437
|
*/
|
|
363
|
-
async #executePropagateArelaPath() {
|
|
438
|
+
async #executePropagateArelaPath(options = {}) {
|
|
364
439
|
let stepStartTime = Date.now();
|
|
365
440
|
try {
|
|
366
441
|
// Import databaseService singleton instance
|
|
@@ -368,9 +443,19 @@ export class AutoProcessingService {
|
|
|
368
443
|
|
|
369
444
|
logger.debug(`[AutoProcessingService] Executing arela_path propagation`);
|
|
370
445
|
|
|
371
|
-
|
|
446
|
+
// Pass apiTarget if specified
|
|
447
|
+
const propagateOptions = {
|
|
372
448
|
showProgress: true,
|
|
373
|
-
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
if (options.apiTarget) {
|
|
452
|
+
propagateOptions.apiTarget = options.apiTarget;
|
|
453
|
+
logger.debug(
|
|
454
|
+
`[AutoProcessingService] Propagation using API target: ${options.apiTarget}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const result = await databaseService.propagateArelaPath(propagateOptions);
|
|
374
459
|
|
|
375
460
|
const duration = Date.now() - stepStartTime;
|
|
376
461
|
logger.info(`✅ Arela path propagation completed in ${duration}ms`);
|
|
@@ -411,19 +496,44 @@ export class AutoProcessingService {
|
|
|
411
496
|
// Import databaseService singleton instance
|
|
412
497
|
const databaseService = (await import('./DatabaseService.js')).default;
|
|
413
498
|
|
|
499
|
+
// Build upload options, including cross-tenant config if set globally
|
|
500
|
+
const uploadOptions = {
|
|
501
|
+
batchSize: options.batchSize || 10,
|
|
502
|
+
showProgress: true,
|
|
503
|
+
folderStructure: options.folderStructure,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Check if cross-tenant mode is enabled globally and pass it to uploadFilesByRfc
|
|
507
|
+
if (appConfig.isCrossTenantMode()) {
|
|
508
|
+
// Cross-tenant mode: source API for reading, target API for uploading
|
|
509
|
+
uploadOptions.sourceApi = appConfig.api.sourceTarget;
|
|
510
|
+
uploadOptions.targetApi = appConfig.api.targetTarget;
|
|
511
|
+
logger.debug(
|
|
512
|
+
`[AutoProcessingService] Cross-tenant upload: source=${uploadOptions.sourceApi}, target=${uploadOptions.targetApi}`,
|
|
513
|
+
);
|
|
514
|
+
} else if (
|
|
515
|
+
appConfig.api.activeTarget &&
|
|
516
|
+
appConfig.api.activeTarget !== 'default'
|
|
517
|
+
) {
|
|
518
|
+
// Single API mode: use the same API for both reading and uploading
|
|
519
|
+
uploadOptions.apiTarget = appConfig.api.activeTarget;
|
|
520
|
+
logger.debug(
|
|
521
|
+
`[AutoProcessingService] Single API upload: target=${uploadOptions.apiTarget}`,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
414
525
|
logger.debug(
|
|
415
526
|
`[AutoProcessingService] Executing RFC-based upload with options:`,
|
|
416
527
|
{
|
|
417
528
|
folderStructure: options.folderStructure,
|
|
418
529
|
batchSize: options.batchSize,
|
|
530
|
+
sourceApi: uploadOptions.sourceApi,
|
|
531
|
+
targetApi: uploadOptions.targetApi,
|
|
532
|
+
apiTarget: uploadOptions.apiTarget,
|
|
419
533
|
},
|
|
420
534
|
);
|
|
421
535
|
|
|
422
|
-
const result = await databaseService.uploadFilesByRfc(
|
|
423
|
-
batchSize: options.batchSize || 10,
|
|
424
|
-
showProgress: true,
|
|
425
|
-
folderStructure: options.folderStructure,
|
|
426
|
-
});
|
|
536
|
+
const result = await databaseService.uploadFilesByRfc(uploadOptions);
|
|
427
537
|
|
|
428
538
|
const duration = Date.now() - stepStartTime;
|
|
429
539
|
logger.info(`✅ RFC upload completed in ${duration}ms`);
|
|
@@ -613,7 +723,7 @@ export class AutoProcessingService {
|
|
|
613
723
|
if (this.isProcessing) {
|
|
614
724
|
logger.warn(
|
|
615
725
|
'⚠️ Processing pipeline timeout - forcing reset after ' +
|
|
616
|
-
|
|
726
|
+
`${this.processingTimeoutDuration}ms`,
|
|
617
727
|
);
|
|
618
728
|
this.isProcessing = false;
|
|
619
729
|
this.lastProcessedFile = null;
|