@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.
@@ -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
- // Step 1: Run stats collection
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
- await this.#executePropagateArelaPath();
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')).default;
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(`[AutoProcessingService] Executing stats collection for watch mode`);
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('filePath and watchDir are required for stats collection');
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(`File not yet ready (attempt ${attempts}/${maxAttempts}), waiting...`);
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(`Error reading file stats (attempt ${attempts}/${maxAttempts}): ${error.message}, retrying...`);
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(`File not found after ${maxAttempts} retries: ${filePath}`);
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 (relatedFilePath === filePath || relatedFilePath.endsWith('.DS_Store')) {
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(`Could not read stats for related file: ${relatedFilePath}`);
321
+ logger.debug(
322
+ `Could not read stats for related file: ${relatedFilePath}`,
323
+ );
275
324
  }
276
325
  }
277
326
 
278
- logger.debug(`📊 Found ${fileObjects.length} file(s) to process (1 detected + ${fileObjects.length - 1} related)`);
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
- const result = await databaseService.detectPedimentosInDatabase({
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
- const result = await databaseService.propagateArelaPath({
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
- `${this.processingTimeoutDuration}ms`,
726
+ `${this.processingTimeoutDuration}ms`,
617
727
  );
618
728
  this.isProcessing = false;
619
729
  this.lastProcessedFile = null;