@arela/uploader 0.2.12 → 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.
Files changed (38) hide show
  1. package/.env.template +66 -0
  2. package/.vscode/settings.json +1 -0
  3. package/README.md +134 -58
  4. package/SUPABASE_UPLOAD_FIX.md +157 -0
  5. package/package.json +3 -2
  6. package/scripts/cleanup-ds-store.js +109 -0
  7. package/scripts/cleanup-system-files.js +69 -0
  8. package/scripts/tests/phase-7-features.test.js +415 -0
  9. package/scripts/tests/signal-handling.test.js +275 -0
  10. package/scripts/tests/smart-watch-integration.test.js +554 -0
  11. package/scripts/tests/watch-service-integration.test.js +584 -0
  12. package/src/commands/UploadCommand.js +36 -2
  13. package/src/commands/WatchCommand.js +1305 -0
  14. package/src/config/config.js +113 -0
  15. package/src/document-type-shared.js +2 -0
  16. package/src/document-types/support-document.js +201 -0
  17. package/src/file-detection.js +2 -1
  18. package/src/index.js +44 -0
  19. package/src/services/AdvancedFilterService.js +505 -0
  20. package/src/services/AutoProcessingService.js +639 -0
  21. package/src/services/BenchmarkingService.js +381 -0
  22. package/src/services/DatabaseService.js +723 -170
  23. package/src/services/ErrorMonitor.js +275 -0
  24. package/src/services/LoggingService.js +419 -1
  25. package/src/services/MonitoringService.js +401 -0
  26. package/src/services/PerformanceOptimizer.js +511 -0
  27. package/src/services/ReportingService.js +511 -0
  28. package/src/services/SignalHandler.js +255 -0
  29. package/src/services/SmartWatchDatabaseService.js +527 -0
  30. package/src/services/WatchService.js +783 -0
  31. package/src/services/upload/ApiUploadService.js +30 -4
  32. package/src/services/upload/SupabaseUploadService.js +28 -6
  33. package/src/utils/CleanupManager.js +262 -0
  34. package/src/utils/FileOperations.js +41 -0
  35. package/src/utils/WatchEventHandler.js +517 -0
  36. package/supabase/migrations/001_create_initial_schema.sql +366 -0
  37. package/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  38. package/commands.md +0 -6
@@ -7,6 +7,7 @@ import fetch from 'node-fetch';
7
7
  import path from 'path';
8
8
 
9
9
  import appConfig from '../../config/config.js';
10
+ import logger from '../LoggingService.js';
10
11
  import { BaseUploadService } from './BaseUploadService.js';
11
12
 
12
13
  /**
@@ -57,22 +58,47 @@ export class ApiUploadService extends BaseUploadService {
57
58
  * @returns {Promise<Object>} API response
58
59
  */
59
60
  async upload(files, options) {
61
+ // Validate files parameter
62
+ if (!files || !Array.isArray(files)) {
63
+ logger.warn(`Invalid files parameter: ${typeof files}`);
64
+ throw new Error('Files must be an array');
65
+ }
66
+
60
67
  const formData = new FormData();
61
68
 
69
+ // Filter out system files (macOS, Windows, etc.)
70
+ const systemFilePattern = /^\.|__pycache__|\.pyc|\.swp|\.swo|Thumbs\.db|desktop\.ini|DS_Store|\$RECYCLE\.BIN|System Volume Information|~\$|\.tmp/i;
71
+ const filteredFiles = files.filter(file => {
72
+ const fileName = file.name || path.basename(file.path);
73
+ if (systemFilePattern.test(fileName)) {
74
+ logger.warn(`Skipping system file from upload: ${fileName}`);
75
+ return false;
76
+ }
77
+ return true;
78
+ });
79
+
80
+ if (filteredFiles.length === 0) {
81
+ throw new Error('No valid files to upload after filtering system files');
82
+ }
83
+
62
84
  // Add files to form data asynchronously
63
- for (const file of files) {
85
+ for (const file of filteredFiles) {
64
86
  try {
65
87
  // Check file size for streaming vs buffer approach
66
- const stats = await fs.promises.stat(file.path);
88
+ let size = file.size;
89
+ if (size === undefined || size === null) {
90
+ const stats = await fs.promises.stat(file.path);
91
+ size = stats.size;
92
+ }
67
93
  const fileSizeThreshold = 10 * 1024 * 1024; // 10MB threshold
68
94
 
69
- if (stats.size > fileSizeThreshold) {
95
+ if (size > fileSizeThreshold) {
70
96
  // Use streaming for large files
71
97
  const fileStream = fs.createReadStream(file.path);
72
98
  formData.append('files', fileStream, {
73
99
  filename: file.name,
74
100
  contentType: file.contentType,
75
- knownLength: stats.size,
101
+ knownLength: size,
76
102
  });
77
103
  } else {
78
104
  // Use buffer for smaller files
@@ -26,9 +26,16 @@ export class SupabaseUploadService extends BaseUploadService {
26
26
  }
27
27
 
28
28
  if (!appConfig.supabase.url || !appConfig.supabase.key || !this.bucket) {
29
- throw new Error('Missing Supabase configuration');
29
+ const missing = [];
30
+ if (!appConfig.supabase.url) missing.push('SUPABASE_URL');
31
+ if (!appConfig.supabase.key) missing.push('SUPABASE_KEY');
32
+ if (!this.bucket) missing.push('SUPABASE_BUCKET');
33
+ throw new Error(`Missing Supabase configuration: ${missing.join(', ')}`);
30
34
  }
31
35
 
36
+ console.log(`šŸ”Œ Connecting to Supabase: ${appConfig.supabase.url}`);
37
+ console.log(`šŸ“¦ Using bucket: ${this.bucket}`);
38
+
32
39
  this.client = createClient(appConfig.supabase.url, appConfig.supabase.key, {
33
40
  db: {
34
41
  schema: 'public',
@@ -48,11 +55,19 @@ export class SupabaseUploadService extends BaseUploadService {
48
55
  },
49
56
  });
50
57
 
51
- // Test connection
52
- const { error } = await this.client.storage.from(this.bucket).list('');
58
+ // Test connection and bucket access
59
+ console.log(`🧪 Testing bucket access...`);
60
+ const { error } = await this.client.storage
61
+ .from(this.bucket)
62
+ .list('', { limit: 1 });
53
63
  if (error) {
54
- throw new Error(`Error connecting to Supabase: ${error.message}`);
64
+ console.error(`āŒ Bucket access test failed:`, error.message);
65
+ throw new Error(
66
+ `Error connecting to Supabase bucket '${this.bucket}': ${error.message}`,
67
+ );
55
68
  }
69
+
70
+ console.log(`āœ… Successfully connected to Supabase bucket: ${this.bucket}`);
56
71
  }
57
72
 
58
73
  /**
@@ -80,18 +95,25 @@ export class SupabaseUploadService extends BaseUploadService {
80
95
  const content = fs.readFileSync(file.path);
81
96
  const contentType = mime.lookup(file.path) || 'application/octet-stream';
82
97
 
98
+ const normalizedPath = uploadPath.replace(/\\/g, '/');
99
+
83
100
  const { data, error } = await this.client.storage
84
101
  .from(this.bucket)
85
- .upload(uploadPath.replace(/\\/g, '/'), content, {
102
+ .upload(normalizedPath, content, {
86
103
  upsert: true,
87
104
  contentType,
88
105
  });
89
106
 
90
107
  if (error) {
108
+ console.error(
109
+ `āŒ Supabase upload failed for ${normalizedPath}:`,
110
+ error.message,
111
+ );
91
112
  return { success: false, error: error.message };
92
113
  }
93
114
 
94
- return { success: true, data };
115
+ console.log(`āœ… Supabase upload successful: ${normalizedPath}`);
116
+ return { success: true, data, uploadPath: normalizedPath };
95
117
  }
96
118
 
97
119
  /**
@@ -0,0 +1,262 @@
1
+ import logger from '../services/LoggingService.js';
2
+
3
+ /**
4
+ * Cleanup Manager
5
+ * Centralizes and coordinates cleanup of resources during graceful shutdown
6
+ * Ensures resources are cleaned up in reverse order of registration (LIFO)
7
+ */
8
+ export class CleanupManager {
9
+ constructor() {
10
+ /**
11
+ * Array of registered resources
12
+ * @type {Array<{name: string, resource: any, cleanupFn: Function}>}
13
+ */
14
+ this.resources = [];
15
+
16
+ /**
17
+ * Flag to prevent concurrent cleanup
18
+ * @type {boolean}
19
+ */
20
+ this.cleanupInProgress = false;
21
+
22
+ /**
23
+ * Timestamp when cleanup started
24
+ * @type {number|null}
25
+ */
26
+ this.cleanupStartTime = null;
27
+
28
+ /**
29
+ * Cleanup timeout duration (30 seconds)
30
+ * @type {number}
31
+ */
32
+ this.cleanupTimeout = 30000;
33
+
34
+ /**
35
+ * Results of cleanup operations
36
+ * @type {Array<{name: string, success: boolean, duration: number, error: string|null}>}
37
+ */
38
+ this.cleanupResults = [];
39
+ }
40
+
41
+ /**
42
+ * Register a resource to be cleaned up during shutdown
43
+ * Resources are cleaned up in reverse order (LIFO)
44
+ * @param {string} name - Name of the resource
45
+ * @param {any} resource - The resource object
46
+ * @param {Function} cleanupFn - Async function to clean up the resource
47
+ * @returns {void}
48
+ */
49
+ registerResource(name, resource, cleanupFn) {
50
+ if (!name || typeof name !== 'string') {
51
+ logger.warn('CleanupManager: Resource name must be a non-empty string');
52
+ return;
53
+ }
54
+
55
+ if (typeof cleanupFn !== 'function') {
56
+ logger.warn(
57
+ `CleanupManager: Cleanup function for "${name}" is not a function`,
58
+ );
59
+ return;
60
+ }
61
+
62
+ this.resources.push({
63
+ name,
64
+ resource,
65
+ cleanupFn,
66
+ });
67
+
68
+ logger.debug(`CleanupManager: Registered resource "${name}"`);
69
+ }
70
+
71
+ /**
72
+ * Perform cleanup of all registered resources in reverse order
73
+ * @param {string} reason - Reason for cleanup (e.g., 'SIGINT', 'SIGTERM')
74
+ * @returns {Promise<Object>} Cleanup results
75
+ */
76
+ async performCleanup(reason = 'unknown') {
77
+ // Prevent concurrent cleanup operations
78
+ if (this.cleanupInProgress) {
79
+ logger.warn(
80
+ `CleanupManager: Cleanup already in progress, skipping new cleanup request`,
81
+ );
82
+ return {
83
+ success: false,
84
+ reason: 'Cleanup already in progress',
85
+ results: this.cleanupResults,
86
+ };
87
+ }
88
+
89
+ this.cleanupInProgress = true;
90
+ this.cleanupStartTime = Date.now();
91
+ this.cleanupResults = [];
92
+
93
+ logger.info(
94
+ `CleanupManager: Starting cleanup sequence (reason: ${reason})`,
95
+ );
96
+
97
+ // Set timeout for cleanup
98
+ const cleanupTimeoutId = setTimeout(() => {
99
+ logger.error('CleanupManager: Cleanup timeout reached, forcing exit');
100
+ }, this.cleanupTimeout);
101
+
102
+ // Clean up resources in reverse order (LIFO)
103
+ for (let i = this.resources.length - 1; i >= 0; i--) {
104
+ const { name, cleanupFn } = this.resources[i];
105
+ const startTime = Date.now();
106
+
107
+ try {
108
+ logger.debug(`CleanupManager: Cleaning up resource "${name}"...`);
109
+
110
+ // Execute cleanup function
111
+ const result = cleanupFn();
112
+
113
+ // Handle both sync and async cleanup functions
114
+ if (result && typeof result.catch === 'function') {
115
+ await result;
116
+ }
117
+
118
+ const duration = Date.now() - startTime;
119
+ this.cleanupResults.push({
120
+ name,
121
+ success: true,
122
+ duration,
123
+ error: null,
124
+ });
125
+
126
+ logger.info(
127
+ `CleanupManager: Successfully cleaned up "${name}" (${duration}ms)`,
128
+ );
129
+ } catch (error) {
130
+ const duration = Date.now() - startTime;
131
+ this.cleanupResults.push({
132
+ name,
133
+ success: false,
134
+ duration,
135
+ error: error.message,
136
+ });
137
+
138
+ logger.error(
139
+ `CleanupManager: Error cleaning up "${name}": ${error.message}`,
140
+ );
141
+ }
142
+ }
143
+
144
+ // Clear timeout
145
+ clearTimeout(cleanupTimeoutId);
146
+
147
+ const totalDuration = Date.now() - this.cleanupStartTime;
148
+ const successCount = this.cleanupResults.filter((r) => r.success).length;
149
+ const failureCount = this.cleanupResults.filter((r) => !r.success).length;
150
+
151
+ logger.info(
152
+ `CleanupManager: Cleanup complete (${totalDuration}ms, ${successCount} successful, ${failureCount} failed)`,
153
+ );
154
+
155
+ return {
156
+ success: failureCount === 0,
157
+ reason,
158
+ totalDuration,
159
+ successCount,
160
+ failureCount,
161
+ results: this.cleanupResults,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Clear all registered resources without cleaning them up
167
+ * Use this for resetting the cleanup manager
168
+ * @returns {void}
169
+ */
170
+ clearResources() {
171
+ const count = this.resources.length;
172
+ this.resources = [];
173
+ logger.debug(`CleanupManager: Cleared ${count} registered resources`);
174
+ }
175
+
176
+ /**
177
+ * Get the number of registered resources
178
+ * @returns {number} Number of registered resources
179
+ */
180
+ getResourceCount() {
181
+ return this.resources.length;
182
+ }
183
+
184
+ /**
185
+ * Get details of all registered resources
186
+ * @returns {Array<string>} Array of resource names
187
+ */
188
+ getResourceNames() {
189
+ return this.resources.map((r) => r.name);
190
+ }
191
+
192
+ /**
193
+ * Check if cleanup is in progress
194
+ * @returns {boolean} True if cleanup is currently running
195
+ */
196
+ isCleanupInProgress() {
197
+ return this.cleanupInProgress;
198
+ }
199
+
200
+ /**
201
+ * Get cleanup results from last operation
202
+ * @returns {Array<Object>} Array of cleanup results
203
+ */
204
+ getCleanupResults() {
205
+ return [...this.cleanupResults];
206
+ }
207
+
208
+ /**
209
+ * Generate a formatted report of the cleanup operation
210
+ * @returns {string} Formatted cleanup report
211
+ */
212
+ generateCleanupReport() {
213
+ if (this.cleanupResults.length === 0) {
214
+ return 'No cleanup operations recorded';
215
+ }
216
+
217
+ const totalDuration = this.cleanupStartTime
218
+ ? Date.now() - this.cleanupStartTime
219
+ : 0;
220
+
221
+ let report =
222
+ '\n═══════════════════════════════════════════════════════════\n';
223
+ report += 'CLEANUP OPERATION REPORT\n';
224
+ report += '═══════════════════════════════════════════════════════════\n\n';
225
+
226
+ for (const result of this.cleanupResults) {
227
+ const status = result.success ? 'āœ…' : 'āŒ';
228
+ report += `${status} ${result.name}\n`;
229
+ report += ` Duration: ${result.duration}ms\n`;
230
+ if (result.error) {
231
+ report += ` Error: ${result.error}\n`;
232
+ }
233
+ report += '\n';
234
+ }
235
+
236
+ const successCount = this.cleanupResults.filter((r) => r.success).length;
237
+ const failureCount = this.cleanupResults.filter((r) => !r.success).length;
238
+
239
+ report += '─────────────────────────────────────────────────────────────\n';
240
+ report += `Total Duration: ${totalDuration}ms\n`;
241
+ report += `Successful: ${successCount}\n`;
242
+ report += `Failed: ${failureCount}\n`;
243
+ report += '═══════════════════════════════════════════════════════════\n';
244
+
245
+ return report;
246
+ }
247
+
248
+ /**
249
+ * Reset cleanup state (useful for testing)
250
+ * @returns {void}
251
+ */
252
+ reset() {
253
+ this.cleanupInProgress = false;
254
+ this.cleanupStartTime = null;
255
+ this.cleanupResults = [];
256
+ logger.debug('CleanupManager: Reset to initial state');
257
+ }
258
+ }
259
+
260
+ // Export singleton instance
261
+ export const cleanupManager = new CleanupManager();
262
+ export default cleanupManager;
@@ -143,6 +143,47 @@ export class FileOperations {
143
143
  return false;
144
144
  }
145
145
  }
146
+
147
+ /**
148
+ * List all files in a directory (non-recursive)
149
+ * @param {string} dirPath - Directory path
150
+ * @param {Object} options - Options { excludePattern, onlyPdf }
151
+ * @returns {Array} Array of file paths
152
+ */
153
+ static listFilesInDirectory(dirPath, options = {}) {
154
+ try {
155
+ const { excludePattern = /(^|[\/\\])\.|node_modules|\.git/, onlyPdf = false } = options;
156
+
157
+ if (!fs.existsSync(dirPath)) {
158
+ return [];
159
+ }
160
+
161
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
162
+ const files = [];
163
+
164
+ for (const entry of entries) {
165
+ const fullPath = path.join(dirPath, entry.name);
166
+
167
+ // Skip excluded patterns
168
+ if (excludePattern && excludePattern.test(fullPath)) {
169
+ continue;
170
+ }
171
+
172
+ // Only include files, not directories
173
+ if (entry.isFile()) {
174
+ // If onlyPdf is true, filter for PDF files
175
+ if (onlyPdf && !entry.name.toLowerCase().endsWith('.pdf')) {
176
+ continue;
177
+ }
178
+ files.push(fullPath);
179
+ }
180
+ }
181
+
182
+ return files;
183
+ } catch (error) {
184
+ return [];
185
+ }
186
+ }
146
187
  }
147
188
 
148
189
  export default FileOperations;