@arela/uploader 0.2.8 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arela/uploader",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "CLI to upload files/directories to Arela",
5
5
  "bin": {
6
6
  "arela": "./src/index.js"
@@ -28,10 +28,10 @@ class Config {
28
28
  const __dirname = path.dirname(__filename);
29
29
  const packageJsonPath = path.resolve(__dirname, '../../package.json');
30
30
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
31
- return packageJson.version || '0.2.8';
31
+ return packageJson.version || '0.2.10';
32
32
  } catch (error) {
33
33
  console.warn('⚠️ Could not read package.json version, using fallback');
34
- return '0.2.8';
34
+ return '0.2.10';
35
35
  }
36
36
  }
37
37
 
@@ -72,10 +72,15 @@ class Config {
72
72
  .map((s) => s.trim())
73
73
  .filter(Boolean);
74
74
 
75
+ const uploadYears = process.env.UPLOAD_YEARS?.split('|')
76
+ .map((s) => s.trim())
77
+ .filter(Boolean);
78
+
75
79
  return {
76
80
  basePath,
77
81
  sources,
78
82
  rfcs: uploadRfcs,
83
+ years: uploadYears,
79
84
  };
80
85
  }
81
86
 
@@ -557,135 +557,226 @@ export class DatabaseService {
557
557
  const supabase = await this.#getSupabaseClient();
558
558
 
559
559
  logger.info('Phase 3: Starting arela_path and year propagation process...');
560
- console.log(
561
- '🔍 Finding pedimento_simplificado records with arela_path and year...',
562
- );
563
-
564
- // Get all pedimento_simplificado records that have arela_path
565
- const { data: pedimentoRecords, error: pedimentoError } = await supabase
566
- .from('uploader')
567
- .select('id, original_path, arela_path, filename, year')
568
- .eq('document_type', 'pedimento_simplificado')
569
- .not('arela_path', 'is', null);
570
-
571
- if (pedimentoError) {
572
- const errorMsg = `Error fetching pedimento records: ${pedimentoError.message}`;
573
- logger.error(errorMsg);
574
- throw new Error(errorMsg);
575
- }
560
+ console.log('🔍 Processing pedimento_simplificado records page by page...');
576
561
 
577
- if (!pedimentoRecords || pedimentoRecords.length === 0) {
578
- logger.info('No pedimento_simplificado records with arela_path found');
562
+ // Log year filtering configuration
563
+ if (appConfig.upload.years && appConfig.upload.years.length > 0) {
564
+ logger.info(
565
+ `🗓️ Year filter enabled: ${appConfig.upload.years.join(', ')}`,
566
+ );
579
567
  console.log(
580
- 'ℹ️ No pedimento_simplificado records with arela_path found',
568
+ `🗓️ Year filter enabled: ${appConfig.upload.years.join(', ')}`,
581
569
  );
582
- return { processedCount: 0, updatedCount: 0, errorCount: 0 };
570
+ } else {
571
+ logger.info('🗓️ No year filter configured - processing all years');
572
+ console.log('🗓️ No year filter configured - processing all years');
583
573
  }
584
574
 
585
- console.log(
586
- `📋 Found ${pedimentoRecords.length} pedimento records with arela_path`,
587
- );
588
- logger.info(
589
- `Found ${pedimentoRecords.length} pedimento records with arela_path to process`,
590
- );
591
-
592
575
  let totalProcessed = 0;
593
576
  let totalUpdated = 0;
594
577
  let totalErrors = 0;
578
+ let offset = 0;
579
+ const pageSize = 50;
580
+ let hasMoreData = true;
581
+ let pageNumber = 1;
595
582
  const BATCH_SIZE = 50; // Process files in batches
596
583
 
597
- // Process each pedimento record
598
- for (const pedimento of pedimentoRecords) {
599
- try {
600
- totalProcessed++;
584
+ // Process pedimento records page by page for memory efficiency
585
+ while (hasMoreData) {
586
+ logger.info(
587
+ `Fetching and processing pedimento records page ${pageNumber} (records ${offset + 1} to ${offset + pageSize})...`,
588
+ );
589
+
590
+ let query = supabase
591
+ .from('uploader')
592
+ .select('id, original_path, arela_path, filename, year')
593
+ .eq('document_type', 'pedimento_simplificado')
594
+ .not('arela_path', 'is', null);
601
595
 
602
- // Extract base path from original_path (remove filename)
603
- const basePath = path.dirname(pedimento.original_path);
596
+ // Add year filter if UPLOAD_YEARS is configured
597
+ if (appConfig.upload.years && appConfig.upload.years.length > 0) {
598
+ query = query.in('year', appConfig.upload.years);
599
+ }
604
600
 
605
- logger.info(
606
- `Processing pedimento: ${pedimento.filename} | Base path: ${basePath} | Year: ${pedimento.year || 'N/A'}`,
607
- );
601
+ const { data: pedimentoPage, error: pedimentoError } = await query
602
+ .range(offset, offset + pageSize - 1)
603
+ .order('created_at');
608
604
 
609
- // Extract folder part from existing arela_path
610
- const existingPath = pedimento.arela_path;
611
- const folderArelaPath = existingPath.includes('/')
612
- ? existingPath.substring(0, existingPath.lastIndexOf('/')) + '/'
613
- : existingPath.endsWith('/')
614
- ? existingPath
615
- : existingPath + '/';
605
+ if (pedimentoError) {
606
+ const errorMsg = `Error fetching pedimento records page ${pageNumber}: ${pedimentoError.message}`;
607
+ logger.error(errorMsg);
608
+ throw new Error(errorMsg);
609
+ }
616
610
 
617
- // Find all files with the same base path that don't have arela_path yet
618
- const { data: relatedFiles, error: relatedError } = await supabase
619
- .from('uploader')
620
- .select('id, filename, original_path')
621
- .like('original_path', `${basePath}%`)
622
- .is('arela_path', null)
623
- .neq('id', pedimento.id); // Exclude the pedimento itself
611
+ if (!pedimentoPage || pedimentoPage.length === 0) {
612
+ hasMoreData = false;
613
+ logger.info('No more pedimento records found');
614
+ break;
615
+ }
624
616
 
625
- if (relatedError) {
626
- logger.error(
627
- `Error finding related files for ${pedimento.filename}: ${relatedError.message}`,
628
- );
629
- totalErrors++;
630
- continue;
631
- }
617
+ logger.info(
618
+ `Processing page ${pageNumber}: ${pedimentoPage.length} pedimento records`,
619
+ );
632
620
 
633
- if (!relatedFiles || relatedFiles.length === 0) {
634
- logger.info(`No related files found for ${pedimento.filename}`);
635
- continue;
636
- }
621
+ // Process each pedimento record in the current page
622
+ for (const pedimento of pedimentoPage) {
623
+ try {
624
+ totalProcessed++;
637
625
 
638
- logger.info(
639
- `Found ${relatedFiles.length} related files to update for ${pedimento.filename}`,
640
- );
626
+ // Extract base path from original_path (remove filename)
627
+ const basePath = path.dirname(pedimento.original_path);
641
628
 
642
- // Process files in batches
643
- const fileIds = relatedFiles.map((f) => f.id);
629
+ logger.info(
630
+ `Processing pedimento: ${pedimento.filename} | Base path: ${basePath} | Year: ${pedimento.year || 'N/A'}`,
631
+ );
644
632
 
645
- for (let i = 0; i < fileIds.length; i += BATCH_SIZE) {
646
- const batchIds = fileIds.slice(i, i + BATCH_SIZE);
647
- const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
648
- const totalBatches = Math.ceil(fileIds.length / BATCH_SIZE);
633
+ // Extract folder part from existing arela_path
634
+ const existingPath = pedimento.arela_path;
635
+ const folderArelaPath = existingPath.includes('/')
636
+ ? existingPath.substring(0, existingPath.lastIndexOf('/')) + '/'
637
+ : existingPath.endsWith('/')
638
+ ? existingPath
639
+ : existingPath + '/';
640
+
641
+ // Process related files page by page for memory efficiency
642
+ let relatedFilesFrom = 0;
643
+ const relatedFilesPageSize = 50;
644
+ let hasMoreRelatedFiles = true;
645
+ let relatedFilesPageNumber = 1;
646
+ let totalRelatedFilesProcessed = 0;
649
647
 
650
648
  logger.info(
651
- `Batch ${batchNumber}/${totalBatches}: Updating ${batchIds.length} files...`,
649
+ `Searching and processing related files in base path: ${basePath}`,
652
650
  );
653
651
 
654
- try {
655
- const { error: updateError } = await supabase
656
- .from('uploader')
657
- .update({
658
- arela_path: folderArelaPath,
659
- year: pedimento.year,
660
- })
661
- .in('id', batchIds);
652
+ while (hasMoreRelatedFiles) {
653
+ const { data: relatedFilesPage, error: relatedError } =
654
+ await supabase
655
+ .from('uploader')
656
+ .select('id, filename, original_path')
657
+ .like('original_path', `${basePath}%`)
658
+ .is('arela_path', null)
659
+ .neq('id', pedimento.id) // Exclude the pedimento itself
660
+ .range(
661
+ relatedFilesFrom,
662
+ relatedFilesFrom + relatedFilesPageSize - 1,
663
+ );
662
664
 
663
- if (updateError) {
665
+ if (relatedError) {
664
666
  logger.error(
665
- `Error in batch ${batchNumber}: ${updateError.message}`,
667
+ `Error finding related files for ${pedimento.filename}: ${relatedError.message}`,
666
668
  );
667
669
  totalErrors++;
670
+ break;
671
+ }
672
+
673
+ if (!relatedFilesPage || relatedFilesPage.length === 0) {
674
+ hasMoreRelatedFiles = false;
675
+ if (totalRelatedFilesProcessed === 0) {
676
+ logger.info(`No related files found for ${pedimento.filename}`);
677
+ }
678
+ break;
679
+ }
680
+
681
+ logger.info(
682
+ `Processing related files page ${relatedFilesPageNumber}: ${relatedFilesPage.length} files for ${pedimento.filename}`,
683
+ );
684
+
685
+ // Process this page of related files in batches
686
+ const pageFileIds = relatedFilesPage.map((f) => f.id);
687
+
688
+ for (let i = 0; i < pageFileIds.length; i += BATCH_SIZE) {
689
+ const batchIds = pageFileIds.slice(i, i + BATCH_SIZE);
690
+ const batchNumber =
691
+ Math.floor(relatedFilesFrom / BATCH_SIZE) +
692
+ Math.floor(i / BATCH_SIZE) +
693
+ 1;
694
+
695
+ logger.info(
696
+ `Updating batch ${batchNumber}: ${batchIds.length} files with arela_path and year...`,
697
+ );
698
+
699
+ try {
700
+ const { error: updateError } = await supabase
701
+ .from('uploader')
702
+ .update({
703
+ arela_path: folderArelaPath,
704
+ year: pedimento.year,
705
+ })
706
+ .in('id', batchIds);
707
+
708
+ if (updateError) {
709
+ logger.error(
710
+ `Error in batch ${batchNumber}: ${updateError.message}`,
711
+ );
712
+ totalErrors++;
713
+ } else {
714
+ totalUpdated += batchIds.length;
715
+ totalRelatedFilesProcessed += batchIds.length;
716
+ logger.info(
717
+ `Successfully updated batch ${batchNumber}: ${batchIds.length} files with arela_path and year`,
718
+ );
719
+ }
720
+ } catch (batchError) {
721
+ logger.error(
722
+ `Exception in batch ${batchNumber}: ${batchError.message}`,
723
+ );
724
+ totalErrors++;
725
+ }
726
+ }
727
+
728
+ // Check if we need to fetch the next page of related files
729
+ if (relatedFilesPage.length < relatedFilesPageSize) {
730
+ hasMoreRelatedFiles = false;
731
+ logger.info(
732
+ `Completed processing related files for ${pedimento.filename}. Total processed: ${totalRelatedFilesProcessed}`,
733
+ );
668
734
  } else {
669
- totalUpdated += batchIds.length;
735
+ relatedFilesFrom += relatedFilesPageSize;
736
+ relatedFilesPageNumber++;
670
737
  logger.info(
671
- `Successfully updated batch ${batchNumber}: ${batchIds.length} files with arela_path and year`,
738
+ `Page ${relatedFilesPageNumber - 1} complete: ${relatedFilesPage.length} files processed, continuing to next page...`,
672
739
  );
673
740
  }
674
- } catch (batchError) {
675
- logger.error(
676
- `Exception in batch ${batchNumber}: ${batchError.message}`,
677
- );
678
- totalErrors++;
679
741
  }
742
+ } catch (error) {
743
+ logger.error(
744
+ `Error processing pedimento ${pedimento.filename}: ${error.message}`,
745
+ );
746
+ totalErrors++;
680
747
  }
681
- } catch (error) {
682
- logger.error(
683
- `Error processing pedimento ${pedimento.filename}: ${error.message}`,
748
+ }
749
+
750
+ // Check if we need to fetch the next page
751
+ if (pedimentoPage.length < pageSize) {
752
+ hasMoreData = false;
753
+ logger.info(
754
+ `Completed processing. Last page ${pageNumber} had ${pedimentoPage.length} records`,
755
+ );
756
+ } else {
757
+ offset += pageSize;
758
+ pageNumber++;
759
+ logger.info(
760
+ `Page ${pageNumber - 1} complete: ${pedimentoPage.length} records processed, moving to next page...`,
684
761
  );
685
- totalErrors++;
686
762
  }
687
763
  }
688
764
 
765
+ // Final summary
766
+ if (totalProcessed === 0) {
767
+ logger.info('No pedimento_simplificado records with arela_path found');
768
+ console.log(
769
+ 'ℹ️ No pedimento_simplificado records with arela_path found',
770
+ );
771
+ } else {
772
+ console.log(
773
+ `📋 Processed ${totalProcessed} pedimento records across ${pageNumber - 1} pages`,
774
+ );
775
+ logger.info(
776
+ `Processed ${totalProcessed} pedimento records across ${pageNumber - 1} pages`,
777
+ );
778
+ }
779
+
689
780
  const result = {
690
781
  processedCount: totalProcessed,
691
782
  updatedCount: totalUpdated,
@@ -743,13 +834,21 @@ export class DatabaseService {
743
834
  console.log(
744
835
  '🎯 Finding pedimento_simplificado records for specified RFCs...',
745
836
  );
837
+
838
+ let pedimentoQuery = supabase
839
+ .from('uploader')
840
+ .select('arela_path')
841
+ .eq('document_type', 'pedimento_simplificado')
842
+ .in('rfc', appConfig.upload.rfcs)
843
+ .not('arela_path', 'is', null);
844
+
845
+ // Add year filter if UPLOAD_YEARS is configured
846
+ if (appConfig.upload.years && appConfig.upload.years.length > 0) {
847
+ pedimentoQuery = pedimentoQuery.in('year', appConfig.upload.years);
848
+ }
849
+
746
850
  const { data: pedimentoRfcRecords, error: pedimentoRfcError } =
747
- await supabase
748
- .from('uploader')
749
- .select('arela_path')
750
- .eq('document_type', 'pedimento_simplificado')
751
- .in('rfc', appConfig.upload.rfcs)
752
- .not('arela_path', 'is', null);
851
+ await pedimentoQuery;
753
852
 
754
853
  if (pedimentoRfcError) {
755
854
  const errorMsg = `Error fetching pedimento RFC records: ${pedimentoRfcError.message}`;
@@ -1003,13 +1102,25 @@ export class DatabaseService {
1003
1102
  console.log(
1004
1103
  '🎯 Finding pedimento_simplificado documents for specified RFCs with arela_path...',
1005
1104
  );
1006
- const { data: pedimentoRecords, error: pedimentoError } = await supabase
1105
+
1106
+ let pedimentoReadyQuery = supabase
1007
1107
  .from('uploader')
1008
1108
  .select('arela_path')
1009
1109
  .eq('document_type', 'pedimento_simplificado')
1010
1110
  .in('rfc', uploadRfcs)
1011
1111
  .not('arela_path', 'is', null);
1012
1112
 
1113
+ // Add year filter if UPLOAD_YEARS is configured
1114
+ if (appConfig.upload.years && appConfig.upload.years.length > 0) {
1115
+ pedimentoReadyQuery = pedimentoReadyQuery.in(
1116
+ 'year',
1117
+ appConfig.upload.years,
1118
+ );
1119
+ }
1120
+
1121
+ const { data: pedimentoRecords, error: pedimentoError } =
1122
+ await pedimentoReadyQuery;
1123
+
1013
1124
  if (pedimentoError) {
1014
1125
  throw new Error(
1015
1126
  `Error querying pedimento_simplificado records: ${pedimentoError.message}`,
@@ -1041,22 +1152,44 @@ export class DatabaseService {
1041
1152
  for (let i = 0; i < uniqueArelaPaths.length; i += chunkSize) {
1042
1153
  const pathChunk = uniqueArelaPaths.slice(i, i + chunkSize);
1043
1154
 
1044
- const { data: chunkFiles, error: chunkError } = await supabase
1045
- .from('uploader')
1046
- .select(
1047
- 'id, original_path, arela_path, filename, rfc, document_type, status',
1048
- )
1049
- .in('arela_path', pathChunk)
1050
- .neq('status', 'file-uploaded')
1051
- .not('original_path', 'is', null);
1155
+ // Query with pagination to get all results (Supabase default limit is 1000)
1156
+ let chunkFiles = [];
1157
+ let from = 0;
1158
+ const pageSize = 1000;
1159
+ let hasMoreData = true;
1052
1160
 
1053
- if (chunkError) {
1054
- throw new Error(
1055
- `Error querying files for arela_paths chunk: ${chunkError.message}`,
1056
- );
1161
+ while (hasMoreData) {
1162
+ const { data: pageData, error: chunkError } = await supabase
1163
+ .from('uploader')
1164
+ .select(
1165
+ 'id, original_path, arela_path, filename, rfc, document_type, status',
1166
+ )
1167
+ .in('arela_path', pathChunk)
1168
+ .neq('status', 'file-uploaded')
1169
+ .not('original_path', 'is', null)
1170
+ .range(from, from + pageSize - 1);
1171
+
1172
+ if (chunkError) {
1173
+ throw new Error(
1174
+ `Error querying files for arela_paths chunk: ${chunkError.message}`,
1175
+ );
1176
+ }
1177
+
1178
+ if (pageData && pageData.length > 0) {
1179
+ chunkFiles = chunkFiles.concat(pageData);
1180
+
1181
+ // Check if we got a full page, indicating there might be more data
1182
+ if (pageData.length < pageSize) {
1183
+ hasMoreData = false;
1184
+ } else {
1185
+ from += pageSize;
1186
+ }
1187
+ } else {
1188
+ hasMoreData = false;
1189
+ }
1057
1190
  }
1058
1191
 
1059
- if (chunkFiles && chunkFiles.length > 0) {
1192
+ if (chunkFiles.length > 0) {
1060
1193
  allReadyFiles = allReadyFiles.concat(chunkFiles);
1061
1194
  }
1062
1195
  }
@@ -1242,17 +1375,36 @@ export class DatabaseService {
1242
1375
  { uploadPath: uploadPath },
1243
1376
  );
1244
1377
 
1245
- // Update database status
1246
- await supabase
1247
- .from('uploader')
1248
- .update({
1249
- status: 'file-uploaded',
1250
- message: 'Successfully uploaded to Supabase',
1251
- })
1252
- .eq('id', file.id);
1378
+ // Check upload result before updating database status
1379
+ if (uploadResult.success) {
1380
+ await supabase
1381
+ .from('uploader')
1382
+ .update({
1383
+ status: 'file-uploaded',
1384
+ message: 'Successfully uploaded to Supabase',
1385
+ })
1386
+ .eq('id', file.id);
1387
+
1388
+ logger.info(`✅ Uploaded: ${file.filename}`);
1389
+ return { success: true, filename: file.filename };
1390
+ } else {
1391
+ await supabase
1392
+ .from('uploader')
1393
+ .update({
1394
+ status: 'upload-error',
1395
+ message: `Upload failed: ${uploadResult.error}`,
1396
+ })
1397
+ .eq('id', file.id);
1253
1398
 
1254
- logger.info(`✅ Uploaded: ${file.filename}`);
1255
- return { success: true, filename: file.filename };
1399
+ logger.error(
1400
+ `❌ Upload failed: ${file.filename} - ${uploadResult.error}`,
1401
+ );
1402
+ return {
1403
+ success: false,
1404
+ error: uploadResult.error,
1405
+ filename: file.filename,
1406
+ };
1407
+ }
1256
1408
  } catch (error) {
1257
1409
  logger.error(
1258
1410
  `❌ Error processing file ${file.filename}: ${error.message}`,
@@ -1335,23 +1487,188 @@ export class DatabaseService {
1335
1487
  { folderStructure: fullFolderStructure },
1336
1488
  );
1337
1489
 
1338
- if (uploadResult.success) {
1339
- // Update all files as uploaded
1340
- const fileIds = validFiles.map((f) => f.dbRecord.id);
1341
- await supabase
1342
- .from('uploader')
1343
- .update({
1344
- status: 'file-uploaded',
1345
- message: 'Successfully uploaded to Arela API (batch)',
1346
- })
1347
- .in('id', fileIds);
1490
+ if (uploadResult.success && uploadResult.data) {
1491
+ const apiResult = uploadResult.data;
1348
1492
 
1349
- uploaded += validFiles.length;
1350
1493
  logger.info(
1351
- `✅ Batch uploaded: ${validFiles.length} files to ${fullFolderStructure}`,
1494
+ `📋 Processing API response: ${apiResult.uploaded?.length || 0} uploaded, ${apiResult.errors?.length || 0} errors`,
1495
+ );
1496
+
1497
+ // Debug logging to understand API response structure
1498
+ logger.debug(
1499
+ `🔍 API Response structure: ${JSON.stringify(apiResult, null, 2)}`,
1500
+ );
1501
+ if (apiResult.uploaded && apiResult.uploaded.length > 0) {
1502
+ logger.debug(
1503
+ `🔍 First uploaded file structure: ${JSON.stringify(apiResult.uploaded[0], null, 2)}`,
1504
+ );
1505
+ }
1506
+
1507
+ // Create filename to file mapping for quick lookup
1508
+ const fileNameToRecord = new Map();
1509
+ validFiles.forEach((f) => {
1510
+ fileNameToRecord.set(f.fileData.name, f.dbRecord);
1511
+ });
1512
+
1513
+ // Debug: Log expected filenames
1514
+ logger.debug(
1515
+ `🔍 Expected filenames: ${Array.from(fileNameToRecord.keys()).join(', ')}`,
1516
+ );
1517
+
1518
+ // Handle successfully uploaded files
1519
+ if (apiResult.uploaded && apiResult.uploaded.length > 0) {
1520
+ const successfulFileIds = [];
1521
+ const matchedFilenames = [];
1522
+
1523
+ apiResult.uploaded.forEach((uploadedFile) => {
1524
+ // Try multiple possible property names for filename
1525
+ const possibleFilename =
1526
+ uploadedFile.fileName ||
1527
+ uploadedFile.filename ||
1528
+ uploadedFile.name ||
1529
+ uploadedFile.file_name ||
1530
+ uploadedFile.originalName ||
1531
+ uploadedFile.original_name;
1532
+
1533
+ logger.debug(
1534
+ `🔍 Trying to match uploaded file: ${JSON.stringify(uploadedFile)}`,
1535
+ );
1536
+
1537
+ const dbRecord = fileNameToRecord.get(possibleFilename);
1538
+ if (dbRecord) {
1539
+ successfulFileIds.push(dbRecord.id);
1540
+ matchedFilenames.push(possibleFilename);
1541
+ logger.debug(`✅ Matched file: ${possibleFilename}`);
1542
+ } else {
1543
+ logger.warn(
1544
+ `⚠️ Could not match uploaded file with any known filename: ${JSON.stringify(uploadedFile)}`,
1545
+ );
1546
+ }
1547
+ });
1548
+
1549
+ // If no individual files matched but API indicates success, use fallback
1550
+ if (
1551
+ successfulFileIds.length === 0 &&
1552
+ apiResult.uploaded.length > 0
1553
+ ) {
1554
+ logger.warn(
1555
+ `🔄 Fallback: No individual file matches found, but API indicates ${apiResult.uploaded.length} uploads. Marking all ${validFiles.length} batch files as uploaded.`,
1556
+ );
1557
+ validFiles.forEach((f) => successfulFileIds.push(f.dbRecord.id));
1558
+ }
1559
+
1560
+ if (successfulFileIds.length > 0) {
1561
+ await supabase
1562
+ .from('uploader')
1563
+ .update({
1564
+ status: 'file-uploaded',
1565
+ message: 'Successfully uploaded to Arela API (batch)',
1566
+ })
1567
+ .in('id', successfulFileIds);
1568
+
1569
+ uploaded += successfulFileIds.length;
1570
+ logger.info(
1571
+ `✅ Batch upload successful: ${successfulFileIds.length} files uploaded`,
1572
+ );
1573
+ }
1574
+ }
1575
+
1576
+ // Handle failed files
1577
+ if (apiResult.errors && apiResult.errors.length > 0) {
1578
+ const failedFileIds = [];
1579
+
1580
+ apiResult.errors.forEach((errorInfo) => {
1581
+ // Try multiple possible property names for filename in errors
1582
+ const possibleFilename =
1583
+ errorInfo.fileName ||
1584
+ errorInfo.filename ||
1585
+ errorInfo.name ||
1586
+ errorInfo.file_name ||
1587
+ errorInfo.originalName ||
1588
+ errorInfo.original_name;
1589
+
1590
+ const dbRecord = fileNameToRecord.get(possibleFilename);
1591
+ if (dbRecord) {
1592
+ failedFileIds.push(dbRecord.id);
1593
+ } else {
1594
+ logger.warn(
1595
+ `⚠️ Could not match error file: ${JSON.stringify(errorInfo)}`,
1596
+ );
1597
+ }
1598
+ });
1599
+
1600
+ if (failedFileIds.length > 0) {
1601
+ await supabase
1602
+ .from('uploader')
1603
+ .update({
1604
+ status: 'upload-error',
1605
+ message: `Upload failed: ${apiResult.errors[0].error}`,
1606
+ })
1607
+ .in('id', failedFileIds);
1608
+
1609
+ errors += failedFileIds.length;
1610
+ logger.error(
1611
+ `❌ Batch upload errors: ${failedFileIds.length} files failed`,
1612
+ );
1613
+ }
1614
+ }
1615
+
1616
+ // Handle any remaining files that weren't in uploaded or errors arrays
1617
+ // Use robust filename extraction for both uploaded and error files
1618
+ const extractFilename = (fileObj) => {
1619
+ return (
1620
+ fileObj.fileName ||
1621
+ fileObj.filename ||
1622
+ fileObj.name ||
1623
+ fileObj.file_name ||
1624
+ fileObj.originalName ||
1625
+ fileObj.original_name
1626
+ );
1627
+ };
1628
+
1629
+ const processedFileNames = new Set([
1630
+ ...(apiResult.uploaded || []).map(extractFilename).filter(Boolean),
1631
+ ...(apiResult.errors || []).map(extractFilename).filter(Boolean),
1632
+ ]);
1633
+
1634
+ const unprocessedFiles = validFiles.filter(
1635
+ (f) => !processedFileNames.has(f.fileData.name),
1352
1636
  );
1637
+ if (unprocessedFiles.length > 0) {
1638
+ // Only mark as unprocessed if we haven't already handled all files through fallback logic
1639
+ // If we used fallback (all files marked as uploaded), don't mark any as unprocessed
1640
+ const alreadyHandledCount = uploaded + errors;
1641
+ const shouldMarkUnprocessed =
1642
+ alreadyHandledCount < validFiles.length;
1643
+
1644
+ if (shouldMarkUnprocessed) {
1645
+ const unprocessedIds = unprocessedFiles.map((f) => f.dbRecord.id);
1646
+ await supabase
1647
+ .from('uploader')
1648
+ .update({
1649
+ status: 'upload-error',
1650
+ message: 'File not found in API response',
1651
+ })
1652
+ .in('id', unprocessedIds);
1653
+
1654
+ errors += unprocessedIds.length;
1655
+ logger.warn(
1656
+ `⚠️ Unprocessed files: ${unprocessedIds.length} files not found in API response`,
1657
+ );
1658
+ logger.debug(
1659
+ `🔍 API response uploaded array: ${JSON.stringify(apiResult.uploaded)}`,
1660
+ );
1661
+ logger.debug(
1662
+ `🔍 Expected filenames: ${validFiles.map((f) => f.fileData.name).join(', ')}`,
1663
+ );
1664
+ } else {
1665
+ logger.debug(
1666
+ `✅ All files already handled (uploaded: ${uploaded}, errors: ${errors}), skipping unprocessed marking`,
1667
+ );
1668
+ }
1669
+ }
1353
1670
  } else {
1354
- // Update all files as failed
1671
+ // Complete batch failure - mark all files as failed
1355
1672
  const fileIds = validFiles.map((f) => f.dbRecord.id);
1356
1673
  await supabase
1357
1674
  .from('uploader')
@@ -1363,7 +1680,7 @@ export class DatabaseService {
1363
1680
 
1364
1681
  errors += validFiles.length;
1365
1682
  logger.error(
1366
- `❌ Batch upload failed: ${validFiles.length} files - ${uploadResult.error}`,
1683
+ `❌ Complete batch failure: ${validFiles.length} files - ${uploadResult.error}`,
1367
1684
  );
1368
1685
  }
1369
1686
  }
@@ -134,8 +134,16 @@ export class ApiUploadService extends BaseUploadService {
134
134
 
135
135
  const result = await response.json();
136
136
 
137
+ // Determine if the operation was successful
138
+ // Success means at least one file was uploaded successfully, even if some failed
139
+ const hasUploads = result.uploaded && result.uploaded.length > 0;
140
+ const hasErrors = result.errors && result.errors.length > 0;
141
+ // Consider it a success if there are uploads, or if there are no errors at all
142
+ const success =
143
+ hasUploads || (!hasErrors && result.stats?.totalFiles > 0);
144
+
137
145
  // Normalize response format to match DatabaseService expectations
138
- return { success: true, data: result };
146
+ return { success, data: result };
139
147
  } catch (fetchError) {
140
148
  // Return normalized error format
141
149
  return { success: false, error: fetchError.message };
@@ -88,10 +88,10 @@ export class SupabaseUploadService extends BaseUploadService {
88
88
  });
89
89
 
90
90
  if (error) {
91
- throw new Error(error.message);
91
+ return { success: false, error: error.message };
92
92
  }
93
93
 
94
- return data;
94
+ return { success: true, data };
95
95
  }
96
96
 
97
97
  /**