@arela/uploader 0.2.13 → 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/.env.template +66 -0
- package/README.md +263 -62
- 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 +3 -2
- package/scripts/cleanup-ds-store.js +109 -0
- package/scripts/cleanup-system-files.js +69 -0
- package/scripts/tests/phase-7-features.test.js +415 -0
- package/scripts/tests/signal-handling.test.js +275 -0
- package/scripts/tests/smart-watch-integration.test.js +554 -0
- package/scripts/tests/watch-service-integration.test.js +584 -0
- package/src/commands/UploadCommand.js +31 -4
- package/src/commands/WatchCommand.js +1342 -0
- package/src/config/config.js +270 -2
- package/src/document-type-shared.js +2 -0
- package/src/document-types/support-document.js +200 -0
- package/src/file-detection.js +9 -1
- package/src/index.js +163 -4
- package/src/services/AdvancedFilterService.js +505 -0
- package/src/services/AutoProcessingService.js +749 -0
- package/src/services/BenchmarkingService.js +381 -0
- package/src/services/DatabaseService.js +1019 -539
- package/src/services/ErrorMonitor.js +275 -0
- package/src/services/LoggingService.js +419 -1
- package/src/services/MonitoringService.js +401 -0
- package/src/services/PerformanceOptimizer.js +511 -0
- package/src/services/ReportingService.js +511 -0
- package/src/services/SignalHandler.js +255 -0
- package/src/services/SmartWatchDatabaseService.js +527 -0
- package/src/services/WatchService.js +783 -0
- package/src/services/upload/ApiUploadService.js +447 -3
- package/src/services/upload/MultiApiUploadService.js +233 -0
- package/src/services/upload/SupabaseUploadService.js +12 -5
- package/src/services/upload/UploadServiceFactory.js +24 -0
- package/src/utils/CleanupManager.js +262 -0
- package/src/utils/FileOperations.js +44 -0
- package/src/utils/WatchEventHandler.js +522 -0
- package/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.envbackup +0 -37
- package/SUPABASE_UPLOAD_FIX.md +0 -157
- package/commands.md +0 -14
|
@@ -25,6 +25,15 @@ export class DatabaseService {
|
|
|
25
25
|
return await supabaseService.getClient();
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Get Supabase client (public wrapper for dependency injection)
|
|
30
|
+
* Used by specialized services like SmartWatchDatabaseService
|
|
31
|
+
* @returns {Promise<Object>} Supabase client
|
|
32
|
+
*/
|
|
33
|
+
async getSupabaseClient() {
|
|
34
|
+
return this.#getSupabaseClient();
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
38
|
* Execute database query with retry logic and exponential backoff
|
|
30
39
|
* @private
|
|
@@ -71,6 +80,18 @@ export class DatabaseService {
|
|
|
71
80
|
throw lastError;
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Execute database query with retry logic (public wrapper for dependency injection)
|
|
85
|
+
* Used by specialized services like SmartWatchDatabaseService
|
|
86
|
+
* @param {Function} queryFn - Query function to execute
|
|
87
|
+
* @param {string} operation - Description of the operation for logging
|
|
88
|
+
* @param {number} maxRetries - Maximum number of retry attempts (default: 3)
|
|
89
|
+
* @returns {Promise<Object>} Query result
|
|
90
|
+
*/
|
|
91
|
+
async queryWithRetry(queryFn, operation, maxRetries = 3) {
|
|
92
|
+
return this.#queryWithRetry(queryFn, operation, maxRetries);
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
/**
|
|
75
96
|
* Insert file stats with document detection into uploader table
|
|
76
97
|
* @param {Array} files - Array of file objects
|
|
@@ -112,6 +133,7 @@ export class DatabaseService {
|
|
|
112
133
|
const filename = file.originalName || path.basename(file.path);
|
|
113
134
|
|
|
114
135
|
const record = {
|
|
136
|
+
name: filename,
|
|
115
137
|
document_type: null,
|
|
116
138
|
size: stats.size,
|
|
117
139
|
num_pedimento: null,
|
|
@@ -122,11 +144,14 @@ export class DatabaseService {
|
|
|
122
144
|
rfc: null,
|
|
123
145
|
message: null,
|
|
124
146
|
file_extension: fileExtension,
|
|
125
|
-
is_like_simplificado:
|
|
126
|
-
fileExtension === 'pdf' && filename.toLowerCase().includes('simp'),
|
|
147
|
+
is_like_simplificado: filename.toLowerCase().includes('simp'),
|
|
127
148
|
year: null,
|
|
128
149
|
created_at: new Date().toISOString(),
|
|
150
|
+
updated_at: new Date().toISOString(),
|
|
129
151
|
modified_at: stats.mtime.toISOString(),
|
|
152
|
+
// Queue/Processing columns (for arela-api)
|
|
153
|
+
processing_status: 'PENDING',
|
|
154
|
+
upload_attempts: 0,
|
|
130
155
|
};
|
|
131
156
|
|
|
132
157
|
// Try to detect document type for supported files
|
|
@@ -147,11 +172,8 @@ export class DatabaseService {
|
|
|
147
172
|
record.year = detection.detectedPedimentoYear;
|
|
148
173
|
}
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
if (rfcField) {
|
|
154
|
-
record.rfc = rfcField.value;
|
|
175
|
+
if (detection.rfc) {
|
|
176
|
+
record.rfc = detection.rfc;
|
|
155
177
|
}
|
|
156
178
|
} else {
|
|
157
179
|
record.status = 'not-detected';
|
|
@@ -181,18 +203,153 @@ export class DatabaseService {
|
|
|
181
203
|
`Inserting ${records.length} new records into uploader table...`,
|
|
182
204
|
);
|
|
183
205
|
|
|
206
|
+
// Use upsert to handle duplicates gracefully
|
|
207
|
+
// This will insert new records or update existing ones (by original_path)
|
|
184
208
|
const { data, error } = await supabase
|
|
185
209
|
.from('uploader')
|
|
186
|
-
.
|
|
210
|
+
.upsert(records, { onConflict: 'original_path' })
|
|
187
211
|
.select();
|
|
188
212
|
|
|
189
213
|
if (error) {
|
|
190
214
|
throw new Error(`Failed to insert stats records: ${error.message}`);
|
|
191
215
|
}
|
|
192
216
|
|
|
217
|
+
// Propagate arela_path to related files in same folder
|
|
218
|
+
if (data && data.length > 0) {
|
|
219
|
+
await this.#propagateArelaPathToRelatedFiles(data, supabase);
|
|
220
|
+
}
|
|
221
|
+
|
|
193
222
|
return data;
|
|
194
223
|
}
|
|
195
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Propagate arela_path from pedimento files to related files in the same directory
|
|
227
|
+
* When a pedimento_simplificado is detected, all files in its directory get the same arela_path
|
|
228
|
+
* Also checks database for existing files in the same directory that need the arela_path
|
|
229
|
+
* @private
|
|
230
|
+
* @param {Array} insertedRecords - Records that were just inserted
|
|
231
|
+
* @param {Object} supabase - Supabase client
|
|
232
|
+
* @returns {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
async #propagateArelaPathToRelatedFiles(insertedRecords, supabase) {
|
|
235
|
+
try {
|
|
236
|
+
// Group records by directory
|
|
237
|
+
const recordsByDir = {};
|
|
238
|
+
|
|
239
|
+
for (const record of insertedRecords) {
|
|
240
|
+
if (!record.original_path) continue;
|
|
241
|
+
|
|
242
|
+
const dirPath = path.dirname(record.original_path);
|
|
243
|
+
if (!recordsByDir[dirPath]) {
|
|
244
|
+
recordsByDir[dirPath] = {
|
|
245
|
+
pedimentos: [],
|
|
246
|
+
allFiles: [],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
recordsByDir[dirPath].allFiles.push(record);
|
|
251
|
+
|
|
252
|
+
// Identify pedimento files
|
|
253
|
+
if (
|
|
254
|
+
record.document_type === 'pedimento_simplificado' &&
|
|
255
|
+
record.arela_path
|
|
256
|
+
) {
|
|
257
|
+
recordsByDir[dirPath].pedimentos.push(record);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// For each directory with a pedimento, propagate its arela_path to related files
|
|
262
|
+
for (const [dirPath, dirData] of Object.entries(recordsByDir)) {
|
|
263
|
+
if (dirData.pedimentos.length === 0) continue;
|
|
264
|
+
|
|
265
|
+
// Use the first pedimento's arela_path (should be only one per directory typically)
|
|
266
|
+
const pedimentoRecord = dirData.pedimentos[0];
|
|
267
|
+
const arelaPath = pedimentoRecord.arela_path;
|
|
268
|
+
|
|
269
|
+
logger.info(
|
|
270
|
+
`📁 Propagating arela_path from pedimento to files in ${path.basename(dirPath)}/`,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Step 1: Update newly inserted files in this directory
|
|
274
|
+
const fileIds = dirData.allFiles
|
|
275
|
+
.filter(
|
|
276
|
+
(f) =>
|
|
277
|
+
f.id &&
|
|
278
|
+
f.arela_path !== arelaPath &&
|
|
279
|
+
f.document_type !== 'pedimento_simplificado',
|
|
280
|
+
)
|
|
281
|
+
.map((f) => f.id);
|
|
282
|
+
|
|
283
|
+
if (fileIds.length > 0) {
|
|
284
|
+
const { error: updateError } = await supabase
|
|
285
|
+
.from('uploader')
|
|
286
|
+
.update({ arela_path: arelaPath })
|
|
287
|
+
.in('id', fileIds);
|
|
288
|
+
|
|
289
|
+
if (updateError) {
|
|
290
|
+
logger.warn(
|
|
291
|
+
`Could not propagate arela_path: ${updateError.message}`,
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
logger.info(
|
|
295
|
+
`✅ Updated ${fileIds.length} related files with arela_path: ${arelaPath}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Step 2: Also find and update any existing files in the same directory that don't have arela_path
|
|
301
|
+
// This handles the case where files were detected earlier but the pedimento is detected now
|
|
302
|
+
try {
|
|
303
|
+
const dirPattern = dirPath.replace(/\\/g, '/'); // Normalize for SQL LIKE
|
|
304
|
+
const { data: existingFiles, error: fetchError } = await supabase
|
|
305
|
+
.from('uploader')
|
|
306
|
+
.select('id, original_path, document_type')
|
|
307
|
+
.like('original_path', `${dirPattern}/%`)
|
|
308
|
+
.is('arela_path', null)
|
|
309
|
+
.limit(1000); // Reasonable limit to avoid huge queries
|
|
310
|
+
|
|
311
|
+
if (fetchError) {
|
|
312
|
+
logger.warn(
|
|
313
|
+
`Could not fetch existing files in ${path.basename(dirPath)}: ${fetchError.message}`,
|
|
314
|
+
);
|
|
315
|
+
} else if (existingFiles && existingFiles.length > 0) {
|
|
316
|
+
const existingFileIds = existingFiles
|
|
317
|
+
.filter(
|
|
318
|
+
(f) => f.id && f.document_type !== 'pedimento_simplificado',
|
|
319
|
+
)
|
|
320
|
+
.map((f) => f.id);
|
|
321
|
+
|
|
322
|
+
if (existingFileIds.length > 0) {
|
|
323
|
+
const { error: existingError } = await supabase
|
|
324
|
+
.from('uploader')
|
|
325
|
+
.update({ arela_path: arelaPath })
|
|
326
|
+
.in('id', existingFileIds);
|
|
327
|
+
|
|
328
|
+
if (existingError) {
|
|
329
|
+
logger.warn(
|
|
330
|
+
`Could not update existing files with arela_path: ${existingError.message}`,
|
|
331
|
+
);
|
|
332
|
+
} else {
|
|
333
|
+
logger.info(
|
|
334
|
+
`✅ Updated ${existingFileIds.length} existing files in directory with arela_path: ${arelaPath}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch (existingFilesError) {
|
|
340
|
+
logger.warn(
|
|
341
|
+
`Error checking for existing files in ${path.basename(dirPath)}: ${existingFilesError.message}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
logger.warn(
|
|
347
|
+
`Error propagating arela_path to related files: ${error.message}`,
|
|
348
|
+
);
|
|
349
|
+
// Don't throw - this is a non-critical operation
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
196
353
|
/**
|
|
197
354
|
* Insert file stats only (no detection) into uploader table
|
|
198
355
|
* @param {Array} files - Array of file objects
|
|
@@ -200,13 +357,30 @@ export class DatabaseService {
|
|
|
200
357
|
* @returns {Promise<Object>} Statistics about the operation
|
|
201
358
|
*/
|
|
202
359
|
async insertStatsOnlyToUploaderTable(files, options) {
|
|
203
|
-
const supabase = await this.#getSupabaseClient();
|
|
204
360
|
const batchSize = 1000;
|
|
361
|
+
const quietMode = options?.quietMode || false;
|
|
205
362
|
const allRecords = [];
|
|
206
363
|
|
|
207
|
-
|
|
364
|
+
if (!quietMode) {
|
|
365
|
+
logger.info('Collecting filesystem stats...');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Filter out system files and hidden files (macOS, Windows, Python, editors)
|
|
369
|
+
const systemFilePattern =
|
|
370
|
+
/^\.|__pycache__|\.pyc|\.swp|\.swo|Thumbs\.db|desktop\.ini|DS_Store|\$RECYCLE\.BIN|System Volume Information|~\$|\.tmp/i;
|
|
371
|
+
|
|
208
372
|
for (const file of files) {
|
|
209
373
|
try {
|
|
374
|
+
const fileName = file.originalName || path.basename(file.path);
|
|
375
|
+
|
|
376
|
+
// Skip system and hidden files
|
|
377
|
+
if (systemFilePattern.test(fileName)) {
|
|
378
|
+
if (!quietMode) {
|
|
379
|
+
logger.debug(`Skipping system file: ${fileName}`);
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
210
384
|
const stats = file.stats || fs.statSync(file.path);
|
|
211
385
|
const originalPath = options.clientPath || file.path;
|
|
212
386
|
const fileExtension = path
|
|
@@ -215,24 +389,27 @@ export class DatabaseService {
|
|
|
215
389
|
.replace('.', '');
|
|
216
390
|
|
|
217
391
|
const record = {
|
|
218
|
-
|
|
392
|
+
name: file.originalName || path.basename(file.path),
|
|
393
|
+
documentType: null,
|
|
219
394
|
size: stats.size,
|
|
220
|
-
|
|
395
|
+
numPedimento: null,
|
|
221
396
|
filename: file.originalName || path.basename(file.path),
|
|
222
|
-
|
|
223
|
-
|
|
397
|
+
originalPath: originalPath,
|
|
398
|
+
arelaPath: null,
|
|
224
399
|
status: 'fs-stats',
|
|
225
400
|
rfc: null,
|
|
226
401
|
message: null,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
is_like_simplificado:
|
|
402
|
+
fileExtension: fileExtension,
|
|
403
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
404
|
+
isLikeSimplificado:
|
|
231
405
|
fileExtension === 'pdf' &&
|
|
232
406
|
(file.originalName || path.basename(file.path))
|
|
233
407
|
.toLowerCase()
|
|
234
408
|
.includes('simp'),
|
|
235
409
|
year: null,
|
|
410
|
+
// Queue/Processing columns (for arela-api)
|
|
411
|
+
processingStatus: 'PENDING',
|
|
412
|
+
uploadAttempts: 0,
|
|
236
413
|
};
|
|
237
414
|
|
|
238
415
|
allRecords.push(record);
|
|
@@ -242,104 +419,66 @@ export class DatabaseService {
|
|
|
242
419
|
}
|
|
243
420
|
|
|
244
421
|
if (allRecords.length === 0) {
|
|
245
|
-
|
|
422
|
+
if (!quietMode) {
|
|
423
|
+
logger.info('No file stats to insert');
|
|
424
|
+
}
|
|
246
425
|
return { totalInserted: 0, totalSkipped: 0, totalProcessed: 0 };
|
|
247
426
|
}
|
|
248
427
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
428
|
+
if (!quietMode) {
|
|
429
|
+
logger.info(
|
|
430
|
+
`Processing ${allRecords.length} file stats in batches of ${batchSize}...`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
252
433
|
|
|
253
434
|
let totalInserted = 0;
|
|
254
435
|
let totalUpdated = 0;
|
|
255
436
|
|
|
437
|
+
// Use API service for batch upsert
|
|
438
|
+
// If apiTarget is specified, use that specific API, otherwise use default
|
|
439
|
+
let uploadService;
|
|
440
|
+
if (options.apiTarget) {
|
|
441
|
+
uploadService = await uploadServiceFactory.getApiServiceForTarget(
|
|
442
|
+
options.apiTarget,
|
|
443
|
+
);
|
|
444
|
+
} else {
|
|
445
|
+
uploadService = await uploadServiceFactory.getUploadService(
|
|
446
|
+
options.forceSupabase,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Use API service for batch upsert
|
|
256
451
|
for (let i = 0; i < allRecords.length; i += batchSize) {
|
|
257
452
|
const batch = allRecords.slice(i, i + batchSize);
|
|
258
453
|
|
|
259
454
|
try {
|
|
260
|
-
|
|
261
|
-
const originalPaths = batch.map((r) => r.original_path);
|
|
262
|
-
const { data: existingRecords, error: checkError } = await supabase
|
|
263
|
-
.from('uploader')
|
|
264
|
-
.select('original_path')
|
|
265
|
-
.in('original_path', originalPaths);
|
|
266
|
-
|
|
267
|
-
if (checkError) {
|
|
268
|
-
logger.error(
|
|
269
|
-
`Error checking existing records: ${checkError.message}`,
|
|
270
|
-
);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
455
|
+
const result = await uploadService.batchUpsertStats(batch);
|
|
273
456
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
);
|
|
277
|
-
const newRecords = batch.filter(
|
|
278
|
-
(r) => !existingPaths.has(r.original_path),
|
|
279
|
-
);
|
|
280
|
-
const updateRecords = batch.filter((r) =>
|
|
281
|
-
existingPaths.has(r.original_path),
|
|
282
|
-
);
|
|
457
|
+
totalInserted += result.inserted || 0;
|
|
458
|
+
totalUpdated += result.updated || 0;
|
|
283
459
|
|
|
284
|
-
// Only log every 10th batch to reduce noise
|
|
460
|
+
// Only log every 10th batch to reduce noise (skip in quiet mode)
|
|
285
461
|
if (
|
|
286
|
-
|
|
287
|
-
Math.floor(i / batchSize) + 1 ===
|
|
462
|
+
!quietMode &&
|
|
463
|
+
((Math.floor(i / batchSize) + 1) % 10 === 0 ||
|
|
464
|
+
Math.floor(i / batchSize) + 1 === 1)
|
|
288
465
|
) {
|
|
289
466
|
logger.info(
|
|
290
|
-
`Batch ${Math.floor(i / batchSize) + 1}: ${
|
|
467
|
+
`Batch ${Math.floor(i / batchSize) + 1}: ${result.inserted || 0} new, ${result.updated || 0} updates`,
|
|
291
468
|
);
|
|
292
469
|
}
|
|
293
|
-
|
|
294
|
-
// Insert new records
|
|
295
|
-
if (newRecords.length > 0) {
|
|
296
|
-
const { error: insertError } = await supabase
|
|
297
|
-
.from('uploader')
|
|
298
|
-
.insert(newRecords);
|
|
299
|
-
|
|
300
|
-
if (insertError) {
|
|
301
|
-
logger.error(`Error inserting new records: ${insertError.message}`);
|
|
302
|
-
} else {
|
|
303
|
-
totalInserted += newRecords.length;
|
|
304
|
-
// Only log the batch insertion, not the summary (which comes at the end)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Update existing records
|
|
309
|
-
if (updateRecords.length > 0) {
|
|
310
|
-
let batchUpdated = 0;
|
|
311
|
-
for (const record of updateRecords) {
|
|
312
|
-
const { error: updateError } = await supabase
|
|
313
|
-
.from('uploader')
|
|
314
|
-
.update({
|
|
315
|
-
size: record.size,
|
|
316
|
-
modified_at: record.modified_at,
|
|
317
|
-
filename: record.filename,
|
|
318
|
-
file_extension: record.file_extension,
|
|
319
|
-
is_like_simplificado: record.is_like_simplificado,
|
|
320
|
-
})
|
|
321
|
-
.eq('original_path', record.original_path);
|
|
322
|
-
|
|
323
|
-
if (!updateError) {
|
|
324
|
-
batchUpdated++;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
totalUpdated += batchUpdated;
|
|
328
|
-
// Reduce logging noise - only log when there are updates
|
|
329
|
-
if (batchUpdated > 0) {
|
|
330
|
-
logger.info(`Updated ${batchUpdated} existing records`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
470
|
} catch (error) {
|
|
334
471
|
logger.error(
|
|
335
|
-
`
|
|
472
|
+
`Error in batch ${Math.floor(i / batchSize) + 1}: ${error.message}`,
|
|
336
473
|
);
|
|
337
474
|
}
|
|
338
475
|
}
|
|
339
476
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
477
|
+
if (!quietMode) {
|
|
478
|
+
logger.success(
|
|
479
|
+
`Phase 1 Summary: ${totalInserted} new records inserted, ${totalUpdated} existing records updated`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
343
482
|
|
|
344
483
|
return {
|
|
345
484
|
totalInserted,
|
|
@@ -354,8 +493,6 @@ export class DatabaseService {
|
|
|
354
493
|
* @returns {Promise<Object>} Processing result
|
|
355
494
|
*/
|
|
356
495
|
async detectPedimentosInDatabase(options = {}) {
|
|
357
|
-
const supabase = await this.#getSupabaseClient();
|
|
358
|
-
|
|
359
496
|
logger.info(
|
|
360
497
|
'Phase 2: Starting PDF detection for pedimento-simplificado documents...',
|
|
361
498
|
);
|
|
@@ -370,6 +507,25 @@ export class DatabaseService {
|
|
|
370
507
|
let offset = 0;
|
|
371
508
|
let chunkNumber = 1;
|
|
372
509
|
|
|
510
|
+
// Get API service - use specific target if provided
|
|
511
|
+
let apiService;
|
|
512
|
+
if (options.apiTarget) {
|
|
513
|
+
apiService = await uploadServiceFactory.getApiServiceForTarget(
|
|
514
|
+
options.apiTarget,
|
|
515
|
+
);
|
|
516
|
+
logger.info(`Using API target: ${options.apiTarget}`);
|
|
517
|
+
} else {
|
|
518
|
+
apiService = await uploadServiceFactory.getUploadService();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (apiService.getServiceName() !== 'Arela API') {
|
|
522
|
+
throw new Error(
|
|
523
|
+
'API service is required for PDF detection. Please configure ARELA_API_URL and ARELA_API_TOKEN.',
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
logger.info('Using API service for PDF detection...');
|
|
528
|
+
|
|
373
529
|
logger.info(
|
|
374
530
|
`Processing PDF files in chunks of ${queryBatchSize} records...`,
|
|
375
531
|
);
|
|
@@ -380,18 +536,12 @@ export class DatabaseService {
|
|
|
380
536
|
);
|
|
381
537
|
|
|
382
538
|
try {
|
|
383
|
-
//
|
|
539
|
+
// Fetch records using API
|
|
384
540
|
const { data: pdfRecords, error: queryError } =
|
|
385
|
-
await
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
.eq('status', 'fs-stats')
|
|
390
|
-
.eq('file_extension', 'pdf')
|
|
391
|
-
.eq('is_like_simplificado', true)
|
|
392
|
-
.range(offset, offset + queryBatchSize - 1)
|
|
393
|
-
.order('created_at');
|
|
394
|
-
}, `fetch PDF records chunk ${chunkNumber}`);
|
|
541
|
+
await apiService.fetchPdfRecordsForDetection({
|
|
542
|
+
offset,
|
|
543
|
+
limit: queryBatchSize,
|
|
544
|
+
});
|
|
395
545
|
|
|
396
546
|
if (queryError) {
|
|
397
547
|
throw new Error(
|
|
@@ -413,9 +563,10 @@ export class DatabaseService {
|
|
|
413
563
|
let chunkErrors = 0;
|
|
414
564
|
|
|
415
565
|
// Process files in smaller batches
|
|
566
|
+
const batchUpdates = [];
|
|
567
|
+
|
|
416
568
|
for (let i = 0; i < pdfRecords.length; i += processingBatchSize) {
|
|
417
569
|
const batch = pdfRecords.slice(i, i + processingBatchSize);
|
|
418
|
-
const updatePromises = [];
|
|
419
570
|
|
|
420
571
|
for (const record of batch) {
|
|
421
572
|
try {
|
|
@@ -423,15 +574,11 @@ export class DatabaseService {
|
|
|
423
574
|
logger.warn(
|
|
424
575
|
`File not found: ${record.filename} at ${record.original_path}`,
|
|
425
576
|
);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
message: 'File no longer exists at original path',
|
|
432
|
-
})
|
|
433
|
-
.eq('id', record.id),
|
|
434
|
-
);
|
|
577
|
+
batchUpdates.push({
|
|
578
|
+
id: record.id,
|
|
579
|
+
status: 'file-not-found',
|
|
580
|
+
message: 'File no longer exists at original path',
|
|
581
|
+
});
|
|
435
582
|
chunkErrors++;
|
|
436
583
|
totalErrors++;
|
|
437
584
|
continue;
|
|
@@ -444,28 +591,24 @@ export class DatabaseService {
|
|
|
444
591
|
totalProcessed++;
|
|
445
592
|
|
|
446
593
|
const updateData = {
|
|
594
|
+
id: record.id,
|
|
447
595
|
status: detection.detectedType ? 'detected' : 'not-detected',
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
596
|
+
documentType: detection.detectedType,
|
|
597
|
+
numPedimento: detection.detectedPedimento,
|
|
598
|
+
arelaPath: detection.arelaPath,
|
|
451
599
|
message: detection.error || null,
|
|
452
600
|
year: detection.detectedPedimentoYear || null,
|
|
453
601
|
};
|
|
454
602
|
|
|
455
|
-
if (detection.
|
|
456
|
-
|
|
457
|
-
(f) => f.name === 'rfc' && f.found,
|
|
458
|
-
);
|
|
459
|
-
if (rfcField) {
|
|
460
|
-
updateData.rfc = rfcField.value;
|
|
461
|
-
}
|
|
603
|
+
if (detection.rfc) {
|
|
604
|
+
updateData.rfc = detection.rfc;
|
|
462
605
|
}
|
|
463
606
|
|
|
464
607
|
if (detection.detectedType) {
|
|
465
608
|
chunkDetected++;
|
|
466
609
|
totalDetected++;
|
|
467
610
|
logger.success(
|
|
468
|
-
`Detected: ${record.filename} -> ${detection.detectedType} | Pedimento: ${detection.detectedPedimento || 'N/A'} | RFC: ${
|
|
611
|
+
`Detected: ${record.filename} -> ${detection.detectedType} | Pedimento: ${detection.detectedPedimento || 'N/A'} | RFC: ${updateData.rfc || 'N/A'}`,
|
|
469
612
|
);
|
|
470
613
|
} else {
|
|
471
614
|
logger.info(
|
|
@@ -473,12 +616,7 @@ export class DatabaseService {
|
|
|
473
616
|
);
|
|
474
617
|
}
|
|
475
618
|
|
|
476
|
-
|
|
477
|
-
supabase
|
|
478
|
-
.from('uploader')
|
|
479
|
-
.update(updateData)
|
|
480
|
-
.eq('id', record.id),
|
|
481
|
-
);
|
|
619
|
+
batchUpdates.push(updateData);
|
|
482
620
|
} catch (error) {
|
|
483
621
|
logger.error(
|
|
484
622
|
`Error detecting ${record.filename}: ${error.message}`,
|
|
@@ -486,25 +624,30 @@ export class DatabaseService {
|
|
|
486
624
|
chunkErrors++;
|
|
487
625
|
totalErrors++;
|
|
488
626
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
message: error.message,
|
|
495
|
-
})
|
|
496
|
-
.eq('id', record.id),
|
|
497
|
-
);
|
|
627
|
+
batchUpdates.push({
|
|
628
|
+
id: record.id,
|
|
629
|
+
status: 'detection-error',
|
|
630
|
+
message: error.message,
|
|
631
|
+
});
|
|
498
632
|
}
|
|
499
633
|
}
|
|
634
|
+
}
|
|
500
635
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
636
|
+
// Batch update using API
|
|
637
|
+
try {
|
|
638
|
+
if (batchUpdates.length > 0) {
|
|
639
|
+
const updateResult =
|
|
640
|
+
await apiService.batchUpdateDetectionResults(batchUpdates);
|
|
641
|
+
if (!updateResult.success) {
|
|
642
|
+
logger.error(
|
|
643
|
+
`Some updates failed in chunk ${chunkNumber}: ${updateResult.errors?.length || 0} errors`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
507
646
|
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
logger.error(
|
|
649
|
+
`Error updating batch in chunk ${chunkNumber}: ${error.message}`,
|
|
650
|
+
);
|
|
508
651
|
}
|
|
509
652
|
|
|
510
653
|
logger.success(
|
|
@@ -550,276 +693,156 @@ export class DatabaseService {
|
|
|
550
693
|
|
|
551
694
|
/**
|
|
552
695
|
* Propagate arela_path from pedimento_simplificado records to related files
|
|
696
|
+
* This operation is performed entirely on the backend for efficiency
|
|
553
697
|
* @param {Object} options - Options for propagation
|
|
554
698
|
* @returns {Promise<Object>} Processing result
|
|
555
699
|
*/
|
|
556
700
|
async propagateArelaPath(options = {}) {
|
|
557
|
-
const supabase = await this.#getSupabaseClient();
|
|
558
|
-
|
|
559
701
|
logger.info('Phase 3: Starting arela_path and year propagation process...');
|
|
560
|
-
console.log('🔍
|
|
702
|
+
console.log('🔍 Triggering backend propagation process...');
|
|
561
703
|
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
console.log(
|
|
568
|
-
`🗓️ Year filter enabled: ${appConfig.upload.years.join(', ')}`,
|
|
704
|
+
// Get API service - use specific target if provided
|
|
705
|
+
let apiService;
|
|
706
|
+
if (options.apiTarget) {
|
|
707
|
+
apiService = await uploadServiceFactory.getApiServiceForTarget(
|
|
708
|
+
options.apiTarget,
|
|
569
709
|
);
|
|
710
|
+
logger.info(`Using API target: ${options.apiTarget}`);
|
|
570
711
|
} else {
|
|
571
|
-
|
|
572
|
-
console.log('🗓️ No year filter configured - processing all years');
|
|
712
|
+
apiService = await uploadServiceFactory.getUploadService();
|
|
573
713
|
}
|
|
574
714
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
let offset = 0;
|
|
579
|
-
const pageSize = 50;
|
|
580
|
-
let hasMoreData = true;
|
|
581
|
-
let pageNumber = 1;
|
|
582
|
-
const BATCH_SIZE = 50; // Process files in batches
|
|
583
|
-
|
|
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);
|
|
595
|
-
|
|
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
|
-
}
|
|
600
|
-
|
|
601
|
-
const { data: pedimentoPage, error: pedimentoError } = await query
|
|
602
|
-
.range(offset, offset + pageSize - 1)
|
|
603
|
-
.order('created_at');
|
|
604
|
-
|
|
605
|
-
if (pedimentoError) {
|
|
606
|
-
const errorMsg = `Error fetching pedimento records page ${pageNumber}: ${pedimentoError.message}`;
|
|
607
|
-
logger.error(errorMsg);
|
|
608
|
-
throw new Error(errorMsg);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (!pedimentoPage || pedimentoPage.length === 0) {
|
|
612
|
-
hasMoreData = false;
|
|
613
|
-
logger.info('No more pedimento records found');
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
logger.info(
|
|
618
|
-
`Processing page ${pageNumber}: ${pedimentoPage.length} pedimento records`,
|
|
715
|
+
if (apiService.getServiceName() !== 'Arela API') {
|
|
716
|
+
throw new Error(
|
|
717
|
+
'API service is required for arela_path propagation. Please configure ARELA_API_URL and ARELA_API_TOKEN.',
|
|
619
718
|
);
|
|
719
|
+
}
|
|
620
720
|
|
|
621
|
-
|
|
622
|
-
for (const pedimento of pedimentoPage) {
|
|
623
|
-
try {
|
|
624
|
-
totalProcessed++;
|
|
625
|
-
|
|
626
|
-
// Extract base path from original_path (remove filename)
|
|
627
|
-
const basePath = path.dirname(pedimento.original_path);
|
|
628
|
-
|
|
629
|
-
logger.info(
|
|
630
|
-
`Processing pedimento: ${pedimento.filename} | Base path: ${basePath} | Year: ${pedimento.year || 'N/A'}`,
|
|
631
|
-
);
|
|
632
|
-
|
|
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;
|
|
647
|
-
|
|
648
|
-
logger.info(
|
|
649
|
-
`Searching and processing related files in base path: ${basePath}`,
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
while (hasMoreRelatedFiles) {
|
|
653
|
-
const { data: relatedFilesPage, error: relatedError } =
|
|
654
|
-
await this.#queryWithRetry(async () => {
|
|
655
|
-
return await supabase
|
|
656
|
-
.from('uploader')
|
|
657
|
-
.select('id, filename, original_path')
|
|
658
|
-
.like('original_path', `${basePath}%`)
|
|
659
|
-
.is('arela_path', null)
|
|
660
|
-
.neq('id', pedimento.id) // Exclude the pedimento itself
|
|
661
|
-
.range(
|
|
662
|
-
relatedFilesFrom,
|
|
663
|
-
relatedFilesFrom + relatedFilesPageSize - 1,
|
|
664
|
-
);
|
|
665
|
-
}, `query related files for ${pedimento.filename} (page ${relatedFilesPageNumber})`);
|
|
666
|
-
|
|
667
|
-
if (relatedError) {
|
|
668
|
-
logger.error(
|
|
669
|
-
`Error finding related files for ${pedimento.filename}: ${relatedError.message}`,
|
|
670
|
-
);
|
|
671
|
-
totalErrors++;
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
// console.log(`query by basePath: ${basePath} count: [${relatedFilesPage.length}]`);
|
|
675
|
-
|
|
676
|
-
if (!relatedFilesPage || relatedFilesPage.length === 0) {
|
|
677
|
-
hasMoreRelatedFiles = false;
|
|
678
|
-
if (totalRelatedFilesProcessed === 0) {
|
|
679
|
-
logger.info(`No related files found for ${pedimento.filename}`);
|
|
680
|
-
}
|
|
681
|
-
break;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
logger.info(
|
|
685
|
-
`Processing related files page ${relatedFilesPageNumber}: ${relatedFilesPage.length} files for ${pedimento.filename}`,
|
|
686
|
-
);
|
|
687
|
-
|
|
688
|
-
// Track if any updates occurred in this page
|
|
689
|
-
let updatesOccurred = false;
|
|
690
|
-
// Process this page of related files in batches
|
|
691
|
-
const pageFileIds = relatedFilesPage.map((f) => f.id);
|
|
721
|
+
logger.info('Using API service for arela_path propagation...');
|
|
692
722
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
723
|
+
// Log year filtering configuration
|
|
724
|
+
const years = appConfig.upload.years || [];
|
|
725
|
+
if (years.length > 0) {
|
|
726
|
+
logger.info(`🗓️ Year filter enabled: ${years.join(', ')}`);
|
|
727
|
+
console.log(`🗓️ Year filter enabled: ${years.join(', ')}`);
|
|
728
|
+
} else {
|
|
729
|
+
logger.info('🗓️ No year filter configured - processing all years');
|
|
730
|
+
console.log('🗓️ No year filter configured - processing all years');
|
|
731
|
+
}
|
|
699
732
|
|
|
700
|
-
|
|
701
|
-
`Updating batch ${batchNumber}: ${batchIds.length} files with arela_path and year...`,
|
|
702
|
-
);
|
|
733
|
+
console.log('⏳ Processing on backend... This may take a moment.');
|
|
703
734
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
.from('uploader')
|
|
707
|
-
.update({
|
|
708
|
-
arela_path: folderArelaPath,
|
|
709
|
-
year: pedimento.year,
|
|
710
|
-
})
|
|
711
|
-
.in('id', batchIds);
|
|
712
|
-
|
|
713
|
-
if (updateError) {
|
|
714
|
-
logger.error(
|
|
715
|
-
`Error in batch ${batchNumber}: ${updateError.message}`,
|
|
716
|
-
);
|
|
717
|
-
totalErrors++;
|
|
718
|
-
} else {
|
|
719
|
-
totalUpdated += batchIds.length;
|
|
720
|
-
totalRelatedFilesProcessed += batchIds.length;
|
|
721
|
-
updatesOccurred = true; // Mark that updates occurred
|
|
722
|
-
logger.info(
|
|
723
|
-
`Successfully updated batch ${batchNumber}: ${batchIds.length} files with arela_path and year`,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
} catch (batchError) {
|
|
727
|
-
logger.error(
|
|
728
|
-
`Exception in batch ${batchNumber}: ${batchError.message}`,
|
|
729
|
-
);
|
|
730
|
-
totalErrors++;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
735
|
+
// Trigger backend propagation - all logic runs server-side
|
|
736
|
+
const result = await apiService.propagateArelaPath({ years });
|
|
733
737
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
logger.info(
|
|
739
|
-
`Completed processing related files for ${pedimento.filename}. Total processed: ${totalRelatedFilesProcessed}`,
|
|
740
|
-
);
|
|
741
|
-
} else {
|
|
742
|
-
// If updates occurred, reset pagination to start from beginning
|
|
743
|
-
// since records that matched the query may no longer match after update
|
|
744
|
-
if (updatesOccurred) {
|
|
745
|
-
relatedFilesFrom = 0;
|
|
746
|
-
logger.info(
|
|
747
|
-
`Page ${relatedFilesPageNumber} complete with updates: ${relatedFilesPage.length} files processed, restarting pagination from beginning due to query condition changes...`,
|
|
748
|
-
);
|
|
749
|
-
} else {
|
|
750
|
-
relatedFilesFrom += relatedFilesPageSize - 1;
|
|
751
|
-
logger.info(
|
|
752
|
-
`Page ${relatedFilesPageNumber} complete: ${relatedFilesPage.length} files processed, continuing to next page...`,
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
relatedFilesPageNumber++;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
} catch (error) {
|
|
759
|
-
logger.error(
|
|
760
|
-
`Error processing pedimento ${pedimento.filename}: ${error.message}`,
|
|
761
|
-
);
|
|
762
|
-
totalErrors++;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Check if we need to fetch the next page
|
|
767
|
-
if (pedimentoPage.length < pageSize) {
|
|
768
|
-
hasMoreData = false;
|
|
769
|
-
logger.info(
|
|
770
|
-
`Completed processing. Last page ${pageNumber} had ${pedimentoPage.length} records`,
|
|
771
|
-
);
|
|
772
|
-
} else {
|
|
773
|
-
offset += pageSize;
|
|
774
|
-
pageNumber++;
|
|
775
|
-
logger.info(
|
|
776
|
-
`Page ${pageNumber - 1} complete: ${pedimentoPage.length} records processed, moving to next page...`,
|
|
777
|
-
);
|
|
778
|
-
}
|
|
738
|
+
if (!result.success) {
|
|
739
|
+
const errorMsg = `Backend propagation failed: ${result.error || 'Unknown error'}`;
|
|
740
|
+
logger.error(errorMsg);
|
|
741
|
+
throw new Error(errorMsg);
|
|
779
742
|
}
|
|
780
743
|
|
|
781
|
-
//
|
|
782
|
-
|
|
744
|
+
// Display results
|
|
745
|
+
const { processedCount = 0, updatedCount = 0, errorCount = 0 } = result;
|
|
746
|
+
|
|
747
|
+
if (processedCount === 0) {
|
|
783
748
|
logger.info('No pedimento_simplificado records with arela_path found');
|
|
784
749
|
console.log(
|
|
785
750
|
'ℹ️ No pedimento_simplificado records with arela_path found',
|
|
786
751
|
);
|
|
787
752
|
} else {
|
|
788
|
-
console.log(
|
|
789
|
-
|
|
790
|
-
);
|
|
791
|
-
logger.info(
|
|
792
|
-
`Processed ${totalProcessed} pedimento records across ${pageNumber - 1} pages`,
|
|
793
|
-
);
|
|
753
|
+
console.log(`📋 Processed ${processedCount} pedimento records`);
|
|
754
|
+
logger.info(`Processed ${processedCount} pedimento records`);
|
|
794
755
|
}
|
|
795
756
|
|
|
796
|
-
const result = {
|
|
797
|
-
processedCount: totalProcessed,
|
|
798
|
-
updatedCount: totalUpdated,
|
|
799
|
-
errorCount: totalErrors,
|
|
800
|
-
};
|
|
801
|
-
|
|
802
757
|
logger.success(
|
|
803
|
-
`Phase 3 Summary: ${
|
|
758
|
+
`Phase 3 Summary: ${processedCount} pedimentos processed, ${updatedCount} files updated with arela_path and year, ${errorCount} errors`,
|
|
804
759
|
);
|
|
805
760
|
|
|
806
|
-
return
|
|
761
|
+
return {
|
|
762
|
+
processedCount,
|
|
763
|
+
updatedCount,
|
|
764
|
+
errorCount,
|
|
765
|
+
};
|
|
807
766
|
}
|
|
808
767
|
|
|
809
768
|
/**
|
|
810
769
|
* Upload files to Arela API based on specific RFC values
|
|
770
|
+
* Supports cross-tenant mode where source and target APIs can be different
|
|
811
771
|
* @param {Object} options - Upload options
|
|
772
|
+
* @param {string} options.sourceApi - Source API target for reading data (cross-tenant mode)
|
|
773
|
+
* @param {string} options.targetApi - Target API target for uploading files (cross-tenant mode)
|
|
774
|
+
* @param {string} options.apiTarget - Single API target for both reading and uploading (single API mode)
|
|
812
775
|
* @returns {Promise<Object>} Processing result
|
|
813
776
|
*/
|
|
814
777
|
async uploadFilesByRfc(options = {}) {
|
|
815
|
-
const supabase = await this.#getSupabaseClient();
|
|
816
|
-
const uploadService = await uploadServiceFactory.getUploadService();
|
|
817
|
-
|
|
818
778
|
// Get configuration
|
|
819
779
|
const appConfig = await import('../config/config.js').then(
|
|
820
780
|
(m) => m.appConfig,
|
|
821
781
|
);
|
|
822
782
|
|
|
783
|
+
// Determine if we're in cross-tenant mode
|
|
784
|
+
const isCrossTenant =
|
|
785
|
+
options.sourceApi &&
|
|
786
|
+
options.targetApi &&
|
|
787
|
+
options.sourceApi !== options.targetApi;
|
|
788
|
+
|
|
789
|
+
// Determine if we're in single API mode with specific target
|
|
790
|
+
const isSingleApiMode = !isCrossTenant && options.apiTarget;
|
|
791
|
+
|
|
792
|
+
let sourceService, targetService;
|
|
793
|
+
|
|
794
|
+
if (isCrossTenant) {
|
|
795
|
+
console.log('🔀 Cross-tenant upload mode enabled');
|
|
796
|
+
console.log(` 📖 Source API: ${options.sourceApi}`);
|
|
797
|
+
console.log(` 📝 Target API: ${options.targetApi}`);
|
|
798
|
+
|
|
799
|
+
// Get separate services for source and target
|
|
800
|
+
sourceService = await uploadServiceFactory.getApiServiceForTarget(
|
|
801
|
+
options.sourceApi,
|
|
802
|
+
);
|
|
803
|
+
targetService = await uploadServiceFactory.getApiServiceForTarget(
|
|
804
|
+
options.targetApi,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
// Verify both services are available
|
|
808
|
+
if (!(await sourceService.isAvailable())) {
|
|
809
|
+
throw new Error(`Source API '${options.sourceApi}' is not available`);
|
|
810
|
+
}
|
|
811
|
+
if (!(await targetService.isAvailable())) {
|
|
812
|
+
throw new Error(`Target API '${options.targetApi}' is not available`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
console.log(`✅ Connected to source: ${sourceService.baseUrl}`);
|
|
816
|
+
console.log(`✅ Connected to target: ${targetService.baseUrl}`);
|
|
817
|
+
} else if (isSingleApiMode) {
|
|
818
|
+
// Single API mode with specific target - use the same service for both
|
|
819
|
+
console.log(`🎯 Single API mode: ${options.apiTarget}`);
|
|
820
|
+
|
|
821
|
+
const apiService = await uploadServiceFactory.getApiServiceForTarget(
|
|
822
|
+
options.apiTarget,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (!(await apiService.isAvailable())) {
|
|
826
|
+
throw new Error(`API '${options.apiTarget}' is not available`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
console.log(`✅ Connected to: ${apiService.baseUrl}`);
|
|
830
|
+
sourceService = apiService;
|
|
831
|
+
targetService = apiService;
|
|
832
|
+
} else {
|
|
833
|
+
// Default mode - use the default service for both
|
|
834
|
+
const apiService = await uploadServiceFactory.getUploadService();
|
|
835
|
+
|
|
836
|
+
if (apiService.getServiceName() !== 'Arela API') {
|
|
837
|
+
throw new Error(
|
|
838
|
+
'API service is required for RFC-based upload. Please configure ARELA_API_URL and ARELA_API_TOKEN.',
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
sourceService = apiService;
|
|
843
|
+
targetService = apiService;
|
|
844
|
+
}
|
|
845
|
+
|
|
823
846
|
if (!appConfig.upload.rfcs || appConfig.upload.rfcs.length === 0) {
|
|
824
847
|
const errorMsg =
|
|
825
848
|
'No RFCs specified. Please set UPLOAD_RFCS environment variable with pipe-separated RFC values.';
|
|
@@ -828,16 +851,18 @@ export class DatabaseService {
|
|
|
828
851
|
}
|
|
829
852
|
|
|
830
853
|
logger.info('Phase 4: Starting RFC-based upload process...');
|
|
854
|
+
logger.info(
|
|
855
|
+
`Using ${isCrossTenant ? 'cross-tenant' : 'standard'} API service for RFC-based upload...`,
|
|
856
|
+
);
|
|
831
857
|
console.log('🎯 RFC-based Upload Mode');
|
|
832
858
|
console.log(`📋 Target RFCs: ${appConfig.upload.rfcs.join(', ')}`);
|
|
833
859
|
console.log('🔍 Searching for files to upload...');
|
|
834
860
|
|
|
835
861
|
// First, count total files for the RFCs to show filtering effect
|
|
836
|
-
const { count: totalRfcFiles, error: countError } =
|
|
837
|
-
.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
.not('arela_path', 'is', null);
|
|
862
|
+
const { count: totalRfcFiles, error: countError } =
|
|
863
|
+
await sourceService.fetchRfcFileCount({
|
|
864
|
+
rfcs: appConfig.upload.rfcs,
|
|
865
|
+
});
|
|
841
866
|
|
|
842
867
|
if (countError) {
|
|
843
868
|
logger.warn(`Could not count total RFC files: ${countError.message}`);
|
|
@@ -863,22 +888,13 @@ export class DatabaseService {
|
|
|
863
888
|
`Fetching pedimento records page ${pageNumber} (records ${offset + 1} to ${offset + pageSize})...`,
|
|
864
889
|
);
|
|
865
890
|
|
|
866
|
-
let pedimentoQuery = supabase
|
|
867
|
-
.from('uploader')
|
|
868
|
-
.select('arela_path')
|
|
869
|
-
.eq('document_type', 'pedimento_simplificado')
|
|
870
|
-
.in('rfc', appConfig.upload.rfcs)
|
|
871
|
-
.not('arela_path', 'is', null);
|
|
872
|
-
|
|
873
|
-
// Add year filter if UPLOAD_YEARS is configured
|
|
874
|
-
if (appConfig.upload.years && appConfig.upload.years.length > 0) {
|
|
875
|
-
pedimentoQuery = pedimentoQuery.in('year', appConfig.upload.years);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
891
|
const { data: pedimentoPage, error: pedimentoError } =
|
|
879
|
-
await
|
|
880
|
-
.
|
|
881
|
-
.
|
|
892
|
+
await sourceService.fetchPedimentosByRfc({
|
|
893
|
+
rfcs: appConfig.upload.rfcs,
|
|
894
|
+
years: appConfig.upload.years || [],
|
|
895
|
+
offset,
|
|
896
|
+
limit: pageSize,
|
|
897
|
+
});
|
|
882
898
|
|
|
883
899
|
if (pedimentoError) {
|
|
884
900
|
const errorMsg = `Error fetching pedimento RFC records page ${pageNumber}: ${pedimentoError.message}`;
|
|
@@ -926,11 +942,13 @@ export class DatabaseService {
|
|
|
926
942
|
const uniqueArelaPaths = [
|
|
927
943
|
...new Set(allPedimentoRecords.map((r) => r.arela_path)),
|
|
928
944
|
];
|
|
945
|
+
// pageNumber represents the current page (starts at 1)
|
|
946
|
+
const totalPages = pageNumber;
|
|
929
947
|
console.log(
|
|
930
|
-
`📋 Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths for specified RFCs across ${
|
|
948
|
+
`📋 Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths for specified RFCs across ${totalPages} page${totalPages !== 1 ? 's' : ''}`,
|
|
931
949
|
);
|
|
932
950
|
logger.info(
|
|
933
|
-
`Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths across ${
|
|
951
|
+
`Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths across ${totalPages} page${totalPages !== 1 ? 's' : ''}`,
|
|
934
952
|
);
|
|
935
953
|
|
|
936
954
|
// Step 2: Process files with optimized single query per chunk
|
|
@@ -940,6 +958,7 @@ export class DatabaseService {
|
|
|
940
958
|
let globalFileCount = 0;
|
|
941
959
|
const arelaPathChunkSize = 50;
|
|
942
960
|
const batchSize = parseInt(options.batchSize) || 10;
|
|
961
|
+
const filePageSize = 1000; // Supabase limit per request
|
|
943
962
|
|
|
944
963
|
// Import performance configuration
|
|
945
964
|
const { performance: perfConfig } = appConfig;
|
|
@@ -959,28 +978,59 @@ export class DatabaseService {
|
|
|
959
978
|
` Processing arela_path chunk ${chunkNumber}/${totalChunks} (${arelaPathChunk.length} paths)`,
|
|
960
979
|
);
|
|
961
980
|
|
|
962
|
-
// Fetch all files for this chunk
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
981
|
+
// Fetch all files for this chunk with pagination to handle >1000 records
|
|
982
|
+
let allChunkFiles = [];
|
|
983
|
+
let fileOffset = 0;
|
|
984
|
+
let hasMoreFiles = true;
|
|
985
|
+
let filePageNum = 1;
|
|
986
|
+
|
|
987
|
+
while (hasMoreFiles) {
|
|
988
|
+
const { data: batch, error: queryError } =
|
|
989
|
+
await sourceService.fetchFilesForUpload({
|
|
990
|
+
arelaPaths: arelaPathChunk,
|
|
991
|
+
offset: fileOffset,
|
|
992
|
+
limit: filePageSize,
|
|
993
|
+
});
|
|
969
994
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
995
|
+
if (queryError) {
|
|
996
|
+
const errorMsg = `Error fetching files for chunk ${chunkNumber} page ${filePageNum}: ${queryError.message}`;
|
|
997
|
+
logger.error(errorMsg);
|
|
998
|
+
throw new Error(errorMsg);
|
|
999
|
+
}
|
|
975
1000
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1001
|
+
if (!batch || batch.length === 0) {
|
|
1002
|
+
hasMoreFiles = false;
|
|
1003
|
+
if (filePageNum === 1) {
|
|
1004
|
+
// No files found at all for this chunk
|
|
1005
|
+
console.log(
|
|
1006
|
+
` ℹ️ Chunk ${chunkNumber}/${totalChunks}: No files to upload`,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
logger.debug(
|
|
1013
|
+
`Chunk ${chunkNumber} page ${filePageNum}: fetched ${batch.length} files`,
|
|
979
1014
|
);
|
|
1015
|
+
allChunkFiles = allChunkFiles.concat(batch);
|
|
1016
|
+
|
|
1017
|
+
// Check if we need more pages
|
|
1018
|
+
if (batch.length < filePageSize) {
|
|
1019
|
+
hasMoreFiles = false;
|
|
1020
|
+
logger.debug(
|
|
1021
|
+
`Chunk ${chunkNumber}: Completed pagination with ${allChunkFiles.length} total files`,
|
|
1022
|
+
);
|
|
1023
|
+
} else {
|
|
1024
|
+
fileOffset += filePageSize;
|
|
1025
|
+
filePageNum++;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (allChunkFiles.length === 0) {
|
|
980
1030
|
continue;
|
|
981
1031
|
}
|
|
982
1032
|
|
|
983
|
-
const chunkFileCount =
|
|
1033
|
+
const chunkFileCount = allChunkFiles.length;
|
|
984
1034
|
globalFileCount += chunkFileCount;
|
|
985
1035
|
|
|
986
1036
|
console.log(
|
|
@@ -989,20 +1039,21 @@ export class DatabaseService {
|
|
|
989
1039
|
|
|
990
1040
|
// Process this batch of files immediately using concurrent processing
|
|
991
1041
|
// Split batch into upload batches
|
|
992
|
-
for (let j = 0; j <
|
|
993
|
-
const uploadBatch =
|
|
1042
|
+
for (let j = 0; j < allChunkFiles.length; j += batchSize) {
|
|
1043
|
+
const uploadBatch = allChunkFiles.slice(j, j + batchSize);
|
|
994
1044
|
const batchNum = Math.floor(j / batchSize) + 1;
|
|
995
|
-
const totalBatches = Math.ceil(
|
|
1045
|
+
const totalBatches = Math.ceil(allChunkFiles.length / batchSize);
|
|
996
1046
|
|
|
997
1047
|
console.log(
|
|
998
1048
|
` 📦 Processing upload batch ${batchNum}/${totalBatches} within chunk ${chunkNumber} (${uploadBatch.length} files)`,
|
|
999
1049
|
);
|
|
1000
1050
|
|
|
1001
1051
|
// Process batch using concurrent processing similar to UploadCommand
|
|
1052
|
+
// In cross-tenant mode: targetService for uploading, sourceService for reading
|
|
1002
1053
|
const batchResults = await this.#processRfcBatch(
|
|
1003
1054
|
uploadBatch,
|
|
1004
|
-
|
|
1005
|
-
|
|
1055
|
+
targetService, // Used for uploading files
|
|
1056
|
+
sourceService, // Used for reading metadata
|
|
1006
1057
|
options,
|
|
1007
1058
|
maxConcurrency,
|
|
1008
1059
|
);
|
|
@@ -1061,7 +1112,14 @@ export class DatabaseService {
|
|
|
1061
1112
|
* @returns {Promise<Array>} Array of files ready for upload
|
|
1062
1113
|
*/
|
|
1063
1114
|
async getFilesReadyForUpload(options = {}) {
|
|
1064
|
-
|
|
1115
|
+
// Get API service
|
|
1116
|
+
const apiService = await uploadServiceFactory.getUploadService();
|
|
1117
|
+
|
|
1118
|
+
if (apiService.getServiceName() !== 'Arela API') {
|
|
1119
|
+
throw new Error(
|
|
1120
|
+
'API service is required for querying files. Please configure ARELA_API_URL and ARELA_API_TOKEN.',
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1065
1123
|
|
|
1066
1124
|
logger.info('Querying files ready for upload...');
|
|
1067
1125
|
console.log('🔍 Querying files ready for upload...');
|
|
@@ -1085,23 +1143,13 @@ export class DatabaseService {
|
|
|
1085
1143
|
'🎯 Finding pedimento_simplificado documents for specified RFCs with arela_path...',
|
|
1086
1144
|
);
|
|
1087
1145
|
|
|
1088
|
-
let pedimentoReadyQuery = supabase
|
|
1089
|
-
.from('uploader')
|
|
1090
|
-
.select('arela_path')
|
|
1091
|
-
.eq('document_type', 'pedimento_simplificado')
|
|
1092
|
-
.in('rfc', uploadRfcs)
|
|
1093
|
-
.not('arela_path', 'is', null);
|
|
1094
|
-
|
|
1095
|
-
// Add year filter if UPLOAD_YEARS is configured
|
|
1096
|
-
if (appConfig.upload.years && appConfig.upload.years.length > 0) {
|
|
1097
|
-
pedimentoReadyQuery = pedimentoReadyQuery.in(
|
|
1098
|
-
'year',
|
|
1099
|
-
appConfig.upload.years,
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
1146
|
const { data: pedimentoRecords, error: pedimentoError } =
|
|
1104
|
-
await
|
|
1147
|
+
await apiService.fetchPedimentosByRfc({
|
|
1148
|
+
rfcs: uploadRfcs,
|
|
1149
|
+
years: appConfig.upload.years || [],
|
|
1150
|
+
offset: 0,
|
|
1151
|
+
limit: 10000, // Fetch all pedimentos in one go for this query
|
|
1152
|
+
});
|
|
1105
1153
|
|
|
1106
1154
|
if (pedimentoError) {
|
|
1107
1155
|
throw new Error(
|
|
@@ -1134,22 +1182,19 @@ export class DatabaseService {
|
|
|
1134
1182
|
for (let i = 0; i < uniqueArelaPaths.length; i += chunkSize) {
|
|
1135
1183
|
const pathChunk = uniqueArelaPaths.slice(i, i + chunkSize);
|
|
1136
1184
|
|
|
1137
|
-
// Query with pagination to get all results
|
|
1185
|
+
// Query with pagination to get all results
|
|
1138
1186
|
let chunkFiles = [];
|
|
1139
1187
|
let from = 0;
|
|
1140
1188
|
const pageSize = 1000;
|
|
1141
1189
|
let hasMoreData = true;
|
|
1142
1190
|
|
|
1143
1191
|
while (hasMoreData) {
|
|
1144
|
-
const { data: pageData, error: chunkError } =
|
|
1145
|
-
.
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
.neq('status', 'file-uploaded')
|
|
1151
|
-
.not('original_path', 'is', null)
|
|
1152
|
-
.range(from, from + pageSize - 1);
|
|
1192
|
+
const { data: pageData, error: chunkError } =
|
|
1193
|
+
await apiService.fetchFilesForUpload({
|
|
1194
|
+
arelaPaths: pathChunk,
|
|
1195
|
+
offset: from,
|
|
1196
|
+
limit: pageSize,
|
|
1197
|
+
});
|
|
1153
1198
|
|
|
1154
1199
|
if (chunkError) {
|
|
1155
1200
|
throw new Error(
|
|
@@ -1213,7 +1258,7 @@ export class DatabaseService {
|
|
|
1213
1258
|
* Process a batch of files using concurrent processing for RFC uploads
|
|
1214
1259
|
* @param {Array} files - Files to process in this batch
|
|
1215
1260
|
* @param {Object} uploadService - Upload service instance
|
|
1216
|
-
* @param {Object}
|
|
1261
|
+
* @param {Object} apiService - API service instance for database updates
|
|
1217
1262
|
* @param {Object} options - Upload options
|
|
1218
1263
|
* @param {number} maxConcurrency - Maximum concurrent operations
|
|
1219
1264
|
* @returns {Promise<Object>} Batch processing results
|
|
@@ -1221,7 +1266,7 @@ export class DatabaseService {
|
|
|
1221
1266
|
async #processRfcBatch(
|
|
1222
1267
|
files,
|
|
1223
1268
|
uploadService,
|
|
1224
|
-
|
|
1269
|
+
apiService,
|
|
1225
1270
|
options,
|
|
1226
1271
|
maxConcurrency,
|
|
1227
1272
|
) {
|
|
@@ -1245,7 +1290,7 @@ export class DatabaseService {
|
|
|
1245
1290
|
return await this.#processRfcSingleFile(
|
|
1246
1291
|
file,
|
|
1247
1292
|
uploadService,
|
|
1248
|
-
|
|
1293
|
+
apiService,
|
|
1249
1294
|
options,
|
|
1250
1295
|
fs,
|
|
1251
1296
|
);
|
|
@@ -1291,7 +1336,7 @@ export class DatabaseService {
|
|
|
1291
1336
|
return await this.#processRfcApiBatch(
|
|
1292
1337
|
chunk,
|
|
1293
1338
|
uploadService,
|
|
1294
|
-
|
|
1339
|
+
apiService,
|
|
1295
1340
|
options,
|
|
1296
1341
|
fs,
|
|
1297
1342
|
);
|
|
@@ -1319,20 +1364,20 @@ export class DatabaseService {
|
|
|
1319
1364
|
/**
|
|
1320
1365
|
* Process a single file for RFC upload (Supabase mode)
|
|
1321
1366
|
*/
|
|
1322
|
-
async #processRfcSingleFile(file, uploadService,
|
|
1367
|
+
async #processRfcSingleFile(file, uploadService, apiService, options, fs) {
|
|
1323
1368
|
try {
|
|
1324
1369
|
// Check if file exists
|
|
1325
1370
|
if (!fs.existsSync(file.original_path)) {
|
|
1326
1371
|
logger.warn(
|
|
1327
1372
|
`File not found: ${file.filename} at ${file.original_path}`,
|
|
1328
1373
|
);
|
|
1329
|
-
await
|
|
1330
|
-
|
|
1331
|
-
|
|
1374
|
+
await apiService.updateFileStatus([
|
|
1375
|
+
{
|
|
1376
|
+
id: file.id,
|
|
1332
1377
|
status: 'file-not-found',
|
|
1333
1378
|
message: 'File no longer exists at original path',
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1379
|
+
},
|
|
1380
|
+
]);
|
|
1336
1381
|
return { success: false, error: 'File not found' };
|
|
1337
1382
|
}
|
|
1338
1383
|
|
|
@@ -1359,24 +1404,25 @@ export class DatabaseService {
|
|
|
1359
1404
|
|
|
1360
1405
|
// Check upload result before updating database status
|
|
1361
1406
|
if (uploadResult.success) {
|
|
1362
|
-
await
|
|
1363
|
-
|
|
1364
|
-
|
|
1407
|
+
await apiService.updateFileStatus([
|
|
1408
|
+
{
|
|
1409
|
+
id: file.id,
|
|
1365
1410
|
status: 'file-uploaded',
|
|
1366
1411
|
message: 'Successfully uploaded to Supabase',
|
|
1367
|
-
|
|
1368
|
-
|
|
1412
|
+
processing_status: 'UPLOADED',
|
|
1413
|
+
},
|
|
1414
|
+
]);
|
|
1369
1415
|
|
|
1370
1416
|
logger.info(`✅ Uploaded: ${file.filename}`);
|
|
1371
1417
|
return { success: true, filename: file.filename };
|
|
1372
1418
|
} else {
|
|
1373
|
-
await
|
|
1374
|
-
|
|
1375
|
-
|
|
1419
|
+
await apiService.updateFileStatus([
|
|
1420
|
+
{
|
|
1421
|
+
id: file.id,
|
|
1376
1422
|
status: 'upload-error',
|
|
1377
1423
|
message: `Upload failed: ${uploadResult.error}`,
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1424
|
+
},
|
|
1425
|
+
]);
|
|
1380
1426
|
|
|
1381
1427
|
logger.error(
|
|
1382
1428
|
`❌ Upload failed: ${file.filename} - ${uploadResult.error}`,
|
|
@@ -1392,13 +1438,13 @@ export class DatabaseService {
|
|
|
1392
1438
|
`❌ Error processing file ${file.filename}: ${error.message}`,
|
|
1393
1439
|
);
|
|
1394
1440
|
|
|
1395
|
-
await
|
|
1396
|
-
|
|
1397
|
-
|
|
1441
|
+
await apiService.updateFileStatus([
|
|
1442
|
+
{
|
|
1443
|
+
id: file.id,
|
|
1398
1444
|
status: 'upload-error',
|
|
1399
1445
|
message: `Processing error: ${error.message}`,
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1446
|
+
},
|
|
1447
|
+
]);
|
|
1402
1448
|
|
|
1403
1449
|
return { success: false, error: error.message, filename: file.filename };
|
|
1404
1450
|
}
|
|
@@ -1407,7 +1453,7 @@ export class DatabaseService {
|
|
|
1407
1453
|
/**
|
|
1408
1454
|
* Process multiple files in a single API batch call (API service mode)
|
|
1409
1455
|
*/
|
|
1410
|
-
async #processRfcApiBatch(files, uploadService,
|
|
1456
|
+
async #processRfcApiBatch(files, uploadService, apiService, options, fs) {
|
|
1411
1457
|
let processed = 0;
|
|
1412
1458
|
let uploaded = 0;
|
|
1413
1459
|
let errors = 0;
|
|
@@ -1439,15 +1485,15 @@ export class DatabaseService {
|
|
|
1439
1485
|
}
|
|
1440
1486
|
|
|
1441
1487
|
// Update invalid files in database
|
|
1442
|
-
|
|
1443
|
-
await
|
|
1444
|
-
.
|
|
1445
|
-
|
|
1488
|
+
if (invalidFiles.length > 0) {
|
|
1489
|
+
await apiService.updateFileStatus(
|
|
1490
|
+
invalidFiles.map((file) => ({
|
|
1491
|
+
id: file.id,
|
|
1446
1492
|
status: 'file-not-found',
|
|
1447
1493
|
message: 'File no longer exists at original path',
|
|
1448
|
-
})
|
|
1449
|
-
|
|
1450
|
-
errors
|
|
1494
|
+
})),
|
|
1495
|
+
);
|
|
1496
|
+
errors += invalidFiles.length;
|
|
1451
1497
|
}
|
|
1452
1498
|
|
|
1453
1499
|
// Process valid files in batch if any exist
|
|
@@ -1464,9 +1510,15 @@ export class DatabaseService {
|
|
|
1464
1510
|
}
|
|
1465
1511
|
|
|
1466
1512
|
// Make single API call with multiple files
|
|
1513
|
+
// Include RFC for multi-database routing (required for cross-tenant uploads)
|
|
1467
1514
|
const uploadResult = await uploadService.upload(
|
|
1468
1515
|
validFiles.map((f) => f.fileData),
|
|
1469
|
-
{
|
|
1516
|
+
{
|
|
1517
|
+
folderStructure: fullFolderStructure,
|
|
1518
|
+
rfc: sampleFile.rfc, // For cross-tenant: routes to correct client DB
|
|
1519
|
+
autoDetect: true, // Enable detection on target API
|
|
1520
|
+
autoOrganize: true, // Enable organization on target API
|
|
1521
|
+
},
|
|
1470
1522
|
);
|
|
1471
1523
|
|
|
1472
1524
|
if (uploadResult.success && uploadResult.data) {
|
|
@@ -1497,6 +1549,9 @@ export class DatabaseService {
|
|
|
1497
1549
|
`🔍 Expected filenames: ${Array.from(fileNameToRecord.keys()).join(', ')}`,
|
|
1498
1550
|
);
|
|
1499
1551
|
|
|
1552
|
+
// Prepare status updates
|
|
1553
|
+
const statusUpdates = [];
|
|
1554
|
+
|
|
1500
1555
|
// Handle successfully uploaded files
|
|
1501
1556
|
if (apiResult.uploaded && apiResult.uploaded.length > 0) {
|
|
1502
1557
|
const successfulFileIds = [];
|
|
@@ -1521,6 +1576,13 @@ export class DatabaseService {
|
|
|
1521
1576
|
successfulFileIds.push(dbRecord.id);
|
|
1522
1577
|
matchedFilenames.push(possibleFilename);
|
|
1523
1578
|
logger.debug(`✅ Matched file: ${possibleFilename}`);
|
|
1579
|
+
|
|
1580
|
+
statusUpdates.push({
|
|
1581
|
+
id: dbRecord.id,
|
|
1582
|
+
status: 'file-uploaded',
|
|
1583
|
+
message: 'Successfully uploaded to Arela API (batch)',
|
|
1584
|
+
processing_status: 'UPLOADED',
|
|
1585
|
+
});
|
|
1524
1586
|
} else {
|
|
1525
1587
|
logger.warn(
|
|
1526
1588
|
`⚠️ Could not match uploaded file with any known filename: ${JSON.stringify(uploadedFile)}`,
|
|
@@ -1536,29 +1598,21 @@ export class DatabaseService {
|
|
|
1536
1598
|
logger.warn(
|
|
1537
1599
|
`🔄 Fallback: No individual file matches found, but API indicates ${apiResult.uploaded.length} uploads. Marking all ${validFiles.length} batch files as uploaded.`,
|
|
1538
1600
|
);
|
|
1539
|
-
validFiles.forEach((f) =>
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (successfulFileIds.length > 0) {
|
|
1543
|
-
await supabase
|
|
1544
|
-
.from('uploader')
|
|
1545
|
-
.update({
|
|
1601
|
+
validFiles.forEach((f) => {
|
|
1602
|
+
statusUpdates.push({
|
|
1603
|
+
id: f.dbRecord.id,
|
|
1546
1604
|
status: 'file-uploaded',
|
|
1547
1605
|
message: 'Successfully uploaded to Arela API (batch)',
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
uploaded += successfulFileIds.length;
|
|
1552
|
-
logger.info(
|
|
1553
|
-
`✅ Batch upload successful: ${successfulFileIds.length} files uploaded`,
|
|
1554
|
-
);
|
|
1606
|
+
processing_status: 'UPLOADED',
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1555
1609
|
}
|
|
1610
|
+
|
|
1611
|
+
uploaded += successfulFileIds.length || validFiles.length;
|
|
1556
1612
|
}
|
|
1557
1613
|
|
|
1558
1614
|
// Handle failed files
|
|
1559
1615
|
if (apiResult.errors && apiResult.errors.length > 0) {
|
|
1560
|
-
const failedFileIds = [];
|
|
1561
|
-
|
|
1562
1616
|
apiResult.errors.forEach((errorInfo) => {
|
|
1563
1617
|
// Try multiple possible property names for filename in errors
|
|
1564
1618
|
const possibleFilename =
|
|
@@ -1571,32 +1625,21 @@ export class DatabaseService {
|
|
|
1571
1625
|
|
|
1572
1626
|
const dbRecord = fileNameToRecord.get(possibleFilename);
|
|
1573
1627
|
if (dbRecord) {
|
|
1574
|
-
|
|
1628
|
+
statusUpdates.push({
|
|
1629
|
+
id: dbRecord.id,
|
|
1630
|
+
status: 'upload-error',
|
|
1631
|
+
message: `Upload failed: ${errorInfo.error || 'Unknown error'}`,
|
|
1632
|
+
});
|
|
1633
|
+
errors++;
|
|
1575
1634
|
} else {
|
|
1576
1635
|
logger.warn(
|
|
1577
1636
|
`⚠️ Could not match error file: ${JSON.stringify(errorInfo)}`,
|
|
1578
1637
|
);
|
|
1579
1638
|
}
|
|
1580
1639
|
});
|
|
1581
|
-
|
|
1582
|
-
if (failedFileIds.length > 0) {
|
|
1583
|
-
await supabase
|
|
1584
|
-
.from('uploader')
|
|
1585
|
-
.update({
|
|
1586
|
-
status: 'upload-error',
|
|
1587
|
-
message: `Upload failed: ${apiResult.errors[0].error}`,
|
|
1588
|
-
})
|
|
1589
|
-
.in('id', failedFileIds);
|
|
1590
|
-
|
|
1591
|
-
errors += failedFileIds.length;
|
|
1592
|
-
logger.error(
|
|
1593
|
-
`❌ Batch upload errors: ${failedFileIds.length} files failed`,
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
1640
|
}
|
|
1597
1641
|
|
|
1598
1642
|
// Handle any remaining files that weren't in uploaded or errors arrays
|
|
1599
|
-
// Use robust filename extraction for both uploaded and error files
|
|
1600
1643
|
const extractFilename = (fileObj) => {
|
|
1601
1644
|
return (
|
|
1602
1645
|
fileObj.fileName ||
|
|
@@ -1616,26 +1659,24 @@ export class DatabaseService {
|
|
|
1616
1659
|
const unprocessedFiles = validFiles.filter(
|
|
1617
1660
|
(f) => !processedFileNames.has(f.fileData.name),
|
|
1618
1661
|
);
|
|
1662
|
+
|
|
1619
1663
|
if (unprocessedFiles.length > 0) {
|
|
1620
|
-
// Only mark as unprocessed if we haven't already handled all files through fallback logic
|
|
1621
|
-
// If we used fallback (all files marked as uploaded), don't mark any as unprocessed
|
|
1622
1664
|
const alreadyHandledCount = uploaded + errors;
|
|
1623
1665
|
const shouldMarkUnprocessed =
|
|
1624
1666
|
alreadyHandledCount < validFiles.length;
|
|
1625
1667
|
|
|
1626
1668
|
if (shouldMarkUnprocessed) {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
.update({
|
|
1669
|
+
unprocessedFiles.forEach((f) => {
|
|
1670
|
+
statusUpdates.push({
|
|
1671
|
+
id: f.dbRecord.id,
|
|
1631
1672
|
status: 'upload-error',
|
|
1632
1673
|
message: 'File not found in API response',
|
|
1633
|
-
})
|
|
1634
|
-
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
errors += unprocessedFiles.length;
|
|
1635
1677
|
|
|
1636
|
-
errors += unprocessedIds.length;
|
|
1637
1678
|
logger.warn(
|
|
1638
|
-
`⚠️ Unprocessed files: ${
|
|
1679
|
+
`⚠️ Unprocessed files: ${unprocessedFiles.length} files not found in API response`,
|
|
1639
1680
|
);
|
|
1640
1681
|
logger.debug(
|
|
1641
1682
|
`🔍 API response uploaded array: ${JSON.stringify(apiResult.uploaded)}`,
|
|
@@ -1649,16 +1690,30 @@ export class DatabaseService {
|
|
|
1649
1690
|
);
|
|
1650
1691
|
}
|
|
1651
1692
|
}
|
|
1693
|
+
|
|
1694
|
+
// Batch update all status changes
|
|
1695
|
+
if (statusUpdates.length > 0) {
|
|
1696
|
+
const updateResult =
|
|
1697
|
+
await apiService.updateFileStatus(statusUpdates);
|
|
1698
|
+
if (!updateResult.success) {
|
|
1699
|
+
logger.error(
|
|
1700
|
+
`Some status updates failed: ${updateResult.errors?.length || 0} errors`,
|
|
1701
|
+
);
|
|
1702
|
+
} else {
|
|
1703
|
+
logger.info(
|
|
1704
|
+
`✅ Batch upload successful: ${uploaded} files uploaded`,
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1652
1708
|
} else {
|
|
1653
1709
|
// Complete batch failure - mark all files as failed
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
.
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
.in('id', fileIds);
|
|
1710
|
+
const failureUpdates = validFiles.map((f) => ({
|
|
1711
|
+
id: f.dbRecord.id,
|
|
1712
|
+
status: 'upload-error',
|
|
1713
|
+
message: uploadResult.error || 'Batch upload failed',
|
|
1714
|
+
}));
|
|
1715
|
+
|
|
1716
|
+
await apiService.updateFileStatus(failureUpdates);
|
|
1662
1717
|
|
|
1663
1718
|
errors += validFiles.length;
|
|
1664
1719
|
logger.error(
|
|
@@ -1670,20 +1725,445 @@ export class DatabaseService {
|
|
|
1670
1725
|
logger.error(`❌ Error processing batch: ${error.message}`);
|
|
1671
1726
|
|
|
1672
1727
|
// Mark all files as failed
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
.
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
.in('id', fileIds);
|
|
1728
|
+
const failureUpdates = files.map((f) => ({
|
|
1729
|
+
id: f.id,
|
|
1730
|
+
status: 'upload-error',
|
|
1731
|
+
message: `Batch processing error: ${error.message}`,
|
|
1732
|
+
}));
|
|
1733
|
+
|
|
1734
|
+
await apiService.updateFileStatus(failureUpdates);
|
|
1681
1735
|
|
|
1682
1736
|
errors += files.length;
|
|
1683
1737
|
}
|
|
1684
1738
|
|
|
1685
1739
|
return { processed, uploaded, errors };
|
|
1686
1740
|
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Insert upload session event into watch_uploads table
|
|
1744
|
+
* @param {Object} uploadEvent - Upload event from LoggingService
|
|
1745
|
+
* @param {string} sessionId - Session ID for tracking
|
|
1746
|
+
* @returns {Promise<Object>} Inserted record
|
|
1747
|
+
*/
|
|
1748
|
+
async insertUploadEvent(uploadEvent, sessionId) {
|
|
1749
|
+
const supabase = await this.#getSupabaseClient();
|
|
1750
|
+
|
|
1751
|
+
const record = {
|
|
1752
|
+
session_id: sessionId,
|
|
1753
|
+
timestamp: uploadEvent.timestamp || new Date().toISOString(),
|
|
1754
|
+
strategy: uploadEvent.strategy, // 'individual', 'batch', 'full-structure'
|
|
1755
|
+
file_count: uploadEvent.fileCount || 0,
|
|
1756
|
+
success_count: uploadEvent.successCount || 0,
|
|
1757
|
+
failure_count: uploadEvent.failureCount || 0,
|
|
1758
|
+
retry_count: uploadEvent.retryCount || 0,
|
|
1759
|
+
duration_ms: uploadEvent.duration || 0,
|
|
1760
|
+
status: uploadEvent.status || 'completed',
|
|
1761
|
+
metadata: uploadEvent.metadata || null,
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
try {
|
|
1765
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
1766
|
+
return await supabase.from('watch_uploads').insert([record]).select();
|
|
1767
|
+
}, `insert upload event for session ${sessionId}`);
|
|
1768
|
+
|
|
1769
|
+
if (error) {
|
|
1770
|
+
logger.error(`Failed to insert upload event: ${error.message}`);
|
|
1771
|
+
throw error;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
return data[0];
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
logger.error(`Error inserting upload event: ${error.message}`);
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Insert retry event into watch_events table
|
|
1783
|
+
* @param {string} uploadEventId - ID of the parent upload event
|
|
1784
|
+
* @param {string} sessionId - Session ID for tracking
|
|
1785
|
+
* @param {Object} retryEvent - Retry event from LoggingService
|
|
1786
|
+
* @returns {Promise<Object>} Inserted record
|
|
1787
|
+
*/
|
|
1788
|
+
async insertRetryEvent(uploadEventId, sessionId, retryEvent) {
|
|
1789
|
+
const supabase = await this.#getSupabaseClient();
|
|
1790
|
+
|
|
1791
|
+
const record = {
|
|
1792
|
+
upload_event_id: uploadEventId,
|
|
1793
|
+
session_id: sessionId,
|
|
1794
|
+
timestamp: retryEvent.timestamp || new Date().toISOString(),
|
|
1795
|
+
attempt_number: retryEvent.attemptNumber || 0,
|
|
1796
|
+
error_message: retryEvent.error || null,
|
|
1797
|
+
backoff_ms: retryEvent.backoffMs || 0,
|
|
1798
|
+
type: 'retry',
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
try {
|
|
1802
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
1803
|
+
return await supabase.from('watch_events').insert([record]).select();
|
|
1804
|
+
}, `insert retry event for upload ${uploadEventId}`);
|
|
1805
|
+
|
|
1806
|
+
if (error) {
|
|
1807
|
+
logger.error(`Failed to insert retry event: ${error.message}`);
|
|
1808
|
+
throw error;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
return data[0];
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
logger.error(`Error inserting retry event: ${error.message}`);
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Get upload history for a session
|
|
1820
|
+
* @param {string} sessionId - Session ID to query
|
|
1821
|
+
* @param {Object} options - Query options (limit, offset, strategy filter)
|
|
1822
|
+
* @returns {Promise<Array>} Array of upload events
|
|
1823
|
+
*/
|
|
1824
|
+
async getSessionUploadHistory(sessionId, options = {}) {
|
|
1825
|
+
const supabase = await this.#getSupabaseClient();
|
|
1826
|
+
const limit = options.limit || 100;
|
|
1827
|
+
const offset = options.offset || 0;
|
|
1828
|
+
|
|
1829
|
+
try {
|
|
1830
|
+
let query = supabase
|
|
1831
|
+
.from('watch_uploads')
|
|
1832
|
+
.select('*')
|
|
1833
|
+
.eq('session_id', sessionId)
|
|
1834
|
+
.order('timestamp', { ascending: false })
|
|
1835
|
+
.range(offset, offset + limit - 1);
|
|
1836
|
+
|
|
1837
|
+
// Filter by strategy if provided
|
|
1838
|
+
if (options.strategy) {
|
|
1839
|
+
query = query.eq('strategy', options.strategy);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
1843
|
+
return await query;
|
|
1844
|
+
}, `fetch upload history for session ${sessionId}`);
|
|
1845
|
+
|
|
1846
|
+
if (error) {
|
|
1847
|
+
logger.error(`Failed to fetch upload history: ${error.message}`);
|
|
1848
|
+
throw error;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
return data || [];
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
logger.error(`Error fetching upload history: ${error.message}`);
|
|
1854
|
+
return [];
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Get retry history for an upload event
|
|
1860
|
+
* @param {string} uploadEventId - Upload event ID to query
|
|
1861
|
+
* @param {Object} options - Query options (limit, offset)
|
|
1862
|
+
* @returns {Promise<Array>} Array of retry events
|
|
1863
|
+
*/
|
|
1864
|
+
async getUploadRetryHistory(uploadEventId, options = {}) {
|
|
1865
|
+
const supabase = await this.#getSupabaseClient();
|
|
1866
|
+
const limit = options.limit || 100;
|
|
1867
|
+
const offset = options.offset || 0;
|
|
1868
|
+
|
|
1869
|
+
try {
|
|
1870
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
1871
|
+
return await supabase
|
|
1872
|
+
.from('watch_events')
|
|
1873
|
+
.select('*')
|
|
1874
|
+
.eq('upload_event_id', uploadEventId)
|
|
1875
|
+
.eq('type', 'retry')
|
|
1876
|
+
.order('timestamp', { ascending: true })
|
|
1877
|
+
.range(offset, offset + limit - 1);
|
|
1878
|
+
}, `fetch retry history for upload ${uploadEventId}`);
|
|
1879
|
+
|
|
1880
|
+
if (error) {
|
|
1881
|
+
logger.error(`Failed to fetch retry history: ${error.message}`);
|
|
1882
|
+
throw error;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
return data || [];
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
logger.error(`Error fetching retry history: ${error.message}`);
|
|
1888
|
+
return [];
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Get session statistics
|
|
1894
|
+
* @param {string} sessionId - Session ID to analyze
|
|
1895
|
+
* @returns {Promise<Object>} Session statistics
|
|
1896
|
+
*/
|
|
1897
|
+
async getSessionStatistics(sessionId) {
|
|
1898
|
+
const supabase = await this.#getSupabaseClient();
|
|
1899
|
+
|
|
1900
|
+
try {
|
|
1901
|
+
// Fetch all upload events for the session
|
|
1902
|
+
const { data: uploads, error: uploadError } = await this.#queryWithRetry(
|
|
1903
|
+
async () => {
|
|
1904
|
+
return await supabase
|
|
1905
|
+
.from('watch_uploads')
|
|
1906
|
+
.select('*')
|
|
1907
|
+
.eq('session_id', sessionId);
|
|
1908
|
+
},
|
|
1909
|
+
`fetch statistics for session ${sessionId}`,
|
|
1910
|
+
);
|
|
1911
|
+
|
|
1912
|
+
if (uploadError) {
|
|
1913
|
+
throw uploadError;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Fetch all retry events for the session
|
|
1917
|
+
const { data: retries, error: retryError } = await this.#queryWithRetry(
|
|
1918
|
+
async () => {
|
|
1919
|
+
return await supabase
|
|
1920
|
+
.from('watch_events')
|
|
1921
|
+
.select('*')
|
|
1922
|
+
.eq('session_id', sessionId)
|
|
1923
|
+
.eq('type', 'retry');
|
|
1924
|
+
},
|
|
1925
|
+
`fetch retry statistics for session ${sessionId}`,
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
if (retryError) {
|
|
1929
|
+
throw retryError;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Calculate statistics
|
|
1933
|
+
const stats = {
|
|
1934
|
+
sessionId,
|
|
1935
|
+
totalUploadEvents: uploads?.length || 0,
|
|
1936
|
+
totalRetryEvents: retries?.length || 0,
|
|
1937
|
+
totalFileCount: 0,
|
|
1938
|
+
totalSuccessCount: 0,
|
|
1939
|
+
totalFailureCount: 0,
|
|
1940
|
+
totalRetryCount: 0,
|
|
1941
|
+
totalDuration: 0,
|
|
1942
|
+
byStrategy: {
|
|
1943
|
+
individual: {
|
|
1944
|
+
uploadCount: 0,
|
|
1945
|
+
totalFiles: 0,
|
|
1946
|
+
totalSuccess: 0,
|
|
1947
|
+
totalFailure: 0,
|
|
1948
|
+
successRate: 0,
|
|
1949
|
+
totalDuration: 0,
|
|
1950
|
+
},
|
|
1951
|
+
batch: {
|
|
1952
|
+
uploadCount: 0,
|
|
1953
|
+
totalFiles: 0,
|
|
1954
|
+
totalSuccess: 0,
|
|
1955
|
+
totalFailure: 0,
|
|
1956
|
+
successRate: 0,
|
|
1957
|
+
totalDuration: 0,
|
|
1958
|
+
},
|
|
1959
|
+
'full-structure': {
|
|
1960
|
+
uploadCount: 0,
|
|
1961
|
+
totalFiles: 0,
|
|
1962
|
+
totalSuccess: 0,
|
|
1963
|
+
totalFailure: 0,
|
|
1964
|
+
successRate: 0,
|
|
1965
|
+
totalDuration: 0,
|
|
1966
|
+
},
|
|
1967
|
+
},
|
|
1968
|
+
retryStats: {
|
|
1969
|
+
totalRetries: retries?.length || 0,
|
|
1970
|
+
uniqueUploadsWithRetries: new Set(
|
|
1971
|
+
retries?.map((r) => r.upload_event_id) || [],
|
|
1972
|
+
).size,
|
|
1973
|
+
totalRetryDuration:
|
|
1974
|
+
retries?.reduce((sum, r) => sum + (r.backoff_ms || 0), 0) || 0,
|
|
1975
|
+
},
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
// Process upload events
|
|
1979
|
+
if (uploads && uploads.length > 0) {
|
|
1980
|
+
uploads.forEach((upload) => {
|
|
1981
|
+
stats.totalFileCount += upload.file_count || 0;
|
|
1982
|
+
stats.totalSuccessCount += upload.success_count || 0;
|
|
1983
|
+
stats.totalFailureCount += upload.failure_count || 0;
|
|
1984
|
+
stats.totalRetryCount += upload.retry_count || 0;
|
|
1985
|
+
stats.totalDuration += upload.duration_ms || 0;
|
|
1986
|
+
|
|
1987
|
+
const strategyKey = upload.strategy || 'individual';
|
|
1988
|
+
if (stats.byStrategy[strategyKey]) {
|
|
1989
|
+
stats.byStrategy[strategyKey].uploadCount += 1;
|
|
1990
|
+
stats.byStrategy[strategyKey].totalFiles += upload.file_count || 0;
|
|
1991
|
+
stats.byStrategy[strategyKey].totalSuccess +=
|
|
1992
|
+
upload.success_count || 0;
|
|
1993
|
+
stats.byStrategy[strategyKey].totalFailure +=
|
|
1994
|
+
upload.failure_count || 0;
|
|
1995
|
+
stats.byStrategy[strategyKey].totalDuration +=
|
|
1996
|
+
upload.duration_ms || 0;
|
|
1997
|
+
|
|
1998
|
+
// Calculate success rate
|
|
1999
|
+
const totalFiles =
|
|
2000
|
+
stats.byStrategy[strategyKey].totalSuccess +
|
|
2001
|
+
stats.byStrategy[strategyKey].totalFailure;
|
|
2002
|
+
if (totalFiles > 0) {
|
|
2003
|
+
stats.byStrategy[strategyKey].successRate = (
|
|
2004
|
+
(stats.byStrategy[strategyKey].totalSuccess / totalFiles) *
|
|
2005
|
+
100
|
|
2006
|
+
).toFixed(2);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
return stats;
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
logger.error(`Error calculating session statistics: ${error.message}`);
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Delete old session data (cleanup)
|
|
2021
|
+
* @param {number} daysOld - Delete sessions older than this many days
|
|
2022
|
+
* @returns {Promise<Object>} Deletion results
|
|
2023
|
+
*/
|
|
2024
|
+
async cleanupOldSessions(daysOld = 30) {
|
|
2025
|
+
const supabase = await this.#getSupabaseClient();
|
|
2026
|
+
|
|
2027
|
+
try {
|
|
2028
|
+
const cutoffDate = new Date();
|
|
2029
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
2030
|
+
|
|
2031
|
+
// Get sessions to delete
|
|
2032
|
+
const { data: sessionsToDelete, error: fetchError } =
|
|
2033
|
+
await this.#queryWithRetry(async () => {
|
|
2034
|
+
return await supabase
|
|
2035
|
+
.from('watch_uploads')
|
|
2036
|
+
.select('session_id')
|
|
2037
|
+
.lt('timestamp', cutoffDate.toISOString())
|
|
2038
|
+
.distinct();
|
|
2039
|
+
}, `fetch sessions older than ${daysOld} days`);
|
|
2040
|
+
|
|
2041
|
+
if (fetchError) {
|
|
2042
|
+
throw fetchError;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
let deletedUploads = 0;
|
|
2046
|
+
let deletedEvents = 0;
|
|
2047
|
+
|
|
2048
|
+
if (sessionsToDelete && sessionsToDelete.length > 0) {
|
|
2049
|
+
const sessionIds = sessionsToDelete.map((s) => s.session_id);
|
|
2050
|
+
|
|
2051
|
+
// Delete events
|
|
2052
|
+
const { count: eventCount, error: eventError } =
|
|
2053
|
+
await this.#queryWithRetry(async () => {
|
|
2054
|
+
return await supabase
|
|
2055
|
+
.from('watch_events')
|
|
2056
|
+
.delete()
|
|
2057
|
+
.in('session_id', sessionIds);
|
|
2058
|
+
}, `delete events for old sessions`);
|
|
2059
|
+
|
|
2060
|
+
if (!eventError) {
|
|
2061
|
+
deletedEvents = eventCount || 0;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Delete uploads
|
|
2065
|
+
const { count: uploadCount, error: uploadError } =
|
|
2066
|
+
await this.#queryWithRetry(async () => {
|
|
2067
|
+
return await supabase
|
|
2068
|
+
.from('watch_uploads')
|
|
2069
|
+
.delete()
|
|
2070
|
+
.in('session_id', sessionIds);
|
|
2071
|
+
}, `delete old session uploads`);
|
|
2072
|
+
|
|
2073
|
+
if (!uploadError) {
|
|
2074
|
+
deletedUploads = uploadCount || 0;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
return {
|
|
2079
|
+
deletedUploads,
|
|
2080
|
+
deletedEvents,
|
|
2081
|
+
sessionsDeleted: sessionsToDelete?.length || 0,
|
|
2082
|
+
};
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
logger.error(`Error cleaning up old sessions: ${error.message}`);
|
|
2085
|
+
return { deletedUploads: 0, deletedEvents: 0, sessionsDeleted: 0 };
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Cleanup database connections and resources
|
|
2091
|
+
* Called during graceful shutdown
|
|
2092
|
+
* @returns {Promise<Object>} Cleanup results
|
|
2093
|
+
*/
|
|
2094
|
+
async cleanup() {
|
|
2095
|
+
try {
|
|
2096
|
+
logger.info('DatabaseService: Starting cleanup...');
|
|
2097
|
+
|
|
2098
|
+
// Commit any pending transactions
|
|
2099
|
+
const transactionResult = await this.commitPendingTransactions();
|
|
2100
|
+
|
|
2101
|
+
// Close database connections
|
|
2102
|
+
const closeResult = await this.closeConnections();
|
|
2103
|
+
|
|
2104
|
+
logger.info('DatabaseService: Cleanup complete');
|
|
2105
|
+
|
|
2106
|
+
return {
|
|
2107
|
+
success: true,
|
|
2108
|
+
transactionsCommitted: transactionResult.count,
|
|
2109
|
+
connectionsClosedResult: closeResult,
|
|
2110
|
+
};
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
logger.error(`DatabaseService: Error during cleanup: ${error.message}`);
|
|
2113
|
+
return {
|
|
2114
|
+
success: false,
|
|
2115
|
+
error: error.message,
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
/**
|
|
2121
|
+
* Commit any pending transactions before shutdown
|
|
2122
|
+
* @private
|
|
2123
|
+
* @returns {Promise<Object>} Results
|
|
2124
|
+
*/
|
|
2125
|
+
async commitPendingTransactions() {
|
|
2126
|
+
try {
|
|
2127
|
+
logger.debug('DatabaseService: Committing pending transactions...');
|
|
2128
|
+
|
|
2129
|
+
// Note: This is a placeholder for actual transaction handling
|
|
2130
|
+
// In a real implementation, you would track active transactions
|
|
2131
|
+
// and ensure they are properly committed before shutdown
|
|
2132
|
+
|
|
2133
|
+
logger.debug('DatabaseService: Pending transactions committed');
|
|
2134
|
+
return { count: 0, success: true };
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
logger.error(
|
|
2137
|
+
`DatabaseService: Error committing transactions: ${error.message}`,
|
|
2138
|
+
);
|
|
2139
|
+
return { count: 0, success: false, error: error.message };
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* Close all database connections
|
|
2145
|
+
* @private
|
|
2146
|
+
* @returns {Promise<Object>} Results
|
|
2147
|
+
*/
|
|
2148
|
+
async closeConnections() {
|
|
2149
|
+
try {
|
|
2150
|
+
logger.debug('DatabaseService: Closing database connections...');
|
|
2151
|
+
|
|
2152
|
+
// Close Supabase connection if available
|
|
2153
|
+
if (this.supabase) {
|
|
2154
|
+
// Supabase client will handle connection cleanup automatically
|
|
2155
|
+
logger.debug('DatabaseService: Supabase connection cleanup initiated');
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
logger.info('DatabaseService: All database connections closed');
|
|
2159
|
+
return { success: true };
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
logger.error(
|
|
2162
|
+
`DatabaseService: Error closing connections: ${error.message}`,
|
|
2163
|
+
);
|
|
2164
|
+
return { success: false, error: error.message };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
1687
2167
|
}
|
|
1688
2168
|
|
|
1689
2169
|
// Export singleton instance
|