@arela/uploader 0.2.13 → 0.3.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 +134 -58
- 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 +1305 -0
- package/src/config/config.js +113 -0
- package/src/document-type-shared.js +2 -0
- package/src/document-types/support-document.js +201 -0
- package/src/file-detection.js +2 -1
- package/src/index.js +44 -0
- package/src/services/AdvancedFilterService.js +505 -0
- package/src/services/AutoProcessingService.js +639 -0
- package/src/services/BenchmarkingService.js +381 -0
- package/src/services/DatabaseService.js +695 -39
- 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 +23 -1
- package/src/services/upload/SupabaseUploadService.js +12 -5
- package/src/utils/CleanupManager.js +262 -0
- package/src/utils/FileOperations.js +41 -0
- package/src/utils/WatchEventHandler.js +517 -0
- package/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- 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
|
|
@@ -181,18 +206,153 @@ export class DatabaseService {
|
|
|
181
206
|
`Inserting ${records.length} new records into uploader table...`,
|
|
182
207
|
);
|
|
183
208
|
|
|
209
|
+
// Use upsert to handle duplicates gracefully
|
|
210
|
+
// This will insert new records or update existing ones (by original_path)
|
|
184
211
|
const { data, error } = await supabase
|
|
185
212
|
.from('uploader')
|
|
186
|
-
.
|
|
213
|
+
.upsert(records, { onConflict: 'original_path' })
|
|
187
214
|
.select();
|
|
188
215
|
|
|
189
216
|
if (error) {
|
|
190
217
|
throw new Error(`Failed to insert stats records: ${error.message}`);
|
|
191
218
|
}
|
|
192
219
|
|
|
220
|
+
// Propagate arela_path to related files in same folder
|
|
221
|
+
if (data && data.length > 0) {
|
|
222
|
+
await this.#propagateArelaPathToRelatedFiles(data, supabase);
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
return data;
|
|
194
226
|
}
|
|
195
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Propagate arela_path from pedimento files to related files in the same directory
|
|
230
|
+
* When a pedimento_simplificado is detected, all files in its directory get the same arela_path
|
|
231
|
+
* Also checks database for existing files in the same directory that need the arela_path
|
|
232
|
+
* @private
|
|
233
|
+
* @param {Array} insertedRecords - Records that were just inserted
|
|
234
|
+
* @param {Object} supabase - Supabase client
|
|
235
|
+
* @returns {Promise<void>}
|
|
236
|
+
*/
|
|
237
|
+
async #propagateArelaPathToRelatedFiles(insertedRecords, supabase) {
|
|
238
|
+
try {
|
|
239
|
+
// Group records by directory
|
|
240
|
+
const recordsByDir = {};
|
|
241
|
+
|
|
242
|
+
for (const record of insertedRecords) {
|
|
243
|
+
if (!record.original_path) continue;
|
|
244
|
+
|
|
245
|
+
const dirPath = path.dirname(record.original_path);
|
|
246
|
+
if (!recordsByDir[dirPath]) {
|
|
247
|
+
recordsByDir[dirPath] = {
|
|
248
|
+
pedimentos: [],
|
|
249
|
+
allFiles: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
recordsByDir[dirPath].allFiles.push(record);
|
|
254
|
+
|
|
255
|
+
// Identify pedimento files
|
|
256
|
+
if (
|
|
257
|
+
record.document_type === 'pedimento_simplificado' &&
|
|
258
|
+
record.arela_path
|
|
259
|
+
) {
|
|
260
|
+
recordsByDir[dirPath].pedimentos.push(record);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// For each directory with a pedimento, propagate its arela_path to related files
|
|
265
|
+
for (const [dirPath, dirData] of Object.entries(recordsByDir)) {
|
|
266
|
+
if (dirData.pedimentos.length === 0) continue;
|
|
267
|
+
|
|
268
|
+
// Use the first pedimento's arela_path (should be only one per directory typically)
|
|
269
|
+
const pedimentoRecord = dirData.pedimentos[0];
|
|
270
|
+
const arelaPath = pedimentoRecord.arela_path;
|
|
271
|
+
|
|
272
|
+
logger.info(
|
|
273
|
+
`📁 Propagating arela_path from pedimento to files in ${path.basename(dirPath)}/`,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Step 1: Update newly inserted files in this directory
|
|
277
|
+
const fileIds = dirData.allFiles
|
|
278
|
+
.filter(
|
|
279
|
+
(f) =>
|
|
280
|
+
f.id &&
|
|
281
|
+
f.arela_path !== arelaPath &&
|
|
282
|
+
f.document_type !== 'pedimento_simplificado',
|
|
283
|
+
)
|
|
284
|
+
.map((f) => f.id);
|
|
285
|
+
|
|
286
|
+
if (fileIds.length > 0) {
|
|
287
|
+
const { error: updateError } = await supabase
|
|
288
|
+
.from('uploader')
|
|
289
|
+
.update({ arela_path: arelaPath })
|
|
290
|
+
.in('id', fileIds);
|
|
291
|
+
|
|
292
|
+
if (updateError) {
|
|
293
|
+
logger.warn(
|
|
294
|
+
`Could not propagate arela_path: ${updateError.message}`,
|
|
295
|
+
);
|
|
296
|
+
} else {
|
|
297
|
+
logger.info(
|
|
298
|
+
`✅ Updated ${fileIds.length} related files with arela_path: ${arelaPath}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Step 2: Also find and update any existing files in the same directory that don't have arela_path
|
|
304
|
+
// This handles the case where files were detected earlier but the pedimento is detected now
|
|
305
|
+
try {
|
|
306
|
+
const dirPattern = dirPath.replace(/\\/g, '/'); // Normalize for SQL LIKE
|
|
307
|
+
const { data: existingFiles, error: fetchError } = await supabase
|
|
308
|
+
.from('uploader')
|
|
309
|
+
.select('id, original_path, document_type')
|
|
310
|
+
.like('original_path', `${dirPattern}/%`)
|
|
311
|
+
.is('arela_path', null)
|
|
312
|
+
.limit(1000); // Reasonable limit to avoid huge queries
|
|
313
|
+
|
|
314
|
+
if (fetchError) {
|
|
315
|
+
logger.warn(
|
|
316
|
+
`Could not fetch existing files in ${path.basename(dirPath)}: ${fetchError.message}`,
|
|
317
|
+
);
|
|
318
|
+
} else if (existingFiles && existingFiles.length > 0) {
|
|
319
|
+
const existingFileIds = existingFiles
|
|
320
|
+
.filter(
|
|
321
|
+
(f) => f.id && f.document_type !== 'pedimento_simplificado',
|
|
322
|
+
)
|
|
323
|
+
.map((f) => f.id);
|
|
324
|
+
|
|
325
|
+
if (existingFileIds.length > 0) {
|
|
326
|
+
const { error: existingError } = await supabase
|
|
327
|
+
.from('uploader')
|
|
328
|
+
.update({ arela_path: arelaPath })
|
|
329
|
+
.in('id', existingFileIds);
|
|
330
|
+
|
|
331
|
+
if (existingError) {
|
|
332
|
+
logger.warn(
|
|
333
|
+
`Could not update existing files with arela_path: ${existingError.message}`,
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
logger.info(
|
|
337
|
+
`✅ Updated ${existingFileIds.length} existing files in directory with arela_path: ${arelaPath}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (existingFilesError) {
|
|
343
|
+
logger.warn(
|
|
344
|
+
`Error checking for existing files in ${path.basename(dirPath)}: ${existingFilesError.message}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
logger.warn(
|
|
350
|
+
`Error propagating arela_path to related files: ${error.message}`,
|
|
351
|
+
);
|
|
352
|
+
// Don't throw - this is a non-critical operation
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
196
356
|
/**
|
|
197
357
|
* Insert file stats only (no detection) into uploader table
|
|
198
358
|
* @param {Array} files - Array of file objects
|
|
@@ -202,11 +362,29 @@ export class DatabaseService {
|
|
|
202
362
|
async insertStatsOnlyToUploaderTable(files, options) {
|
|
203
363
|
const supabase = await this.#getSupabaseClient();
|
|
204
364
|
const batchSize = 1000;
|
|
365
|
+
const quietMode = options?.quietMode || false;
|
|
205
366
|
const allRecords = [];
|
|
206
367
|
|
|
207
|
-
|
|
368
|
+
if (!quietMode) {
|
|
369
|
+
logger.info('Collecting filesystem stats...');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Filter out system files and hidden files (macOS, Windows, Python, editors)
|
|
373
|
+
const systemFilePattern =
|
|
374
|
+
/^\.|__pycache__|\.pyc|\.swp|\.swo|Thumbs\.db|desktop\.ini|DS_Store|\$RECYCLE\.BIN|System Volume Information|~\$|\.tmp/i;
|
|
375
|
+
|
|
208
376
|
for (const file of files) {
|
|
209
377
|
try {
|
|
378
|
+
const fileName = file.originalName || path.basename(file.path);
|
|
379
|
+
|
|
380
|
+
// Skip system and hidden files
|
|
381
|
+
if (systemFilePattern.test(fileName)) {
|
|
382
|
+
if (!quietMode) {
|
|
383
|
+
logger.debug(`Skipping system file: ${fileName}`);
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
210
388
|
const stats = file.stats || fs.statSync(file.path);
|
|
211
389
|
const originalPath = options.clientPath || file.path;
|
|
212
390
|
const fileExtension = path
|
|
@@ -215,6 +393,7 @@ export class DatabaseService {
|
|
|
215
393
|
.replace('.', '');
|
|
216
394
|
|
|
217
395
|
const record = {
|
|
396
|
+
name: file.originalName || path.basename(file.path),
|
|
218
397
|
document_type: null,
|
|
219
398
|
size: stats.size,
|
|
220
399
|
num_pedimento: null,
|
|
@@ -226,6 +405,7 @@ export class DatabaseService {
|
|
|
226
405
|
message: null,
|
|
227
406
|
file_extension: fileExtension,
|
|
228
407
|
created_at: new Date().toISOString(),
|
|
408
|
+
updated_at: new Date().toISOString(),
|
|
229
409
|
modified_at: stats.mtime.toISOString(),
|
|
230
410
|
is_like_simplificado:
|
|
231
411
|
fileExtension === 'pdf' &&
|
|
@@ -233,6 +413,9 @@ export class DatabaseService {
|
|
|
233
413
|
.toLowerCase()
|
|
234
414
|
.includes('simp'),
|
|
235
415
|
year: null,
|
|
416
|
+
// Queue/Processing columns (for arela-api)
|
|
417
|
+
processing_status: 'PENDING',
|
|
418
|
+
upload_attempts: 0,
|
|
236
419
|
};
|
|
237
420
|
|
|
238
421
|
allRecords.push(record);
|
|
@@ -242,13 +425,17 @@ export class DatabaseService {
|
|
|
242
425
|
}
|
|
243
426
|
|
|
244
427
|
if (allRecords.length === 0) {
|
|
245
|
-
|
|
428
|
+
if (!quietMode) {
|
|
429
|
+
logger.info('No file stats to insert');
|
|
430
|
+
}
|
|
246
431
|
return { totalInserted: 0, totalSkipped: 0, totalProcessed: 0 };
|
|
247
432
|
}
|
|
248
433
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
434
|
+
if (!quietMode) {
|
|
435
|
+
logger.info(
|
|
436
|
+
`Processing ${allRecords.length} file stats in batches of ${batchSize}...`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
252
439
|
|
|
253
440
|
let totalInserted = 0;
|
|
254
441
|
let totalUpdated = 0;
|
|
@@ -281,10 +468,11 @@ export class DatabaseService {
|
|
|
281
468
|
existingPaths.has(r.original_path),
|
|
282
469
|
);
|
|
283
470
|
|
|
284
|
-
// Only log every 10th batch to reduce noise
|
|
471
|
+
// Only log every 10th batch to reduce noise (skip in quiet mode)
|
|
285
472
|
if (
|
|
286
|
-
|
|
287
|
-
Math.floor(i / batchSize) + 1 ===
|
|
473
|
+
!quietMode &&
|
|
474
|
+
((Math.floor(i / batchSize) + 1) % 10 === 0 ||
|
|
475
|
+
Math.floor(i / batchSize) + 1 === 1)
|
|
288
476
|
) {
|
|
289
477
|
logger.info(
|
|
290
478
|
`Batch ${Math.floor(i / batchSize) + 1}: ${newRecords.length} new, ${updateRecords.length} updates`,
|
|
@@ -312,6 +500,7 @@ export class DatabaseService {
|
|
|
312
500
|
const { error: updateError } = await supabase
|
|
313
501
|
.from('uploader')
|
|
314
502
|
.update({
|
|
503
|
+
name: record.filename,
|
|
315
504
|
size: record.size,
|
|
316
505
|
modified_at: record.modified_at,
|
|
317
506
|
filename: record.filename,
|
|
@@ -325,8 +514,8 @@ export class DatabaseService {
|
|
|
325
514
|
}
|
|
326
515
|
}
|
|
327
516
|
totalUpdated += batchUpdated;
|
|
328
|
-
// Reduce logging noise - only log when there are updates
|
|
329
|
-
if (batchUpdated > 0) {
|
|
517
|
+
// Reduce logging noise - only log when there are updates (skip in quiet mode)
|
|
518
|
+
if (!quietMode && batchUpdated > 0) {
|
|
330
519
|
logger.info(`Updated ${batchUpdated} existing records`);
|
|
331
520
|
}
|
|
332
521
|
}
|
|
@@ -337,9 +526,11 @@ export class DatabaseService {
|
|
|
337
526
|
}
|
|
338
527
|
}
|
|
339
528
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
529
|
+
if (!quietMode) {
|
|
530
|
+
logger.success(
|
|
531
|
+
`Phase 1 Summary: ${totalInserted} new records inserted, ${totalUpdated} existing records updated`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
343
534
|
|
|
344
535
|
return {
|
|
345
536
|
totalInserted,
|
|
@@ -785,11 +976,13 @@ export class DatabaseService {
|
|
|
785
976
|
'ℹ️ No pedimento_simplificado records with arela_path found',
|
|
786
977
|
);
|
|
787
978
|
} else {
|
|
979
|
+
// pageNumber represents the current page (starts at 1)
|
|
980
|
+
const totalPages = pageNumber;
|
|
788
981
|
console.log(
|
|
789
|
-
`📋 Processed ${totalProcessed} pedimento records across ${
|
|
982
|
+
`📋 Processed ${totalProcessed} pedimento records across ${totalPages} page${totalPages !== 1 ? 's' : ''}`,
|
|
790
983
|
);
|
|
791
984
|
logger.info(
|
|
792
|
-
`Processed ${totalProcessed} pedimento records across ${
|
|
985
|
+
`Processed ${totalProcessed} pedimento records across ${totalPages} page${totalPages !== 1 ? 's' : ''}`,
|
|
793
986
|
);
|
|
794
987
|
}
|
|
795
988
|
|
|
@@ -926,11 +1119,13 @@ export class DatabaseService {
|
|
|
926
1119
|
const uniqueArelaPaths = [
|
|
927
1120
|
...new Set(allPedimentoRecords.map((r) => r.arela_path)),
|
|
928
1121
|
];
|
|
1122
|
+
// pageNumber represents the current page (starts at 1)
|
|
1123
|
+
const totalPages = pageNumber;
|
|
929
1124
|
console.log(
|
|
930
|
-
`📋 Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths for specified RFCs across ${
|
|
1125
|
+
`📋 Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths for specified RFCs across ${totalPages} page${totalPages !== 1 ? 's' : ''}`,
|
|
931
1126
|
);
|
|
932
1127
|
logger.info(
|
|
933
|
-
`Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths across ${
|
|
1128
|
+
`Found ${allPedimentoRecords.length} pedimento records with ${uniqueArelaPaths.length} unique arela_paths across ${totalPages} page${totalPages !== 1 ? 's' : ''}`,
|
|
934
1129
|
);
|
|
935
1130
|
|
|
936
1131
|
// Step 2: Process files with optimized single query per chunk
|
|
@@ -940,6 +1135,7 @@ export class DatabaseService {
|
|
|
940
1135
|
let globalFileCount = 0;
|
|
941
1136
|
const arelaPathChunkSize = 50;
|
|
942
1137
|
const batchSize = parseInt(options.batchSize) || 10;
|
|
1138
|
+
const filePageSize = 1000; // Supabase limit per request
|
|
943
1139
|
|
|
944
1140
|
// Import performance configuration
|
|
945
1141
|
const { performance: perfConfig } = appConfig;
|
|
@@ -959,28 +1155,60 @@ export class DatabaseService {
|
|
|
959
1155
|
` Processing arela_path chunk ${chunkNumber}/${totalChunks} (${arelaPathChunk.length} paths)`,
|
|
960
1156
|
);
|
|
961
1157
|
|
|
962
|
-
// Fetch all files for this chunk
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
.neq('status', 'file-uploaded')
|
|
968
|
-
.order('created_at');
|
|
1158
|
+
// Fetch all files for this chunk with pagination to handle >1000 records
|
|
1159
|
+
let allChunkFiles = [];
|
|
1160
|
+
let fileOffset = 0;
|
|
1161
|
+
let hasMoreFiles = true;
|
|
1162
|
+
let filePageNum = 1;
|
|
969
1163
|
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1164
|
+
while (hasMoreFiles) {
|
|
1165
|
+
const { data: batch, error: queryError } = await supabase
|
|
1166
|
+
.from('uploader')
|
|
1167
|
+
.select('id, original_path, arela_path, filename, rfc, document_type')
|
|
1168
|
+
.in('arela_path', arelaPathChunk)
|
|
1169
|
+
.neq('status', 'file-uploaded')
|
|
1170
|
+
.order('created_at')
|
|
1171
|
+
.range(fileOffset, fileOffset + filePageSize - 1);
|
|
975
1172
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1173
|
+
if (queryError) {
|
|
1174
|
+
const errorMsg = `Error fetching files for chunk ${chunkNumber} page ${filePageNum}: ${queryError.message}`;
|
|
1175
|
+
logger.error(errorMsg);
|
|
1176
|
+
throw new Error(errorMsg);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (!batch || batch.length === 0) {
|
|
1180
|
+
hasMoreFiles = false;
|
|
1181
|
+
if (filePageNum === 1) {
|
|
1182
|
+
// No files found at all for this chunk
|
|
1183
|
+
console.log(
|
|
1184
|
+
` ℹ️ Chunk ${chunkNumber}/${totalChunks}: No files to upload`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
logger.debug(
|
|
1191
|
+
`Chunk ${chunkNumber} page ${filePageNum}: fetched ${batch.length} files`,
|
|
979
1192
|
);
|
|
1193
|
+
allChunkFiles = allChunkFiles.concat(batch);
|
|
1194
|
+
|
|
1195
|
+
// Check if we need more pages
|
|
1196
|
+
if (batch.length < filePageSize) {
|
|
1197
|
+
hasMoreFiles = false;
|
|
1198
|
+
logger.debug(
|
|
1199
|
+
`Chunk ${chunkNumber}: Completed pagination with ${allChunkFiles.length} total files`,
|
|
1200
|
+
);
|
|
1201
|
+
} else {
|
|
1202
|
+
fileOffset += filePageSize;
|
|
1203
|
+
filePageNum++;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (allChunkFiles.length === 0) {
|
|
980
1208
|
continue;
|
|
981
1209
|
}
|
|
982
1210
|
|
|
983
|
-
const chunkFileCount =
|
|
1211
|
+
const chunkFileCount = allChunkFiles.length;
|
|
984
1212
|
globalFileCount += chunkFileCount;
|
|
985
1213
|
|
|
986
1214
|
console.log(
|
|
@@ -989,10 +1217,10 @@ export class DatabaseService {
|
|
|
989
1217
|
|
|
990
1218
|
// Process this batch of files immediately using concurrent processing
|
|
991
1219
|
// Split batch into upload batches
|
|
992
|
-
for (let j = 0; j <
|
|
993
|
-
const uploadBatch =
|
|
1220
|
+
for (let j = 0; j < allChunkFiles.length; j += batchSize) {
|
|
1221
|
+
const uploadBatch = allChunkFiles.slice(j, j + batchSize);
|
|
994
1222
|
const batchNum = Math.floor(j / batchSize) + 1;
|
|
995
|
-
const totalBatches = Math.ceil(
|
|
1223
|
+
const totalBatches = Math.ceil(allChunkFiles.length / batchSize);
|
|
996
1224
|
|
|
997
1225
|
console.log(
|
|
998
1226
|
` 📦 Processing upload batch ${batchNum}/${totalBatches} within chunk ${chunkNumber} (${uploadBatch.length} files)`,
|
|
@@ -1364,6 +1592,7 @@ export class DatabaseService {
|
|
|
1364
1592
|
.update({
|
|
1365
1593
|
status: 'file-uploaded',
|
|
1366
1594
|
message: 'Successfully uploaded to Supabase',
|
|
1595
|
+
processing_status: 'UPLOADED',
|
|
1367
1596
|
})
|
|
1368
1597
|
.eq('id', file.id);
|
|
1369
1598
|
|
|
@@ -1545,6 +1774,7 @@ export class DatabaseService {
|
|
|
1545
1774
|
.update({
|
|
1546
1775
|
status: 'file-uploaded',
|
|
1547
1776
|
message: 'Successfully uploaded to Arela API (batch)',
|
|
1777
|
+
processing_status: 'UPLOADED',
|
|
1548
1778
|
})
|
|
1549
1779
|
.in('id', successfulFileIds);
|
|
1550
1780
|
|
|
@@ -1684,6 +1914,432 @@ export class DatabaseService {
|
|
|
1684
1914
|
|
|
1685
1915
|
return { processed, uploaded, errors };
|
|
1686
1916
|
}
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* Insert upload session event into watch_uploads table
|
|
1920
|
+
* @param {Object} uploadEvent - Upload event from LoggingService
|
|
1921
|
+
* @param {string} sessionId - Session ID for tracking
|
|
1922
|
+
* @returns {Promise<Object>} Inserted record
|
|
1923
|
+
*/
|
|
1924
|
+
async insertUploadEvent(uploadEvent, sessionId) {
|
|
1925
|
+
const supabase = await this.#getSupabaseClient();
|
|
1926
|
+
|
|
1927
|
+
const record = {
|
|
1928
|
+
session_id: sessionId,
|
|
1929
|
+
timestamp: uploadEvent.timestamp || new Date().toISOString(),
|
|
1930
|
+
strategy: uploadEvent.strategy, // 'individual', 'batch', 'full-structure'
|
|
1931
|
+
file_count: uploadEvent.fileCount || 0,
|
|
1932
|
+
success_count: uploadEvent.successCount || 0,
|
|
1933
|
+
failure_count: uploadEvent.failureCount || 0,
|
|
1934
|
+
retry_count: uploadEvent.retryCount || 0,
|
|
1935
|
+
duration_ms: uploadEvent.duration || 0,
|
|
1936
|
+
status: uploadEvent.status || 'completed',
|
|
1937
|
+
metadata: uploadEvent.metadata || null,
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
try {
|
|
1941
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
1942
|
+
return await supabase.from('watch_uploads').insert([record]).select();
|
|
1943
|
+
}, `insert upload event for session ${sessionId}`);
|
|
1944
|
+
|
|
1945
|
+
if (error) {
|
|
1946
|
+
logger.error(`Failed to insert upload event: ${error.message}`);
|
|
1947
|
+
throw error;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
return data[0];
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
logger.error(`Error inserting upload event: ${error.message}`);
|
|
1953
|
+
throw error;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Insert retry event into watch_events table
|
|
1959
|
+
* @param {string} uploadEventId - ID of the parent upload event
|
|
1960
|
+
* @param {string} sessionId - Session ID for tracking
|
|
1961
|
+
* @param {Object} retryEvent - Retry event from LoggingService
|
|
1962
|
+
* @returns {Promise<Object>} Inserted record
|
|
1963
|
+
*/
|
|
1964
|
+
async insertRetryEvent(uploadEventId, sessionId, retryEvent) {
|
|
1965
|
+
const supabase = await this.#getSupabaseClient();
|
|
1966
|
+
|
|
1967
|
+
const record = {
|
|
1968
|
+
upload_event_id: uploadEventId,
|
|
1969
|
+
session_id: sessionId,
|
|
1970
|
+
timestamp: retryEvent.timestamp || new Date().toISOString(),
|
|
1971
|
+
attempt_number: retryEvent.attemptNumber || 0,
|
|
1972
|
+
error_message: retryEvent.error || null,
|
|
1973
|
+
backoff_ms: retryEvent.backoffMs || 0,
|
|
1974
|
+
type: 'retry',
|
|
1975
|
+
};
|
|
1976
|
+
|
|
1977
|
+
try {
|
|
1978
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
1979
|
+
return await supabase.from('watch_events').insert([record]).select();
|
|
1980
|
+
}, `insert retry event for upload ${uploadEventId}`);
|
|
1981
|
+
|
|
1982
|
+
if (error) {
|
|
1983
|
+
logger.error(`Failed to insert retry event: ${error.message}`);
|
|
1984
|
+
throw error;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
return data[0];
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
logger.error(`Error inserting retry event: ${error.message}`);
|
|
1990
|
+
throw error;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Get upload history for a session
|
|
1996
|
+
* @param {string} sessionId - Session ID to query
|
|
1997
|
+
* @param {Object} options - Query options (limit, offset, strategy filter)
|
|
1998
|
+
* @returns {Promise<Array>} Array of upload events
|
|
1999
|
+
*/
|
|
2000
|
+
async getSessionUploadHistory(sessionId, options = {}) {
|
|
2001
|
+
const supabase = await this.#getSupabaseClient();
|
|
2002
|
+
const limit = options.limit || 100;
|
|
2003
|
+
const offset = options.offset || 0;
|
|
2004
|
+
|
|
2005
|
+
try {
|
|
2006
|
+
let query = supabase
|
|
2007
|
+
.from('watch_uploads')
|
|
2008
|
+
.select('*')
|
|
2009
|
+
.eq('session_id', sessionId)
|
|
2010
|
+
.order('timestamp', { ascending: false })
|
|
2011
|
+
.range(offset, offset + limit - 1);
|
|
2012
|
+
|
|
2013
|
+
// Filter by strategy if provided
|
|
2014
|
+
if (options.strategy) {
|
|
2015
|
+
query = query.eq('strategy', options.strategy);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
2019
|
+
return await query;
|
|
2020
|
+
}, `fetch upload history for session ${sessionId}`);
|
|
2021
|
+
|
|
2022
|
+
if (error) {
|
|
2023
|
+
logger.error(`Failed to fetch upload history: ${error.message}`);
|
|
2024
|
+
throw error;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
return data || [];
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
logger.error(`Error fetching upload history: ${error.message}`);
|
|
2030
|
+
return [];
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Get retry history for an upload event
|
|
2036
|
+
* @param {string} uploadEventId - Upload event ID to query
|
|
2037
|
+
* @param {Object} options - Query options (limit, offset)
|
|
2038
|
+
* @returns {Promise<Array>} Array of retry events
|
|
2039
|
+
*/
|
|
2040
|
+
async getUploadRetryHistory(uploadEventId, options = {}) {
|
|
2041
|
+
const supabase = await this.#getSupabaseClient();
|
|
2042
|
+
const limit = options.limit || 100;
|
|
2043
|
+
const offset = options.offset || 0;
|
|
2044
|
+
|
|
2045
|
+
try {
|
|
2046
|
+
const { data, error } = await this.#queryWithRetry(async () => {
|
|
2047
|
+
return await supabase
|
|
2048
|
+
.from('watch_events')
|
|
2049
|
+
.select('*')
|
|
2050
|
+
.eq('upload_event_id', uploadEventId)
|
|
2051
|
+
.eq('type', 'retry')
|
|
2052
|
+
.order('timestamp', { ascending: true })
|
|
2053
|
+
.range(offset, offset + limit - 1);
|
|
2054
|
+
}, `fetch retry history for upload ${uploadEventId}`);
|
|
2055
|
+
|
|
2056
|
+
if (error) {
|
|
2057
|
+
logger.error(`Failed to fetch retry history: ${error.message}`);
|
|
2058
|
+
throw error;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return data || [];
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
logger.error(`Error fetching retry history: ${error.message}`);
|
|
2064
|
+
return [];
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Get session statistics
|
|
2070
|
+
* @param {string} sessionId - Session ID to analyze
|
|
2071
|
+
* @returns {Promise<Object>} Session statistics
|
|
2072
|
+
*/
|
|
2073
|
+
async getSessionStatistics(sessionId) {
|
|
2074
|
+
const supabase = await this.#getSupabaseClient();
|
|
2075
|
+
|
|
2076
|
+
try {
|
|
2077
|
+
// Fetch all upload events for the session
|
|
2078
|
+
const { data: uploads, error: uploadError } = await this.#queryWithRetry(
|
|
2079
|
+
async () => {
|
|
2080
|
+
return await supabase
|
|
2081
|
+
.from('watch_uploads')
|
|
2082
|
+
.select('*')
|
|
2083
|
+
.eq('session_id', sessionId);
|
|
2084
|
+
},
|
|
2085
|
+
`fetch statistics for session ${sessionId}`,
|
|
2086
|
+
);
|
|
2087
|
+
|
|
2088
|
+
if (uploadError) {
|
|
2089
|
+
throw uploadError;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Fetch all retry events for the session
|
|
2093
|
+
const { data: retries, error: retryError } = await this.#queryWithRetry(
|
|
2094
|
+
async () => {
|
|
2095
|
+
return await supabase
|
|
2096
|
+
.from('watch_events')
|
|
2097
|
+
.select('*')
|
|
2098
|
+
.eq('session_id', sessionId)
|
|
2099
|
+
.eq('type', 'retry');
|
|
2100
|
+
},
|
|
2101
|
+
`fetch retry statistics for session ${sessionId}`,
|
|
2102
|
+
);
|
|
2103
|
+
|
|
2104
|
+
if (retryError) {
|
|
2105
|
+
throw retryError;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// Calculate statistics
|
|
2109
|
+
const stats = {
|
|
2110
|
+
sessionId,
|
|
2111
|
+
totalUploadEvents: uploads?.length || 0,
|
|
2112
|
+
totalRetryEvents: retries?.length || 0,
|
|
2113
|
+
totalFileCount: 0,
|
|
2114
|
+
totalSuccessCount: 0,
|
|
2115
|
+
totalFailureCount: 0,
|
|
2116
|
+
totalRetryCount: 0,
|
|
2117
|
+
totalDuration: 0,
|
|
2118
|
+
byStrategy: {
|
|
2119
|
+
individual: {
|
|
2120
|
+
uploadCount: 0,
|
|
2121
|
+
totalFiles: 0,
|
|
2122
|
+
totalSuccess: 0,
|
|
2123
|
+
totalFailure: 0,
|
|
2124
|
+
successRate: 0,
|
|
2125
|
+
totalDuration: 0,
|
|
2126
|
+
},
|
|
2127
|
+
batch: {
|
|
2128
|
+
uploadCount: 0,
|
|
2129
|
+
totalFiles: 0,
|
|
2130
|
+
totalSuccess: 0,
|
|
2131
|
+
totalFailure: 0,
|
|
2132
|
+
successRate: 0,
|
|
2133
|
+
totalDuration: 0,
|
|
2134
|
+
},
|
|
2135
|
+
'full-structure': {
|
|
2136
|
+
uploadCount: 0,
|
|
2137
|
+
totalFiles: 0,
|
|
2138
|
+
totalSuccess: 0,
|
|
2139
|
+
totalFailure: 0,
|
|
2140
|
+
successRate: 0,
|
|
2141
|
+
totalDuration: 0,
|
|
2142
|
+
},
|
|
2143
|
+
},
|
|
2144
|
+
retryStats: {
|
|
2145
|
+
totalRetries: retries?.length || 0,
|
|
2146
|
+
uniqueUploadsWithRetries: new Set(
|
|
2147
|
+
retries?.map((r) => r.upload_event_id) || [],
|
|
2148
|
+
).size,
|
|
2149
|
+
totalRetryDuration:
|
|
2150
|
+
retries?.reduce((sum, r) => sum + (r.backoff_ms || 0), 0) || 0,
|
|
2151
|
+
},
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
// Process upload events
|
|
2155
|
+
if (uploads && uploads.length > 0) {
|
|
2156
|
+
uploads.forEach((upload) => {
|
|
2157
|
+
stats.totalFileCount += upload.file_count || 0;
|
|
2158
|
+
stats.totalSuccessCount += upload.success_count || 0;
|
|
2159
|
+
stats.totalFailureCount += upload.failure_count || 0;
|
|
2160
|
+
stats.totalRetryCount += upload.retry_count || 0;
|
|
2161
|
+
stats.totalDuration += upload.duration_ms || 0;
|
|
2162
|
+
|
|
2163
|
+
const strategyKey = upload.strategy || 'individual';
|
|
2164
|
+
if (stats.byStrategy[strategyKey]) {
|
|
2165
|
+
stats.byStrategy[strategyKey].uploadCount += 1;
|
|
2166
|
+
stats.byStrategy[strategyKey].totalFiles += upload.file_count || 0;
|
|
2167
|
+
stats.byStrategy[strategyKey].totalSuccess +=
|
|
2168
|
+
upload.success_count || 0;
|
|
2169
|
+
stats.byStrategy[strategyKey].totalFailure +=
|
|
2170
|
+
upload.failure_count || 0;
|
|
2171
|
+
stats.byStrategy[strategyKey].totalDuration +=
|
|
2172
|
+
upload.duration_ms || 0;
|
|
2173
|
+
|
|
2174
|
+
// Calculate success rate
|
|
2175
|
+
const totalFiles =
|
|
2176
|
+
stats.byStrategy[strategyKey].totalSuccess +
|
|
2177
|
+
stats.byStrategy[strategyKey].totalFailure;
|
|
2178
|
+
if (totalFiles > 0) {
|
|
2179
|
+
stats.byStrategy[strategyKey].successRate = (
|
|
2180
|
+
(stats.byStrategy[strategyKey].totalSuccess / totalFiles) *
|
|
2181
|
+
100
|
|
2182
|
+
).toFixed(2);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
return stats;
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
logger.error(`Error calculating session statistics: ${error.message}`);
|
|
2191
|
+
return null;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Delete old session data (cleanup)
|
|
2197
|
+
* @param {number} daysOld - Delete sessions older than this many days
|
|
2198
|
+
* @returns {Promise<Object>} Deletion results
|
|
2199
|
+
*/
|
|
2200
|
+
async cleanupOldSessions(daysOld = 30) {
|
|
2201
|
+
const supabase = await this.#getSupabaseClient();
|
|
2202
|
+
|
|
2203
|
+
try {
|
|
2204
|
+
const cutoffDate = new Date();
|
|
2205
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
2206
|
+
|
|
2207
|
+
// Get sessions to delete
|
|
2208
|
+
const { data: sessionsToDelete, error: fetchError } =
|
|
2209
|
+
await this.#queryWithRetry(async () => {
|
|
2210
|
+
return await supabase
|
|
2211
|
+
.from('watch_uploads')
|
|
2212
|
+
.select('session_id')
|
|
2213
|
+
.lt('timestamp', cutoffDate.toISOString())
|
|
2214
|
+
.distinct();
|
|
2215
|
+
}, `fetch sessions older than ${daysOld} days`);
|
|
2216
|
+
|
|
2217
|
+
if (fetchError) {
|
|
2218
|
+
throw fetchError;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
let deletedUploads = 0;
|
|
2222
|
+
let deletedEvents = 0;
|
|
2223
|
+
|
|
2224
|
+
if (sessionsToDelete && sessionsToDelete.length > 0) {
|
|
2225
|
+
const sessionIds = sessionsToDelete.map((s) => s.session_id);
|
|
2226
|
+
|
|
2227
|
+
// Delete events
|
|
2228
|
+
const { count: eventCount, error: eventError } =
|
|
2229
|
+
await this.#queryWithRetry(async () => {
|
|
2230
|
+
return await supabase
|
|
2231
|
+
.from('watch_events')
|
|
2232
|
+
.delete()
|
|
2233
|
+
.in('session_id', sessionIds);
|
|
2234
|
+
}, `delete events for old sessions`);
|
|
2235
|
+
|
|
2236
|
+
if (!eventError) {
|
|
2237
|
+
deletedEvents = eventCount || 0;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// Delete uploads
|
|
2241
|
+
const { count: uploadCount, error: uploadError } =
|
|
2242
|
+
await this.#queryWithRetry(async () => {
|
|
2243
|
+
return await supabase
|
|
2244
|
+
.from('watch_uploads')
|
|
2245
|
+
.delete()
|
|
2246
|
+
.in('session_id', sessionIds);
|
|
2247
|
+
}, `delete old session uploads`);
|
|
2248
|
+
|
|
2249
|
+
if (!uploadError) {
|
|
2250
|
+
deletedUploads = uploadCount || 0;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
return {
|
|
2255
|
+
deletedUploads,
|
|
2256
|
+
deletedEvents,
|
|
2257
|
+
sessionsDeleted: sessionsToDelete?.length || 0,
|
|
2258
|
+
};
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
logger.error(`Error cleaning up old sessions: ${error.message}`);
|
|
2261
|
+
return { deletedUploads: 0, deletedEvents: 0, sessionsDeleted: 0 };
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/**
|
|
2266
|
+
* Cleanup database connections and resources
|
|
2267
|
+
* Called during graceful shutdown
|
|
2268
|
+
* @returns {Promise<Object>} Cleanup results
|
|
2269
|
+
*/
|
|
2270
|
+
async cleanup() {
|
|
2271
|
+
try {
|
|
2272
|
+
logger.info('DatabaseService: Starting cleanup...');
|
|
2273
|
+
|
|
2274
|
+
// Commit any pending transactions
|
|
2275
|
+
const transactionResult = await this.commitPendingTransactions();
|
|
2276
|
+
|
|
2277
|
+
// Close database connections
|
|
2278
|
+
const closeResult = await this.closeConnections();
|
|
2279
|
+
|
|
2280
|
+
logger.info('DatabaseService: Cleanup complete');
|
|
2281
|
+
|
|
2282
|
+
return {
|
|
2283
|
+
success: true,
|
|
2284
|
+
transactionsCommitted: transactionResult.count,
|
|
2285
|
+
connectionsClosedResult: closeResult,
|
|
2286
|
+
};
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
logger.error(`DatabaseService: Error during cleanup: ${error.message}`);
|
|
2289
|
+
return {
|
|
2290
|
+
success: false,
|
|
2291
|
+
error: error.message,
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/**
|
|
2297
|
+
* Commit any pending transactions before shutdown
|
|
2298
|
+
* @private
|
|
2299
|
+
* @returns {Promise<Object>} Results
|
|
2300
|
+
*/
|
|
2301
|
+
async commitPendingTransactions() {
|
|
2302
|
+
try {
|
|
2303
|
+
logger.debug('DatabaseService: Committing pending transactions...');
|
|
2304
|
+
|
|
2305
|
+
// Note: This is a placeholder for actual transaction handling
|
|
2306
|
+
// In a real implementation, you would track active transactions
|
|
2307
|
+
// and ensure they are properly committed before shutdown
|
|
2308
|
+
|
|
2309
|
+
logger.debug('DatabaseService: Pending transactions committed');
|
|
2310
|
+
return { count: 0, success: true };
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
logger.error(
|
|
2313
|
+
`DatabaseService: Error committing transactions: ${error.message}`,
|
|
2314
|
+
);
|
|
2315
|
+
return { count: 0, success: false, error: error.message };
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* Close all database connections
|
|
2321
|
+
* @private
|
|
2322
|
+
* @returns {Promise<Object>} Results
|
|
2323
|
+
*/
|
|
2324
|
+
async closeConnections() {
|
|
2325
|
+
try {
|
|
2326
|
+
logger.debug('DatabaseService: Closing database connections...');
|
|
2327
|
+
|
|
2328
|
+
// Close Supabase connection if available
|
|
2329
|
+
if (this.supabase) {
|
|
2330
|
+
// Supabase client will handle connection cleanup automatically
|
|
2331
|
+
logger.debug('DatabaseService: Supabase connection cleanup initiated');
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
logger.info('DatabaseService: All database connections closed');
|
|
2335
|
+
return { success: true };
|
|
2336
|
+
} catch (error) {
|
|
2337
|
+
logger.error(
|
|
2338
|
+
`DatabaseService: Error closing connections: ${error.message}`,
|
|
2339
|
+
);
|
|
2340
|
+
return { success: false, error: error.message };
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
1687
2343
|
}
|
|
1688
2344
|
|
|
1689
2345
|
// Export singleton instance
|