@arela/uploader 0.2.7 → 0.2.9

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.
@@ -109,7 +109,7 @@ export class DatabaseService {
109
109
  .toLowerCase()
110
110
  .replace('.', '');
111
111
  const filename = file.originalName || path.basename(file.path);
112
-
112
+
113
113
  const record = {
114
114
  document_type: null,
115
115
  size: stats.size,
@@ -121,8 +121,8 @@ export class DatabaseService {
121
121
  rfc: null,
122
122
  message: null,
123
123
  file_extension: fileExtension,
124
- is_like_simplificado: fileExtension === 'pdf' &&
125
- filename.toLowerCase().includes('simp'),
124
+ is_like_simplificado:
125
+ fileExtension === 'pdf' && filename.toLowerCase().includes('simp'),
126
126
  year: null,
127
127
  created_at: new Date().toISOString(),
128
128
  modified_at: stats.mtime.toISOString(),
@@ -226,8 +226,11 @@ export class DatabaseService {
226
226
  file_extension: fileExtension,
227
227
  created_at: new Date().toISOString(),
228
228
  modified_at: stats.mtime.toISOString(),
229
- is_like_simplificado: fileExtension === 'pdf' &&
230
- (file.originalName || path.basename(file.path)).toLowerCase().includes('simp'),
229
+ is_like_simplificado:
230
+ fileExtension === 'pdf' &&
231
+ (file.originalName || path.basename(file.path))
232
+ .toLowerCase()
233
+ .includes('simp'),
231
234
  year: null,
232
235
  };
233
236
 
@@ -277,9 +280,15 @@ export class DatabaseService {
277
280
  existingPaths.has(r.original_path),
278
281
  );
279
282
 
280
- logger.info(
281
- `Batch ${Math.floor(i / batchSize) + 1}: ${newRecords.length} new, ${updateRecords.length} updates`,
282
- );
283
+ // Only log every 10th batch to reduce noise
284
+ if (
285
+ (Math.floor(i / batchSize) + 1) % 10 === 0 ||
286
+ Math.floor(i / batchSize) + 1 === 1
287
+ ) {
288
+ logger.info(
289
+ `Batch ${Math.floor(i / batchSize) + 1}: ${newRecords.length} new, ${updateRecords.length} updates`,
290
+ );
291
+ }
283
292
 
284
293
  // Insert new records
285
294
  if (newRecords.length > 0) {
@@ -291,7 +300,7 @@ export class DatabaseService {
291
300
  logger.error(`Error inserting new records: ${insertError.message}`);
292
301
  } else {
293
302
  totalInserted += newRecords.length;
294
- logger.success(`Inserted ${newRecords.length} new records`);
303
+ // Only log the batch insertion, not the summary (which comes at the end)
295
304
  }
296
305
  }
297
306
 
@@ -316,7 +325,10 @@ export class DatabaseService {
316
325
  }
317
326
  }
318
327
  totalUpdated += batchUpdated;
319
- logger.info(`Updated ${batchUpdated} existing records`);
328
+ // Reduce logging noise - only log when there are updates
329
+ if (batchUpdated > 0) {
330
+ logger.info(`Updated ${batchUpdated} existing records`);
331
+ }
320
332
  }
321
333
  } catch (error) {
322
334
  logger.error(
@@ -545,7 +557,9 @@ export class DatabaseService {
545
557
  const supabase = await this.#getSupabaseClient();
546
558
 
547
559
  logger.info('Phase 3: Starting arela_path and year propagation process...');
548
- console.log('🔍 Finding pedimento_simplificado records with arela_path and year...');
560
+ console.log(
561
+ '🔍 Finding pedimento_simplificado records with arela_path and year...',
562
+ );
549
563
 
550
564
  // Get all pedimento_simplificado records that have arela_path
551
565
  const { data: pedimentoRecords, error: pedimentoError } = await supabase
@@ -601,32 +615,66 @@ export class DatabaseService {
601
615
  : existingPath + '/';
602
616
 
603
617
  // Find all files with the same base path that don't have arela_path yet
604
- const { data: relatedFiles, error: relatedError } = await supabase
605
- .from('uploader')
606
- .select('id, filename, original_path')
607
- .like('original_path', `${basePath}%`)
608
- .is('arela_path', null)
609
- .neq('id', pedimento.id); // Exclude the pedimento itself
618
+ // Use pagination to handle cases with more than 1000 related files
619
+ let allRelatedFiles = [];
620
+ let from = 0;
621
+ const pageSize = 1000;
622
+ let hasMoreData = true;
623
+ let paginationError = null;
610
624
 
611
- if (relatedError) {
612
- logger.error(
613
- `Error finding related files for ${pedimento.filename}: ${relatedError.message}`,
614
- );
615
- totalErrors++;
625
+ logger.info(`Searching for related files in base path: ${basePath}`);
626
+
627
+ while (hasMoreData) {
628
+ const { data: relatedFilesPage, error: relatedError } = await supabase
629
+ .from('uploader')
630
+ .select('id, filename, original_path')
631
+ .like('original_path', `${basePath}%`)
632
+ .is('arela_path', null)
633
+ .neq('id', pedimento.id) // Exclude the pedimento itself
634
+ .range(from, from + pageSize - 1);
635
+
636
+ if (relatedError) {
637
+ logger.error(
638
+ `Error finding related files for ${pedimento.filename}: ${relatedError.message}`,
639
+ );
640
+ paginationError = relatedError;
641
+ totalErrors++;
642
+ break;
643
+ }
644
+
645
+ if (relatedFilesPage && relatedFilesPage.length > 0) {
646
+ allRelatedFiles = allRelatedFiles.concat(relatedFilesPage);
647
+
648
+ // Check if we got a full page, indicating there might be more data
649
+ if (relatedFilesPage.length < pageSize) {
650
+ hasMoreData = false;
651
+ } else {
652
+ from += pageSize;
653
+ logger.info(
654
+ `Fetched ${relatedFilesPage.length} files, continuing pagination (total so far: ${allRelatedFiles.length})`,
655
+ );
656
+ }
657
+ } else {
658
+ hasMoreData = false;
659
+ }
660
+ }
661
+
662
+ // Continue with error handling if pagination failed
663
+ if (paginationError) {
616
664
  continue;
617
665
  }
618
666
 
619
- if (!relatedFiles || relatedFiles.length === 0) {
667
+ if (!allRelatedFiles || allRelatedFiles.length === 0) {
620
668
  logger.info(`No related files found for ${pedimento.filename}`);
621
669
  continue;
622
670
  }
623
671
 
624
672
  logger.info(
625
- `Found ${relatedFiles.length} related files to update for ${pedimento.filename}`,
673
+ `Found ${allRelatedFiles.length} related files to update for ${pedimento.filename}`,
626
674
  );
627
675
 
628
676
  // Process files in batches
629
- const fileIds = relatedFiles.map((f) => f.id);
677
+ const fileIds = allRelatedFiles.map((f) => f.id);
630
678
 
631
679
  for (let i = 0; i < fileIds.length; i += BATCH_SIZE) {
632
680
  const batchIds = fileIds.slice(i, i + BATCH_SIZE);
@@ -640,9 +688,9 @@ export class DatabaseService {
640
688
  try {
641
689
  const { error: updateError } = await supabase
642
690
  .from('uploader')
643
- .update({
691
+ .update({
644
692
  arela_path: folderArelaPath,
645
- year: pedimento.year
693
+ year: pedimento.year,
646
694
  })
647
695
  .in('id', batchIds);
648
696
 
@@ -885,133 +933,54 @@ export class DatabaseService {
885
933
  console.log(`📋 Total files to upload: ${allRelatedFiles.length}`);
886
934
  logger.info(`Total files to upload: ${allRelatedFiles.length}`);
887
935
 
888
- // Step 4: Upload all related files
936
+ // Step 4: Upload all related files using concurrent batch processing
889
937
  let totalProcessed = 0;
890
938
  let totalUploaded = 0;
891
939
  let totalErrors = 0;
892
940
  const batchSize = parseInt(options.batchSize) || 10;
893
941
 
894
- for (let i = 0; i < allRelatedFiles.length; i += batchSize) {
895
- const batch = allRelatedFiles.slice(i, i + batchSize);
896
-
897
- for (const file of batch) {
898
- try {
899
- totalProcessed++;
942
+ // Import performance configuration
943
+ const { performance: perfConfig } = appConfig;
944
+ const maxConcurrency = perfConfig?.maxApiConnections || 3;
900
945
 
901
- // Check if file exists
902
- if (!fs.existsSync(file.original_path)) {
903
- logger.warn(
904
- `File not found: ${file.filename} at ${file.original_path}`,
905
- );
906
- await supabase
907
- .from('uploader')
908
- .update({
909
- status: 'file-not-found',
910
- message: 'File no longer exists at original path',
911
- })
912
- .eq('id', file.id);
913
- totalErrors++;
914
- continue;
915
- }
916
-
917
- // Upload the file (handle both API and Supabase services)
918
- let uploadResult;
919
- if (uploadService.getServiceName() === 'Supabase') {
920
- // Supabase requires single file upload with uploadPath
921
- let uploadPath;
922
- if (options.folderStructure && file.arela_path) {
923
- // Combine folder structure with arela_path: palco/RFC/Year/Patente/Aduana/Pedimento/filename
924
- uploadPath = `uploads/${options.folderStructure}/${file.arela_path}${file.filename}`;
925
- } else if (file.arela_path) {
926
- // Use existing arela_path: RFC/Year/Patente/Aduana/Pedimento/filename
927
- uploadPath = `uploads/${file.arela_path}${file.filename}`;
928
- } else {
929
- // Fallback to RFC folder
930
- uploadPath = `uploads/${file.rfc}/${file.filename}`;
931
- }
932
-
933
- uploadResult = await uploadService.upload(
934
- [
935
- {
936
- path: file.original_path,
937
- name: file.filename,
938
- contentType: 'application/octet-stream',
939
- },
940
- ],
941
- {
942
- uploadPath: uploadPath,
943
- },
944
- );
945
- uploadResult = { success: true, data: uploadResult };
946
- } else {
947
- // API service supports batch uploads and returns normalized response
948
- let fullFolderStructure;
949
- if (options.folderStructure && file.arela_path) {
950
- // Combine folder structure with arela_path: palco/RFC/Year/Patente/Aduana/Pedimento/
951
- fullFolderStructure = `${options.folderStructure}/${file.arela_path}`;
952
- } else if (file.arela_path) {
953
- // Use existing arela_path: RFC/Year/Patente/Aduana/Pedimento/
954
- fullFolderStructure = file.arela_path;
955
- } else {
956
- // Fallback to RFC folder
957
- fullFolderStructure = `${file.rfc}/`;
958
- }
946
+ console.log(
947
+ `🚀 Starting batch upload: ${allRelatedFiles.length} files in batches of ${batchSize}`,
948
+ );
949
+ console.log(
950
+ `⚡ Concurrent processing: up to ${maxConcurrency} parallel operations`,
951
+ );
959
952
 
960
- uploadResult = await uploadService.upload(
961
- [
962
- {
963
- path: file.original_path,
964
- name: file.filename,
965
- contentType: 'application/octet-stream',
966
- },
967
- ],
968
- {
969
- folderStructure: fullFolderStructure,
970
- },
971
- );
972
- }
953
+ // Process files in batches with concurrent processing
954
+ for (let i = 0; i < allRelatedFiles.length; i += batchSize) {
955
+ const batch = allRelatedFiles.slice(i, i + batchSize);
956
+ const batchNum = Math.floor(i / batchSize) + 1;
957
+ const totalBatches = Math.ceil(allRelatedFiles.length / batchSize);
973
958
 
974
- if (uploadResult.success) {
975
- // Update database status
976
- await supabase
977
- .from('uploader')
978
- .update({
979
- status: 'file-uploaded',
980
- message: 'Successfully uploaded to Arela API',
981
- })
982
- .eq('id', file.id);
959
+ console.log(
960
+ `📦 Processing batch ${batchNum}/${totalBatches} (${batch.length} files)`,
961
+ );
983
962
 
984
- totalUploaded++;
985
- logger.info(`Uploaded: ${file.filename}`);
986
- } else {
987
- await supabase
988
- .from('uploader')
989
- .update({
990
- status: 'upload-error',
991
- message: uploadResult.error || 'Upload failed',
992
- })
993
- .eq('id', file.id);
963
+ // Process batch using concurrent processing similar to UploadCommand
964
+ const batchResults = await this.#processRfcBatch(
965
+ batch,
966
+ uploadService,
967
+ supabase,
968
+ options,
969
+ maxConcurrency,
970
+ );
994
971
 
995
- totalErrors++;
996
- logger.error(
997
- `Upload failed: ${file.filename} - ${uploadResult.error}`,
998
- );
999
- }
1000
- } catch (error) {
1001
- totalErrors++;
1002
- logger.error(
1003
- `Error processing file ${file.filename}: ${error.message}`,
1004
- );
972
+ totalProcessed += batchResults.processed;
973
+ totalUploaded += batchResults.uploaded;
974
+ totalErrors += batchResults.errors;
1005
975
 
1006
- await supabase
1007
- .from('uploader')
1008
- .update({
1009
- status: 'upload-error',
1010
- message: `Processing error: ${error.message}`,
1011
- })
1012
- .eq('id', file.id);
1013
- }
1014
- }
976
+ // Progress update
977
+ const progress = (
978
+ ((i + batch.length) / allRelatedFiles.length) *
979
+ 100
980
+ ).toFixed(1);
981
+ console.log(
982
+ `📊 Batch ${batchNum} complete - Progress: ${progress}% (${totalUploaded}/${allRelatedFiles.length} uploaded)`,
983
+ );
1015
984
  }
1016
985
 
1017
986
  const result = {
@@ -1106,22 +1075,44 @@ export class DatabaseService {
1106
1075
  for (let i = 0; i < uniqueArelaPaths.length; i += chunkSize) {
1107
1076
  const pathChunk = uniqueArelaPaths.slice(i, i + chunkSize);
1108
1077
 
1109
- const { data: chunkFiles, error: chunkError } = await supabase
1110
- .from('uploader')
1111
- .select(
1112
- 'id, original_path, arela_path, filename, rfc, document_type, status',
1113
- )
1114
- .in('arela_path', pathChunk)
1115
- .neq('status', 'file-uploaded')
1116
- .not('original_path', 'is', null);
1078
+ // Query with pagination to get all results (Supabase default limit is 1000)
1079
+ let chunkFiles = [];
1080
+ let from = 0;
1081
+ const pageSize = 1000;
1082
+ let hasMoreData = true;
1117
1083
 
1118
- if (chunkError) {
1119
- throw new Error(
1120
- `Error querying files for arela_paths chunk: ${chunkError.message}`,
1121
- );
1084
+ while (hasMoreData) {
1085
+ const { data: pageData, error: chunkError } = await supabase
1086
+ .from('uploader')
1087
+ .select(
1088
+ 'id, original_path, arela_path, filename, rfc, document_type, status',
1089
+ )
1090
+ .in('arela_path', pathChunk)
1091
+ .neq('status', 'file-uploaded')
1092
+ .not('original_path', 'is', null)
1093
+ .range(from, from + pageSize - 1);
1094
+
1095
+ if (chunkError) {
1096
+ throw new Error(
1097
+ `Error querying files for arela_paths chunk: ${chunkError.message}`,
1098
+ );
1099
+ }
1100
+
1101
+ if (pageData && pageData.length > 0) {
1102
+ chunkFiles = chunkFiles.concat(pageData);
1103
+
1104
+ // Check if we got a full page, indicating there might be more data
1105
+ if (pageData.length < pageSize) {
1106
+ hasMoreData = false;
1107
+ } else {
1108
+ from += pageSize;
1109
+ }
1110
+ } else {
1111
+ hasMoreData = false;
1112
+ }
1122
1113
  }
1123
1114
 
1124
- if (chunkFiles && chunkFiles.length > 0) {
1115
+ if (chunkFiles.length > 0) {
1125
1116
  allReadyFiles = allReadyFiles.concat(chunkFiles);
1126
1117
  }
1127
1118
  }
@@ -1158,6 +1149,482 @@ export class DatabaseService {
1158
1149
 
1159
1150
  return readyFiles || [];
1160
1151
  }
1152
+
1153
+ /**
1154
+ * Process a batch of files using concurrent processing for RFC uploads
1155
+ * @param {Array} files - Files to process in this batch
1156
+ * @param {Object} uploadService - Upload service instance
1157
+ * @param {Object} supabase - Supabase client
1158
+ * @param {Object} options - Upload options
1159
+ * @param {number} maxConcurrency - Maximum concurrent operations
1160
+ * @returns {Promise<Object>} Batch processing results
1161
+ */
1162
+ async #processRfcBatch(
1163
+ files,
1164
+ uploadService,
1165
+ supabase,
1166
+ options,
1167
+ maxConcurrency,
1168
+ ) {
1169
+ const fs = (await import('fs')).default;
1170
+
1171
+ let processed = 0;
1172
+ let uploaded = 0;
1173
+ let errors = 0;
1174
+
1175
+ // For Supabase, process files individually (required by service)
1176
+ if (uploadService.getServiceName() === 'Supabase') {
1177
+ // Process files in concurrent chunks within the batch
1178
+ const chunks = [];
1179
+ for (let i = 0; i < files.length; i += maxConcurrency) {
1180
+ chunks.push(files.slice(i, i + maxConcurrency));
1181
+ }
1182
+
1183
+ // Process each chunk concurrently
1184
+ for (const chunk of chunks) {
1185
+ const chunkPromises = chunk.map(async (file) => {
1186
+ return await this.#processRfcSingleFile(
1187
+ file,
1188
+ uploadService,
1189
+ supabase,
1190
+ options,
1191
+ fs,
1192
+ );
1193
+ });
1194
+
1195
+ // Wait for all files in this chunk to complete
1196
+ const chunkResults = await Promise.allSettled(chunkPromises);
1197
+
1198
+ // Count results
1199
+ for (const result of chunkResults) {
1200
+ processed++;
1201
+ if (result.status === 'fulfilled' && result.value.success) {
1202
+ uploaded++;
1203
+ } else {
1204
+ errors++;
1205
+ }
1206
+ }
1207
+ }
1208
+ } else {
1209
+ // For API service, use true batch processing (multiple files per API call)
1210
+ const apiChunks = [];
1211
+ const apiChunkSize = Math.min(
1212
+ 5,
1213
+ Math.ceil(files.length / maxConcurrency),
1214
+ ); // 5 files per API call, or distribute evenly
1215
+
1216
+ for (let i = 0; i < files.length; i += apiChunkSize) {
1217
+ apiChunks.push(files.slice(i, i + apiChunkSize));
1218
+ }
1219
+
1220
+ console.log(
1221
+ ` 🚀 Processing ${apiChunks.length} API calls with ${apiChunkSize} files each (max ${maxConcurrency} concurrent)`,
1222
+ );
1223
+
1224
+ // Process API chunks with controlled concurrency
1225
+ const concurrentChunks = [];
1226
+ for (let i = 0; i < apiChunks.length; i += maxConcurrency) {
1227
+ concurrentChunks.push(apiChunks.slice(i, i + maxConcurrency));
1228
+ }
1229
+
1230
+ for (const concurrentSet of concurrentChunks) {
1231
+ const batchPromises = concurrentSet.map(async (chunk) => {
1232
+ return await this.#processRfcApiBatch(
1233
+ chunk,
1234
+ uploadService,
1235
+ supabase,
1236
+ options,
1237
+ fs,
1238
+ );
1239
+ });
1240
+
1241
+ // Wait for all concurrent batches to complete
1242
+ const batchResults = await Promise.allSettled(batchPromises);
1243
+
1244
+ // Count results
1245
+ for (const result of batchResults) {
1246
+ if (result.status === 'fulfilled') {
1247
+ processed += result.value.processed;
1248
+ uploaded += result.value.uploaded;
1249
+ errors += result.value.errors;
1250
+ } else {
1251
+ errors += result.value?.processed || 0;
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ return { processed, uploaded, errors };
1258
+ }
1259
+
1260
+ /**
1261
+ * Process a single file for RFC upload (Supabase mode)
1262
+ */
1263
+ async #processRfcSingleFile(file, uploadService, supabase, options, fs) {
1264
+ try {
1265
+ // Check if file exists
1266
+ if (!fs.existsSync(file.original_path)) {
1267
+ logger.warn(
1268
+ `File not found: ${file.filename} at ${file.original_path}`,
1269
+ );
1270
+ await supabase
1271
+ .from('uploader')
1272
+ .update({
1273
+ status: 'file-not-found',
1274
+ message: 'File no longer exists at original path',
1275
+ })
1276
+ .eq('id', file.id);
1277
+ return { success: false, error: 'File not found' };
1278
+ }
1279
+
1280
+ // Supabase requires single file upload with uploadPath
1281
+ let uploadPath;
1282
+ if (options.folderStructure && file.arela_path) {
1283
+ uploadPath = `uploads/${options.folderStructure}/${file.arela_path}${file.filename}`;
1284
+ } else if (file.arela_path) {
1285
+ uploadPath = `uploads/${file.arela_path}${file.filename}`;
1286
+ } else {
1287
+ uploadPath = `uploads/${file.rfc}/${file.filename}`;
1288
+ }
1289
+
1290
+ const uploadResult = await uploadService.upload(
1291
+ [
1292
+ {
1293
+ path: file.original_path,
1294
+ name: file.filename,
1295
+ contentType: 'application/octet-stream',
1296
+ },
1297
+ ],
1298
+ { uploadPath: uploadPath },
1299
+ );
1300
+
1301
+ // Check upload result before updating database status
1302
+ if (uploadResult.success) {
1303
+ await supabase
1304
+ .from('uploader')
1305
+ .update({
1306
+ status: 'file-uploaded',
1307
+ message: 'Successfully uploaded to Supabase',
1308
+ })
1309
+ .eq('id', file.id);
1310
+
1311
+ logger.info(`✅ Uploaded: ${file.filename}`);
1312
+ return { success: true, filename: file.filename };
1313
+ } else {
1314
+ await supabase
1315
+ .from('uploader')
1316
+ .update({
1317
+ status: 'upload-error',
1318
+ message: `Upload failed: ${uploadResult.error}`,
1319
+ })
1320
+ .eq('id', file.id);
1321
+
1322
+ logger.error(
1323
+ `❌ Upload failed: ${file.filename} - ${uploadResult.error}`,
1324
+ );
1325
+ return {
1326
+ success: false,
1327
+ error: uploadResult.error,
1328
+ filename: file.filename,
1329
+ };
1330
+ }
1331
+ } catch (error) {
1332
+ logger.error(
1333
+ `❌ Error processing file ${file.filename}: ${error.message}`,
1334
+ );
1335
+
1336
+ await supabase
1337
+ .from('uploader')
1338
+ .update({
1339
+ status: 'upload-error',
1340
+ message: `Processing error: ${error.message}`,
1341
+ })
1342
+ .eq('id', file.id);
1343
+
1344
+ return { success: false, error: error.message, filename: file.filename };
1345
+ }
1346
+ }
1347
+
1348
+ /**
1349
+ * Process multiple files in a single API batch call (API service mode)
1350
+ */
1351
+ async #processRfcApiBatch(files, uploadService, supabase, options, fs) {
1352
+ let processed = 0;
1353
+ let uploaded = 0;
1354
+ let errors = 0;
1355
+
1356
+ try {
1357
+ // Prepare files for batch upload
1358
+ const validFiles = [];
1359
+ const invalidFiles = [];
1360
+
1361
+ for (const file of files) {
1362
+ processed++;
1363
+
1364
+ if (!fs.existsSync(file.original_path)) {
1365
+ logger.warn(
1366
+ `File not found: ${file.filename} at ${file.original_path}`,
1367
+ );
1368
+ invalidFiles.push(file);
1369
+ continue;
1370
+ }
1371
+
1372
+ validFiles.push({
1373
+ fileData: {
1374
+ path: file.original_path,
1375
+ name: file.filename,
1376
+ contentType: 'application/octet-stream',
1377
+ },
1378
+ dbRecord: file,
1379
+ });
1380
+ }
1381
+
1382
+ // Update invalid files in database
1383
+ for (const file of invalidFiles) {
1384
+ await supabase
1385
+ .from('uploader')
1386
+ .update({
1387
+ status: 'file-not-found',
1388
+ message: 'File no longer exists at original path',
1389
+ })
1390
+ .eq('id', file.id);
1391
+ errors++;
1392
+ }
1393
+
1394
+ // Process valid files in batch if any exist
1395
+ if (validFiles.length > 0) {
1396
+ // Determine folder structure (all files in this batch should have same arela_path)
1397
+ const sampleFile = validFiles[0].dbRecord;
1398
+ let fullFolderStructure;
1399
+ if (options.folderStructure && sampleFile.arela_path) {
1400
+ fullFolderStructure = `${options.folderStructure}/${sampleFile.arela_path}`;
1401
+ } else if (sampleFile.arela_path) {
1402
+ fullFolderStructure = sampleFile.arela_path;
1403
+ } else {
1404
+ fullFolderStructure = `${sampleFile.rfc}/`;
1405
+ }
1406
+
1407
+ // Make single API call with multiple files
1408
+ const uploadResult = await uploadService.upload(
1409
+ validFiles.map((f) => f.fileData),
1410
+ { folderStructure: fullFolderStructure },
1411
+ );
1412
+
1413
+ if (uploadResult.success && uploadResult.data) {
1414
+ const apiResult = uploadResult.data;
1415
+
1416
+ logger.info(
1417
+ `📋 Processing API response: ${apiResult.uploaded?.length || 0} uploaded, ${apiResult.errors?.length || 0} errors`,
1418
+ );
1419
+
1420
+ // Debug logging to understand API response structure
1421
+ logger.debug(
1422
+ `🔍 API Response structure: ${JSON.stringify(apiResult, null, 2)}`,
1423
+ );
1424
+ if (apiResult.uploaded && apiResult.uploaded.length > 0) {
1425
+ logger.debug(
1426
+ `🔍 First uploaded file structure: ${JSON.stringify(apiResult.uploaded[0], null, 2)}`,
1427
+ );
1428
+ }
1429
+
1430
+ // Create filename to file mapping for quick lookup
1431
+ const fileNameToRecord = new Map();
1432
+ validFiles.forEach((f) => {
1433
+ fileNameToRecord.set(f.fileData.name, f.dbRecord);
1434
+ });
1435
+
1436
+ // Debug: Log expected filenames
1437
+ logger.debug(
1438
+ `🔍 Expected filenames: ${Array.from(fileNameToRecord.keys()).join(', ')}`,
1439
+ );
1440
+
1441
+ // Handle successfully uploaded files
1442
+ if (apiResult.uploaded && apiResult.uploaded.length > 0) {
1443
+ const successfulFileIds = [];
1444
+ const matchedFilenames = [];
1445
+
1446
+ apiResult.uploaded.forEach((uploadedFile) => {
1447
+ // Try multiple possible property names for filename
1448
+ const possibleFilename =
1449
+ uploadedFile.fileName ||
1450
+ uploadedFile.filename ||
1451
+ uploadedFile.name ||
1452
+ uploadedFile.file_name ||
1453
+ uploadedFile.originalName ||
1454
+ uploadedFile.original_name;
1455
+
1456
+ logger.debug(
1457
+ `🔍 Trying to match uploaded file: ${JSON.stringify(uploadedFile)}`,
1458
+ );
1459
+
1460
+ const dbRecord = fileNameToRecord.get(possibleFilename);
1461
+ if (dbRecord) {
1462
+ successfulFileIds.push(dbRecord.id);
1463
+ matchedFilenames.push(possibleFilename);
1464
+ logger.debug(`✅ Matched file: ${possibleFilename}`);
1465
+ } else {
1466
+ logger.warn(
1467
+ `⚠️ Could not match uploaded file with any known filename: ${JSON.stringify(uploadedFile)}`,
1468
+ );
1469
+ }
1470
+ });
1471
+
1472
+ // If no individual files matched but API indicates success, use fallback
1473
+ if (
1474
+ successfulFileIds.length === 0 &&
1475
+ apiResult.uploaded.length > 0
1476
+ ) {
1477
+ logger.warn(
1478
+ `🔄 Fallback: No individual file matches found, but API indicates ${apiResult.uploaded.length} uploads. Marking all ${validFiles.length} batch files as uploaded.`,
1479
+ );
1480
+ validFiles.forEach((f) => successfulFileIds.push(f.dbRecord.id));
1481
+ }
1482
+
1483
+ if (successfulFileIds.length > 0) {
1484
+ await supabase
1485
+ .from('uploader')
1486
+ .update({
1487
+ status: 'file-uploaded',
1488
+ message: 'Successfully uploaded to Arela API (batch)',
1489
+ })
1490
+ .in('id', successfulFileIds);
1491
+
1492
+ uploaded += successfulFileIds.length;
1493
+ logger.info(
1494
+ `✅ Batch upload successful: ${successfulFileIds.length} files uploaded`,
1495
+ );
1496
+ }
1497
+ }
1498
+
1499
+ // Handle failed files
1500
+ if (apiResult.errors && apiResult.errors.length > 0) {
1501
+ const failedFileIds = [];
1502
+
1503
+ apiResult.errors.forEach((errorInfo) => {
1504
+ // Try multiple possible property names for filename in errors
1505
+ const possibleFilename =
1506
+ errorInfo.fileName ||
1507
+ errorInfo.filename ||
1508
+ errorInfo.name ||
1509
+ errorInfo.file_name ||
1510
+ errorInfo.originalName ||
1511
+ errorInfo.original_name;
1512
+
1513
+ const dbRecord = fileNameToRecord.get(possibleFilename);
1514
+ if (dbRecord) {
1515
+ failedFileIds.push(dbRecord.id);
1516
+ } else {
1517
+ logger.warn(
1518
+ `⚠️ Could not match error file: ${JSON.stringify(errorInfo)}`,
1519
+ );
1520
+ }
1521
+ });
1522
+
1523
+ if (failedFileIds.length > 0) {
1524
+ await supabase
1525
+ .from('uploader')
1526
+ .update({
1527
+ status: 'upload-error',
1528
+ message: `Upload failed: ${apiResult.errors[0].error}`,
1529
+ })
1530
+ .in('id', failedFileIds);
1531
+
1532
+ errors += failedFileIds.length;
1533
+ logger.error(
1534
+ `❌ Batch upload errors: ${failedFileIds.length} files failed`,
1535
+ );
1536
+ }
1537
+ }
1538
+
1539
+ // Handle any remaining files that weren't in uploaded or errors arrays
1540
+ // Use robust filename extraction for both uploaded and error files
1541
+ const extractFilename = (fileObj) => {
1542
+ return (
1543
+ fileObj.fileName ||
1544
+ fileObj.filename ||
1545
+ fileObj.name ||
1546
+ fileObj.file_name ||
1547
+ fileObj.originalName ||
1548
+ fileObj.original_name
1549
+ );
1550
+ };
1551
+
1552
+ const processedFileNames = new Set([
1553
+ ...(apiResult.uploaded || []).map(extractFilename).filter(Boolean),
1554
+ ...(apiResult.errors || []).map(extractFilename).filter(Boolean),
1555
+ ]);
1556
+
1557
+ const unprocessedFiles = validFiles.filter(
1558
+ (f) => !processedFileNames.has(f.fileData.name),
1559
+ );
1560
+ if (unprocessedFiles.length > 0) {
1561
+ // Only mark as unprocessed if we haven't already handled all files through fallback logic
1562
+ // If we used fallback (all files marked as uploaded), don't mark any as unprocessed
1563
+ const alreadyHandledCount = uploaded + errors;
1564
+ const shouldMarkUnprocessed =
1565
+ alreadyHandledCount < validFiles.length;
1566
+
1567
+ if (shouldMarkUnprocessed) {
1568
+ const unprocessedIds = unprocessedFiles.map((f) => f.dbRecord.id);
1569
+ await supabase
1570
+ .from('uploader')
1571
+ .update({
1572
+ status: 'upload-error',
1573
+ message: 'File not found in API response',
1574
+ })
1575
+ .in('id', unprocessedIds);
1576
+
1577
+ errors += unprocessedIds.length;
1578
+ logger.warn(
1579
+ `⚠️ Unprocessed files: ${unprocessedIds.length} files not found in API response`,
1580
+ );
1581
+ logger.debug(
1582
+ `🔍 API response uploaded array: ${JSON.stringify(apiResult.uploaded)}`,
1583
+ );
1584
+ logger.debug(
1585
+ `🔍 Expected filenames: ${validFiles.map((f) => f.fileData.name).join(', ')}`,
1586
+ );
1587
+ } else {
1588
+ logger.debug(
1589
+ `✅ All files already handled (uploaded: ${uploaded}, errors: ${errors}), skipping unprocessed marking`,
1590
+ );
1591
+ }
1592
+ }
1593
+ } else {
1594
+ // Complete batch failure - mark all files as failed
1595
+ const fileIds = validFiles.map((f) => f.dbRecord.id);
1596
+ await supabase
1597
+ .from('uploader')
1598
+ .update({
1599
+ status: 'upload-error',
1600
+ message: uploadResult.error || 'Batch upload failed',
1601
+ })
1602
+ .in('id', fileIds);
1603
+
1604
+ errors += validFiles.length;
1605
+ logger.error(
1606
+ `❌ Complete batch failure: ${validFiles.length} files - ${uploadResult.error}`,
1607
+ );
1608
+ }
1609
+ }
1610
+ } catch (error) {
1611
+ logger.error(`❌ Error processing batch: ${error.message}`);
1612
+
1613
+ // Mark all files as failed
1614
+ const fileIds = files.map((f) => f.id);
1615
+ await supabase
1616
+ .from('uploader')
1617
+ .update({
1618
+ status: 'upload-error',
1619
+ message: `Batch processing error: ${error.message}`,
1620
+ })
1621
+ .in('id', fileIds);
1622
+
1623
+ errors += files.length;
1624
+ }
1625
+
1626
+ return { processed, uploaded, errors };
1627
+ }
1161
1628
  }
1162
1629
 
1163
1630
  // Export singleton instance