@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
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
|
|
4
|
+
import logger from './LoggingService.js';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SmartWatchDatabaseService
|
|
11
|
+
* Specialized database service for Smart Watch Queue management
|
|
12
|
+
* Handles file state transitions, batch processing, and progress tracking
|
|
13
|
+
*/
|
|
14
|
+
export class SmartWatchDatabaseService {
|
|
15
|
+
#databaseService;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize with a DatabaseService instance
|
|
19
|
+
* @param {DatabaseService} databaseService - The main database service
|
|
20
|
+
*/
|
|
21
|
+
constructor(databaseService) {
|
|
22
|
+
if (!databaseService) {
|
|
23
|
+
throw new Error('DatabaseService instance is required');
|
|
24
|
+
}
|
|
25
|
+
this.#databaseService = databaseService;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Inserta un archivo a la tabla uploader con estado PENDING o READY_TO_UPLOAD
|
|
30
|
+
* @param {string} filePath - Ruta del archivo
|
|
31
|
+
* @param {Object} options - Opciones { processingStatus, dependsOnPath, size, extension }
|
|
32
|
+
* @returns {Promise<Object>} Archivo insertado
|
|
33
|
+
*/
|
|
34
|
+
async insertFileToUploader(filePath, options = {}) {
|
|
35
|
+
const {
|
|
36
|
+
processingStatus = 'PENDING',
|
|
37
|
+
dependsOnPath = null,
|
|
38
|
+
documentType = null,
|
|
39
|
+
size = null,
|
|
40
|
+
fileExtension = null,
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
44
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
45
|
+
const fileName = path.basename(filePath);
|
|
46
|
+
|
|
47
|
+
const fileData = {
|
|
48
|
+
name: fileName,
|
|
49
|
+
original_path: filePath,
|
|
50
|
+
filename: fileName,
|
|
51
|
+
file_extension:
|
|
52
|
+
fileExtension ||
|
|
53
|
+
path.extname(fileName).toLowerCase().replace('.', ''),
|
|
54
|
+
size: size || 0,
|
|
55
|
+
document_type: documentType,
|
|
56
|
+
processing_status: processingStatus,
|
|
57
|
+
depends_on_path: dependsOnPath,
|
|
58
|
+
created_at: new Date().toISOString(),
|
|
59
|
+
updated_at: new Date().toISOString(),
|
|
60
|
+
upload_attempts: 0,
|
|
61
|
+
status: 'pending',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const { data, error } = await supabase
|
|
65
|
+
.from('uploader')
|
|
66
|
+
.insert([fileData])
|
|
67
|
+
.select();
|
|
68
|
+
|
|
69
|
+
// Handle duplicate key errors gracefully
|
|
70
|
+
if (error) {
|
|
71
|
+
if (error.message.includes('duplicate key')) {
|
|
72
|
+
logger.debug(`⏭️ File already exists: ${fileName}`);
|
|
73
|
+
// Return existing file data instead of throwing
|
|
74
|
+
return { file_path: filePath, file_name: fileName };
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Error inserting file: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.debug(
|
|
80
|
+
`✅ File inserted: ${fileName} (status: ${processingStatus})`,
|
|
81
|
+
);
|
|
82
|
+
return data?.[0];
|
|
83
|
+
}, 'Insert file to uploader');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Obtiene archivos PENDING en un directorio específico
|
|
88
|
+
* @param {string} dirPath - Ruta del directorio
|
|
89
|
+
* @returns {Promise<Array>} Archivos pendientes
|
|
90
|
+
*/
|
|
91
|
+
async getPendingFilesInDirectory(dirPath) {
|
|
92
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
93
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
94
|
+
|
|
95
|
+
const { data, error } = await supabase
|
|
96
|
+
.from('uploader')
|
|
97
|
+
.select('*')
|
|
98
|
+
.eq('depends_on_path', dirPath)
|
|
99
|
+
.eq('processing_status', 'PENDING')
|
|
100
|
+
.order('created_at', { ascending: true });
|
|
101
|
+
|
|
102
|
+
if (error) {
|
|
103
|
+
throw new Error(`Error getting pending files: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return data || [];
|
|
107
|
+
}, 'Get pending files in directory');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Obtiene archivos con ciertos status
|
|
112
|
+
* @param {Array} statuses - Array de status a buscar
|
|
113
|
+
* @param {number} limit - Límite de resultados
|
|
114
|
+
* @returns {Promise<Array>} Archivos con los status especificados
|
|
115
|
+
*/
|
|
116
|
+
async getFilesWithStatus(statuses, limit = 10) {
|
|
117
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
118
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
119
|
+
|
|
120
|
+
let query = supabase
|
|
121
|
+
.from('uploader')
|
|
122
|
+
.select('*')
|
|
123
|
+
.in('processing_status', statuses)
|
|
124
|
+
.order('created_at', { ascending: true })
|
|
125
|
+
.limit(limit);
|
|
126
|
+
|
|
127
|
+
const { data, error } = await query;
|
|
128
|
+
|
|
129
|
+
if (error) {
|
|
130
|
+
throw new Error(`Error getting files with status: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return data || [];
|
|
134
|
+
}, 'Get files with status');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Actualiza el status de un archivo
|
|
139
|
+
* @param {string} filePath - Ruta original del archivo
|
|
140
|
+
* @param {Object} updates - Actualizaciones a realizar
|
|
141
|
+
* @returns {Promise<Object>} Archivo actualizado
|
|
142
|
+
*/
|
|
143
|
+
async updateFileStatus(filePath, updates) {
|
|
144
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
145
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
146
|
+
|
|
147
|
+
const updateData = {
|
|
148
|
+
...updates,
|
|
149
|
+
updated_at: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const { data, error } = await supabase
|
|
153
|
+
.from('uploader')
|
|
154
|
+
.update(updateData)
|
|
155
|
+
.eq('original_path', filePath)
|
|
156
|
+
.select();
|
|
157
|
+
|
|
158
|
+
if (error) {
|
|
159
|
+
throw new Error(`Error updating file status: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.debug(
|
|
163
|
+
`✅ File status updated: ${path.basename(filePath)} -> ${updates.processing_status}`,
|
|
164
|
+
);
|
|
165
|
+
return data?.[0];
|
|
166
|
+
}, 'Update file status');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Marca un directorio como "tiene pedimento detectado"
|
|
171
|
+
* @param {string} dirPath - Ruta del directorio
|
|
172
|
+
* @param {string} pedimentoPath - Ruta del archivo pedimento
|
|
173
|
+
* @returns {Promise<Object>} Archivo pedimento actualizado
|
|
174
|
+
*/
|
|
175
|
+
async markPedimentoDetected(dirPath, pedimentoPath) {
|
|
176
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
177
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
178
|
+
|
|
179
|
+
const { data, error } = await supabase
|
|
180
|
+
.from('uploader')
|
|
181
|
+
.update({
|
|
182
|
+
pedimento_detected_at: new Date().toISOString(),
|
|
183
|
+
processing_status: 'READY_TO_UPLOAD',
|
|
184
|
+
updated_at: new Date().toISOString(),
|
|
185
|
+
})
|
|
186
|
+
.eq('original_path', pedimentoPath)
|
|
187
|
+
.select();
|
|
188
|
+
|
|
189
|
+
if (error) {
|
|
190
|
+
throw new Error(`Error marking pedimento detected: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
logger.info(
|
|
194
|
+
`✅ Pedimento marked as detected: ${path.basename(pedimentoPath)}`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Ahora marcar archivos pendientes como READY_TO_UPLOAD
|
|
198
|
+
const pendingFiles = await this.getPendingFilesInDirectory(dirPath);
|
|
199
|
+
|
|
200
|
+
if (pendingFiles.length > 0) {
|
|
201
|
+
logger.info(
|
|
202
|
+
`🔄 Marking ${pendingFiles.length} pending files as READY_TO_UPLOAD`,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
for (const file of pendingFiles) {
|
|
206
|
+
await this.updateFileStatus(file.original_path, {
|
|
207
|
+
processing_status: 'READY_TO_UPLOAD',
|
|
208
|
+
pedimento_detected_at: new Date().toISOString(),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return data?.[0];
|
|
214
|
+
}, 'Mark pedimento detected');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Obtiene archivos que llevan esperando más del máximo
|
|
219
|
+
* @param {number} maxWaitTimeSeconds - Tiempo máximo en segundos
|
|
220
|
+
* @returns {Promise<Array>} Archivos expirados
|
|
221
|
+
*/
|
|
222
|
+
async getExpiredWaitingFiles(maxWaitTimeSeconds) {
|
|
223
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
224
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
225
|
+
const cutoffTime = new Date(
|
|
226
|
+
Date.now() - maxWaitTimeSeconds * 1000,
|
|
227
|
+
).toISOString();
|
|
228
|
+
|
|
229
|
+
const { data, error } = await supabase
|
|
230
|
+
.from('uploader')
|
|
231
|
+
.select('*')
|
|
232
|
+
.eq('processing_status', 'PENDING')
|
|
233
|
+
.lt('created_at', cutoffTime)
|
|
234
|
+
.order('created_at', { ascending: true });
|
|
235
|
+
|
|
236
|
+
if (error) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Error getting expired waiting files: ${error.message}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return data || [];
|
|
243
|
+
}, 'Get expired waiting files');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Obtiene estadísticas de procesamiento
|
|
248
|
+
* @returns {Promise<Object>} Estadísticas por status
|
|
249
|
+
*/
|
|
250
|
+
async getProcessingStats() {
|
|
251
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
252
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
253
|
+
const statuses = [
|
|
254
|
+
'PENDING',
|
|
255
|
+
'READY_TO_UPLOAD',
|
|
256
|
+
'PROCESSING',
|
|
257
|
+
'UPLOADED',
|
|
258
|
+
'FAILED',
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const stats = {
|
|
262
|
+
pending: 0,
|
|
263
|
+
readyToUpload: 0,
|
|
264
|
+
processing: 0,
|
|
265
|
+
uploaded: 0,
|
|
266
|
+
failed: 0,
|
|
267
|
+
total: 0,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
for (const status of statuses) {
|
|
271
|
+
const { count, error } = await supabase
|
|
272
|
+
.from('uploader')
|
|
273
|
+
.select('*', { count: 'exact', head: true })
|
|
274
|
+
.eq('processing_status', status);
|
|
275
|
+
|
|
276
|
+
if (!error && count !== null) {
|
|
277
|
+
stats.total += count;
|
|
278
|
+
switch (status) {
|
|
279
|
+
case 'PENDING':
|
|
280
|
+
stats.pending = count;
|
|
281
|
+
break;
|
|
282
|
+
case 'READY_TO_UPLOAD':
|
|
283
|
+
stats.readyToUpload = count;
|
|
284
|
+
break;
|
|
285
|
+
case 'PROCESSING':
|
|
286
|
+
stats.processing = count;
|
|
287
|
+
break;
|
|
288
|
+
case 'UPLOADED':
|
|
289
|
+
stats.uploaded = count;
|
|
290
|
+
break;
|
|
291
|
+
case 'FAILED':
|
|
292
|
+
stats.failed = count;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return stats;
|
|
299
|
+
}, 'Get processing stats');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Obtiene archivos fallidos listos para reintentar
|
|
304
|
+
* @param {number} maxAttempts - Máximo número de intentos
|
|
305
|
+
* @returns {Promise<Array>} Archivos fallidos para reintentar
|
|
306
|
+
*/
|
|
307
|
+
async getFailedFilesForRetry(maxAttempts = 3) {
|
|
308
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
309
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
310
|
+
|
|
311
|
+
const { data, error } = await supabase
|
|
312
|
+
.from('uploader')
|
|
313
|
+
.select('*')
|
|
314
|
+
.eq('processing_status', 'FAILED')
|
|
315
|
+
.lt('upload_attempts', maxAttempts)
|
|
316
|
+
.order('upload_attempts', { ascending: true })
|
|
317
|
+
.order('updated_at', { ascending: true });
|
|
318
|
+
|
|
319
|
+
if (error) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Error getting failed files for retry: ${error.message}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return data || [];
|
|
326
|
+
}, 'Get failed files for retry');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Reinicia un archivo fallido
|
|
331
|
+
* @param {string} filePath - Ruta del archivo
|
|
332
|
+
* @returns {Promise<Object>} Archivo reiniciado
|
|
333
|
+
*/
|
|
334
|
+
async retryFile(filePath) {
|
|
335
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
336
|
+
return this.updateFileStatus(filePath, {
|
|
337
|
+
processing_status: 'READY_TO_UPLOAD',
|
|
338
|
+
upload_attempts: 0,
|
|
339
|
+
last_error: null,
|
|
340
|
+
});
|
|
341
|
+
}, `Retry file: ${filePath}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Reinicia múltiples archivos fallidos
|
|
346
|
+
* @param {number} limit - Cantidad máxima a reintentar
|
|
347
|
+
* @param {number} maxAttempts - Máximo número de intentos permitidos
|
|
348
|
+
* @returns {Promise<Object>} Resultado del reintento
|
|
349
|
+
*/
|
|
350
|
+
async retryFailedFiles(limit = 10, maxAttempts = 3) {
|
|
351
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
352
|
+
const failedFiles = await this.getFailedFilesForRetry(maxAttempts);
|
|
353
|
+
const toRetry = failedFiles.slice(0, limit);
|
|
354
|
+
|
|
355
|
+
const results = {
|
|
356
|
+
retried: 0,
|
|
357
|
+
skipped: 0,
|
|
358
|
+
files: [],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
for (const file of toRetry) {
|
|
362
|
+
try {
|
|
363
|
+
const updated = await this.retryFile(file.original_path);
|
|
364
|
+
results.retried++;
|
|
365
|
+
results.files.push(updated);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
logger.error(
|
|
368
|
+
`Error retrying file ${file.filename}: ${error.message}`,
|
|
369
|
+
);
|
|
370
|
+
results.skipped++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
logger.info(
|
|
375
|
+
`🔄 Retried ${results.retried} files, skipped ${results.skipped}`,
|
|
376
|
+
);
|
|
377
|
+
return results;
|
|
378
|
+
}, 'Retry failed files');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Obtiene progreso general del procesamiento
|
|
383
|
+
* @returns {Promise<Object>} Progreso con ETA
|
|
384
|
+
*/
|
|
385
|
+
async getOverallProgress() {
|
|
386
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
387
|
+
const stats = await this.getProcessingStats();
|
|
388
|
+
|
|
389
|
+
const totalFiles =
|
|
390
|
+
stats.pending +
|
|
391
|
+
stats.readyToUpload +
|
|
392
|
+
stats.processing +
|
|
393
|
+
stats.uploaded +
|
|
394
|
+
stats.failed;
|
|
395
|
+
const processedFiles = stats.uploaded;
|
|
396
|
+
const failedFiles = stats.failed;
|
|
397
|
+
const pendingFiles =
|
|
398
|
+
stats.pending + stats.readyToUpload + stats.processing;
|
|
399
|
+
|
|
400
|
+
// Obtener archivos recientemente subidos para calcular promedio
|
|
401
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
402
|
+
const { data: recentUploaded } = await supabase
|
|
403
|
+
.from('uploader')
|
|
404
|
+
.select('*')
|
|
405
|
+
.eq('processing_status', 'UPLOADED')
|
|
406
|
+
.order('updated_at', { ascending: false })
|
|
407
|
+
.limit(100);
|
|
408
|
+
|
|
409
|
+
let avgProcessingTimeMs = 0;
|
|
410
|
+
if (recentUploaded && recentUploaded.length > 0) {
|
|
411
|
+
const totalTime = recentUploaded.reduce((sum, file) => {
|
|
412
|
+
if (file.created_at && file.updated_at) {
|
|
413
|
+
return (
|
|
414
|
+
sum +
|
|
415
|
+
(new Date(file.updated_at).getTime() -
|
|
416
|
+
new Date(file.created_at).getTime())
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return sum;
|
|
420
|
+
}, 0);
|
|
421
|
+
avgProcessingTimeMs = totalTime / recentUploaded.length;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Estimar tiempo restante
|
|
425
|
+
const remainingTime = pendingFiles * avgProcessingTimeMs;
|
|
426
|
+
const hours = Math.floor(remainingTime / (1000 * 60 * 60));
|
|
427
|
+
const minutes = Math.floor(
|
|
428
|
+
(remainingTime % (1000 * 60 * 60)) / (1000 * 60),
|
|
429
|
+
);
|
|
430
|
+
const estimatedTimeRemaining = `${hours}h ${minutes}m`;
|
|
431
|
+
|
|
432
|
+
const percentComplete =
|
|
433
|
+
totalFiles > 0 ? Math.round((processedFiles / totalFiles) * 100) : 0;
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
totalFiles,
|
|
437
|
+
processedFiles,
|
|
438
|
+
failedFiles,
|
|
439
|
+
pendingFiles,
|
|
440
|
+
percentComplete,
|
|
441
|
+
estimatedTimeRemaining,
|
|
442
|
+
avgProcessingTimeMs: Math.round(avgProcessingTimeMs),
|
|
443
|
+
};
|
|
444
|
+
}, 'Get overall progress');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Obtiene detalles de un archivo específico
|
|
449
|
+
* @param {string} filePath - Ruta del archivo
|
|
450
|
+
* @returns {Promise<Object>} Detalles del archivo
|
|
451
|
+
*/
|
|
452
|
+
async getFileDetails(filePath) {
|
|
453
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
454
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
455
|
+
|
|
456
|
+
const { data, error } = await supabase
|
|
457
|
+
.from('uploader')
|
|
458
|
+
.select('*')
|
|
459
|
+
.eq('original_path', filePath)
|
|
460
|
+
.single();
|
|
461
|
+
|
|
462
|
+
if (error) {
|
|
463
|
+
throw new Error(`File not found: ${filePath}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
...data,
|
|
468
|
+
timeInQueue: data.created_at
|
|
469
|
+
? `${Math.round((Date.now() - new Date(data.created_at).getTime()) / 1000)}s`
|
|
470
|
+
: null,
|
|
471
|
+
attempts: data.upload_attempts,
|
|
472
|
+
hasError: !!data.last_error,
|
|
473
|
+
};
|
|
474
|
+
}, `Get file details: ${filePath}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Busca archivos por múltiples criterios
|
|
479
|
+
* @param {Object} filters - Filtros de búsqueda
|
|
480
|
+
* @returns {Promise<Array>} Archivos que coinciden
|
|
481
|
+
*/
|
|
482
|
+
async searchFiles(filters = {}) {
|
|
483
|
+
return this.#databaseService.queryWithRetry(async () => {
|
|
484
|
+
const { status, rfc, year, dirPath, numPedimento, limit = 50 } = filters;
|
|
485
|
+
|
|
486
|
+
const supabase = await this.#databaseService.getSupabaseClient();
|
|
487
|
+
let query = supabase.from('uploader').select('*');
|
|
488
|
+
|
|
489
|
+
if (status) {
|
|
490
|
+
query = query.eq('processing_status', status);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (rfc) {
|
|
494
|
+
query = query.eq('rfc', rfc);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (year) {
|
|
498
|
+
query = query.eq('year', year);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (dirPath) {
|
|
502
|
+
query = query.eq('depends_on_path', dirPath);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (numPedimento) {
|
|
506
|
+
query = query.ilike('num_pedimento', `%${numPedimento}%`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
query = query.order('created_at', { ascending: false }).limit(limit);
|
|
510
|
+
|
|
511
|
+
const { data, error } = await query;
|
|
512
|
+
|
|
513
|
+
if (error) {
|
|
514
|
+
throw new Error(`Error searching files: ${error.message}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return data || [];
|
|
518
|
+
}, 'Search files');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Export singleton factory function
|
|
523
|
+
export function createSmartWatchDatabaseService(databaseService) {
|
|
524
|
+
return new SmartWatchDatabaseService(databaseService);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export default SmartWatchDatabaseService;
|