@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.
Files changed (43) hide show
  1. package/.env.template +66 -0
  2. package/README.md +263 -62
  3. package/docs/API_ENDPOINTS_FOR_DETECTION.md +647 -0
  4. package/docs/QUICK_REFERENCE_API_DETECTION.md +264 -0
  5. package/docs/REFACTORING_SUMMARY_DETECT_PEDIMENTOS.md +200 -0
  6. package/package.json +3 -2
  7. package/scripts/cleanup-ds-store.js +109 -0
  8. package/scripts/cleanup-system-files.js +69 -0
  9. package/scripts/tests/phase-7-features.test.js +415 -0
  10. package/scripts/tests/signal-handling.test.js +275 -0
  11. package/scripts/tests/smart-watch-integration.test.js +554 -0
  12. package/scripts/tests/watch-service-integration.test.js +584 -0
  13. package/src/commands/UploadCommand.js +31 -4
  14. package/src/commands/WatchCommand.js +1342 -0
  15. package/src/config/config.js +270 -2
  16. package/src/document-type-shared.js +2 -0
  17. package/src/document-types/support-document.js +200 -0
  18. package/src/file-detection.js +9 -1
  19. package/src/index.js +163 -4
  20. package/src/services/AdvancedFilterService.js +505 -0
  21. package/src/services/AutoProcessingService.js +749 -0
  22. package/src/services/BenchmarkingService.js +381 -0
  23. package/src/services/DatabaseService.js +1019 -539
  24. package/src/services/ErrorMonitor.js +275 -0
  25. package/src/services/LoggingService.js +419 -1
  26. package/src/services/MonitoringService.js +401 -0
  27. package/src/services/PerformanceOptimizer.js +511 -0
  28. package/src/services/ReportingService.js +511 -0
  29. package/src/services/SignalHandler.js +255 -0
  30. package/src/services/SmartWatchDatabaseService.js +527 -0
  31. package/src/services/WatchService.js +783 -0
  32. package/src/services/upload/ApiUploadService.js +447 -3
  33. package/src/services/upload/MultiApiUploadService.js +233 -0
  34. package/src/services/upload/SupabaseUploadService.js +12 -5
  35. package/src/services/upload/UploadServiceFactory.js +24 -0
  36. package/src/utils/CleanupManager.js +262 -0
  37. package/src/utils/FileOperations.js +44 -0
  38. package/src/utils/WatchEventHandler.js +522 -0
  39. package/supabase/migrations/001_create_initial_schema.sql +366 -0
  40. package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  41. package/.envbackup +0 -37
  42. package/SUPABASE_UPLOAD_FIX.md +0 -157
  43. package/commands.md +0 -14
@@ -0,0 +1,505 @@
1
+ /**
2
+ * AdvancedFilterService.js
3
+ * Phase 7 - Task 1: Advanced Filtering & Validation
4
+ *
5
+ * Provides advanced filtering options for files including:
6
+ * - File type filtering
7
+ * - File size filtering
8
+ * - Date range filtering
9
+ * - Custom validation rules
10
+ * - Complex filter combinations
11
+ */
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+
15
+ class AdvancedFilterService {
16
+ constructor(logger) {
17
+ this.logger = logger;
18
+
19
+ // Filter presets
20
+ this.filterPresets = {
21
+ pdfOnly: {
22
+ name: 'PDF Files Only',
23
+ filters: [{ type: 'extension', value: '.pdf', operator: 'equals' }],
24
+ },
25
+ largeFiles: {
26
+ name: 'Large Files (>10MB)',
27
+ filters: [
28
+ { type: 'size', value: 10 * 1024 * 1024, operator: 'greaterThan' },
29
+ ],
30
+ },
31
+ recentFiles: {
32
+ name: 'Recent Files (Last 7 days)',
33
+ filters: [{ type: 'dateModified', value: 7, operator: 'daysAgo' }],
34
+ },
35
+ documentsOnly: {
36
+ name: 'Documents',
37
+ filters: [
38
+ {
39
+ type: 'extension',
40
+ value: ['.pdf', '.doc', '.docx', '.xls', '.xlsx'],
41
+ operator: 'in',
42
+ },
43
+ ],
44
+ },
45
+ };
46
+
47
+ // Validation rules
48
+ this.validationRules = {
49
+ pdfs: {
50
+ extension: '.pdf',
51
+ minSize: 0,
52
+ maxSize: 500 * 1024 * 1024, // 500MB
53
+ allowedFormats: ['pdf'],
54
+ },
55
+ documents: {
56
+ extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx'],
57
+ minSize: 0,
58
+ maxSize: 100 * 1024 * 1024,
59
+ allowedFormats: ['pdf', 'doc', 'docx', 'xls', 'xlsx'],
60
+ },
61
+ images: {
62
+ extension: ['.jpg', '.jpeg', '.png', '.gif', '.bmp'],
63
+ minSize: 0,
64
+ maxSize: 50 * 1024 * 1024,
65
+ allowedFormats: ['jpg', 'jpeg', 'png', 'gif', 'bmp'],
66
+ },
67
+ };
68
+
69
+ // Statistics
70
+ this.stats = {
71
+ filesFiltered: 0,
72
+ filesMatched: 0,
73
+ filesRejected: 0,
74
+ filterApplications: 0,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Apply multiple filters to a file list
80
+ */
81
+ async filterFiles(files, filterConfig) {
82
+ const startTime = Date.now();
83
+ this.stats.filterApplications++;
84
+
85
+ let filtered = [...files];
86
+ const filterResults = {
87
+ original: files.length,
88
+ stages: [],
89
+ final: 0,
90
+ duration: 0,
91
+ rejectedFiles: [],
92
+ };
93
+
94
+ if (!filterConfig || Object.keys(filterConfig).length === 0) {
95
+ return {
96
+ files: filtered,
97
+ results: filterResults,
98
+ };
99
+ }
100
+
101
+ // Apply each filter in sequence
102
+ const filterEntries = Object.entries(filterConfig);
103
+ for (const [filterType, filterValue] of filterEntries) {
104
+ const beforeCount = filtered.length;
105
+
106
+ switch (filterType) {
107
+ case 'extension':
108
+ filtered = await this._filterByExtension(filtered, filterValue);
109
+ break;
110
+ case 'size':
111
+ filtered = await this._filterBySize(filtered, filterValue);
112
+ break;
113
+ case 'dateModified':
114
+ filtered = await this._filterByDateModified(filtered, filterValue);
115
+ break;
116
+ case 'dateCreated':
117
+ filtered = await this._filterByDateCreated(filtered, filterValue);
118
+ break;
119
+ case 'custom':
120
+ filtered = await this._filterByCustomRule(filtered, filterValue);
121
+ break;
122
+ case 'validation':
123
+ filtered = await this._filterByValidation(filtered, filterValue);
124
+ break;
125
+ }
126
+
127
+ const afterCount = filtered.length;
128
+ filterResults.stages.push({
129
+ filterType,
130
+ before: beforeCount,
131
+ after: afterCount,
132
+ removed: beforeCount - afterCount,
133
+ });
134
+ }
135
+
136
+ filterResults.final = filtered.length;
137
+ filterResults.duration = Date.now() - startTime;
138
+
139
+ this.stats.filesFiltered += files.length;
140
+ this.stats.filesMatched += filtered.length;
141
+ this.stats.filesRejected += files.length - filtered.length;
142
+
143
+ this.logger.debug(
144
+ `Filtered ${files.length} files to ${filtered.length} (${filterResults.duration}ms)`,
145
+ );
146
+
147
+ return {
148
+ files: filtered,
149
+ results: filterResults,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Filter by file extension
155
+ */
156
+ async _filterByExtension(files, extensions) {
157
+ const extensionList = Array.isArray(extensions) ? extensions : [extensions];
158
+ const normalized = extensionList.map((ext) => ext.toLowerCase());
159
+
160
+ return files.filter((file) => {
161
+ const fileExt = path.extname(file).toLowerCase();
162
+ return normalized.includes(fileExt);
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Filter by file size
168
+ */
169
+ async _filterBySize(files, sizeConfig) {
170
+ const filtered = [];
171
+
172
+ for (const file of files) {
173
+ try {
174
+ const stats = await fs.stat(file);
175
+
176
+ if (sizeConfig.min && stats.size < sizeConfig.min) continue;
177
+ if (sizeConfig.max && stats.size > sizeConfig.max) continue;
178
+
179
+ filtered.push(file);
180
+ } catch (error) {
181
+ this.logger.warn(`Failed to stat file: ${file}`, error.message);
182
+ }
183
+ }
184
+
185
+ return filtered;
186
+ }
187
+
188
+ /**
189
+ * Filter by date modified
190
+ */
191
+ async _filterByDateModified(files, dateConfig) {
192
+ const filtered = [];
193
+ const now = Date.now();
194
+
195
+ for (const file of files) {
196
+ try {
197
+ const stats = await fs.stat(file);
198
+ const fileTime = stats.mtime.getTime();
199
+
200
+ if (dateConfig.daysAgo !== undefined) {
201
+ const daysInMs = dateConfig.daysAgo * 24 * 60 * 60 * 1000;
202
+ if (now - fileTime > daysInMs) continue;
203
+ }
204
+
205
+ if (
206
+ dateConfig.before &&
207
+ fileTime > new Date(dateConfig.before).getTime()
208
+ )
209
+ continue;
210
+ if (dateConfig.after && fileTime < new Date(dateConfig.after).getTime())
211
+ continue;
212
+
213
+ filtered.push(file);
214
+ } catch (error) {
215
+ this.logger.warn(`Failed to stat file: ${file}`, error.message);
216
+ }
217
+ }
218
+
219
+ return filtered;
220
+ }
221
+
222
+ /**
223
+ * Filter by date created
224
+ */
225
+ async _filterByDateCreated(files, dateConfig) {
226
+ const filtered = [];
227
+
228
+ for (const file of files) {
229
+ try {
230
+ const stats = await fs.stat(file);
231
+ const birthTime = stats.birthtime.getTime();
232
+ const now = Date.now();
233
+
234
+ if (dateConfig.daysAgo !== undefined) {
235
+ const daysInMs = dateConfig.daysAgo * 24 * 60 * 60 * 1000;
236
+ if (now - birthTime > daysInMs) continue;
237
+ }
238
+
239
+ filtered.push(file);
240
+ } catch (error) {
241
+ this.logger.warn(`Failed to stat file: ${file}`, error.message);
242
+ }
243
+ }
244
+
245
+ return filtered;
246
+ }
247
+
248
+ /**
249
+ * Filter by custom validation rule
250
+ */
251
+ async _filterByCustomRule(files, ruleFunction) {
252
+ const filtered = [];
253
+
254
+ for (const file of files) {
255
+ try {
256
+ const stats = await fs.stat(file);
257
+ const shouldInclude = await ruleFunction({
258
+ path: file,
259
+ name: path.basename(file),
260
+ size: stats.size,
261
+ modified: stats.mtime,
262
+ created: stats.birthtime,
263
+ isDirectory: stats.isDirectory(),
264
+ });
265
+
266
+ if (shouldInclude) {
267
+ filtered.push(file);
268
+ }
269
+ } catch (error) {
270
+ this.logger.warn(`Custom filter error for ${file}:`, error.message);
271
+ }
272
+ }
273
+
274
+ return filtered;
275
+ }
276
+
277
+ /**
278
+ * Filter by validation rule preset
279
+ */
280
+ async _filterByValidation(files, validationRule) {
281
+ const rule =
282
+ typeof validationRule === 'string'
283
+ ? this.validationRules[validationRule]
284
+ : validationRule;
285
+
286
+ if (!rule) {
287
+ this.logger.warn(`Unknown validation rule: ${validationRule}`);
288
+ return files;
289
+ }
290
+
291
+ const filtered = [];
292
+
293
+ for (const file of files) {
294
+ try {
295
+ const ext = path.extname(file).toLowerCase();
296
+ const stats = await fs.stat(file);
297
+
298
+ // Check extension
299
+ const allowedExts = Array.isArray(rule.extension)
300
+ ? rule.extension
301
+ : [rule.extension];
302
+ if (!allowedExts.includes(ext)) continue;
303
+
304
+ // Check size
305
+ if (stats.size < rule.minSize) continue;
306
+ if (stats.size > rule.maxSize) continue;
307
+
308
+ filtered.push(file);
309
+ } catch (error) {
310
+ this.logger.warn(`Validation error for ${file}:`, error.message);
311
+ }
312
+ }
313
+
314
+ return filtered;
315
+ }
316
+
317
+ /**
318
+ * Create a complex filter query
319
+ */
320
+ createFilterQuery(config) {
321
+ const query = {
322
+ filters: [],
323
+ logic: config.logic || 'AND',
324
+ description: config.description || 'Custom filter query',
325
+ };
326
+
327
+ if (config.extensions) {
328
+ query.filters.push({
329
+ type: 'extension',
330
+ operator: 'in',
331
+ values: Array.isArray(config.extensions)
332
+ ? config.extensions
333
+ : [config.extensions],
334
+ });
335
+ }
336
+
337
+ if (config.minSize !== undefined) {
338
+ query.filters.push({
339
+ type: 'size',
340
+ operator: 'greaterThan',
341
+ value: config.minSize,
342
+ });
343
+ }
344
+
345
+ if (config.maxSize !== undefined) {
346
+ query.filters.push({
347
+ type: 'size',
348
+ operator: 'lessThan',
349
+ value: config.maxSize,
350
+ });
351
+ }
352
+
353
+ if (config.modifiedInDays !== undefined) {
354
+ query.filters.push({
355
+ type: 'dateModified',
356
+ operator: 'daysAgo',
357
+ value: config.modifiedInDays,
358
+ });
359
+ }
360
+
361
+ return query;
362
+ }
363
+
364
+ /**
365
+ * Apply a filter preset
366
+ */
367
+ async applyPreset(files, presetName) {
368
+ const preset = this.filterPresets[presetName];
369
+
370
+ if (!preset) {
371
+ this.logger.error(`Unknown preset: ${presetName}`);
372
+ return { files, results: { error: `Unknown preset: ${presetName}` } };
373
+ }
374
+
375
+ const filterConfig = {};
376
+
377
+ for (const filter of preset.filters) {
378
+ if (filter.type === 'extension') {
379
+ filterConfig.extension = filter.value;
380
+ } else if (filter.type === 'size') {
381
+ filterConfig.size = {
382
+ [filter.operator === 'greaterThan' ? 'min' : 'max']: filter.value,
383
+ };
384
+ } else if (filter.type === 'dateModified') {
385
+ filterConfig.dateModified = { daysAgo: filter.value };
386
+ }
387
+ }
388
+
389
+ return this.filterFiles(files, filterConfig);
390
+ }
391
+
392
+ /**
393
+ * Get available filter presets
394
+ */
395
+ getFilterPresets() {
396
+ return Object.entries(this.filterPresets).map(([key, preset]) => ({
397
+ key,
398
+ name: preset.name,
399
+ description: `Filter: ${preset.filters.map((f) => f.type).join(', ')}`,
400
+ }));
401
+ }
402
+
403
+ /**
404
+ * Get available validation rules
405
+ */
406
+ getValidationRules() {
407
+ return Object.entries(this.validationRules).map(([key, rule]) => ({
408
+ key,
409
+ extensions: Array.isArray(rule.extension)
410
+ ? rule.extension
411
+ : [rule.extension],
412
+ minSize: rule.minSize,
413
+ maxSize: rule.maxSize,
414
+ }));
415
+ }
416
+
417
+ /**
418
+ * Validate a single file against rule
419
+ */
420
+ async validateFile(filePath, ruleName) {
421
+ const rule = this.validationRules[ruleName];
422
+
423
+ if (!rule) {
424
+ return { valid: false, error: `Unknown rule: ${ruleName}` };
425
+ }
426
+
427
+ try {
428
+ const stats = await fs.stat(filePath);
429
+ const ext = path.extname(filePath).toLowerCase();
430
+ const allowedExts = Array.isArray(rule.extension)
431
+ ? rule.extension
432
+ : [rule.extension];
433
+
434
+ const validation = {
435
+ filePath,
436
+ ruleName,
437
+ valid: true,
438
+ checks: [],
439
+ };
440
+
441
+ // Check extension
442
+ const extCheck = {
443
+ check: 'extension',
444
+ allowed: allowedExts,
445
+ actual: ext,
446
+ passed: allowedExts.includes(ext),
447
+ };
448
+ validation.checks.push(extCheck);
449
+ if (!extCheck.passed) validation.valid = false;
450
+
451
+ // Check size
452
+ const sizeCheck = {
453
+ check: 'size',
454
+ min: rule.minSize,
455
+ max: rule.maxSize,
456
+ actual: stats.size,
457
+ passed: stats.size >= rule.minSize && stats.size <= rule.maxSize,
458
+ };
459
+ validation.checks.push(sizeCheck);
460
+ if (!sizeCheck.passed) validation.valid = false;
461
+
462
+ return validation;
463
+ } catch (error) {
464
+ return {
465
+ valid: false,
466
+ error: error.message,
467
+ filePath,
468
+ };
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Get filter statistics
474
+ */
475
+ getStatistics() {
476
+ return {
477
+ ...this.stats,
478
+ averageFilesPerFilter:
479
+ this.stats.filterApplications > 0
480
+ ? Math.round(this.stats.filesFiltered / this.stats.filterApplications)
481
+ : 0,
482
+ matchRate:
483
+ this.stats.filesFiltered > 0
484
+ ? (
485
+ (this.stats.filesMatched / this.stats.filesFiltered) *
486
+ 100
487
+ ).toFixed(2) + '%'
488
+ : 'N/A',
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Reset statistics
494
+ */
495
+ resetStatistics() {
496
+ this.stats = {
497
+ filesFiltered: 0,
498
+ filesMatched: 0,
499
+ filesRejected: 0,
500
+ filterApplications: 0,
501
+ };
502
+ }
503
+ }
504
+
505
+ export default AdvancedFilterService;