@arela/uploader 1.1.4 → 1.2.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 (95) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/index.js +55 -0
  92. package/src/services/LoggingService.js +35 -0
  93. package/src/services/PipelineApiService.js +7 -1
  94. package/src/services/PipelineJobRunner.js +389 -0
  95. package/src/services/ProfileManager.js +164 -0
@@ -0,0 +1,198 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Path Detection Utility
5
+ * Extracts year and pedimento information from file paths with caching
6
+ */
7
+ export class PathDetector {
8
+ constructor() {
9
+ this.cache = new Map();
10
+ }
11
+
12
+ /**
13
+ * Extract year and pedimento number from file path with caching
14
+ * @param {string} filePath - Full file path
15
+ * @param {string} basePath - Base path to make relative
16
+ * @returns {Object} Detection result with year, pedimento, and detected flag
17
+ */
18
+ extractYearAndPedimentoFromPath(filePath, basePath) {
19
+ const cacheKey = `${filePath}|${basePath}`;
20
+
21
+ if (this.cache.has(cacheKey)) {
22
+ return this.cache.get(cacheKey);
23
+ }
24
+
25
+ const detection = this.#performDetection(filePath, basePath);
26
+ this.cache.set(cacheKey, detection);
27
+
28
+ return detection;
29
+ }
30
+
31
+ /**
32
+ * Perform the actual path detection
33
+ * @private
34
+ * @param {string} filePath - Full file path
35
+ * @param {string} basePath - Base path to make relative
36
+ * @returns {Object} Detection result
37
+ */
38
+ #performDetection(filePath, basePath) {
39
+ try {
40
+ const relativePath = path.relative(basePath, filePath);
41
+ const pathParts = relativePath.split(path.sep);
42
+
43
+ let year = null;
44
+ let pedimento = null;
45
+
46
+ // Pattern 1: Direct year/pedimento structure (2024/4023260)
47
+ year = this.#detectDirectPattern(pathParts);
48
+ pedimento = year ? this.#findPedimentoForYear(pathParts, year) : null;
49
+
50
+ // Pattern 2: Named patterns (año2024, ped4023260)
51
+ if (!year || !pedimento) {
52
+ const namedDetection = this.#detectNamedPatterns(pathParts);
53
+ year = year || namedDetection.year;
54
+ pedimento = pedimento || namedDetection.pedimento;
55
+ }
56
+
57
+ // Pattern 3: Loose year detection
58
+ if (!year) {
59
+ year = this.#detectLooseYear(pathParts);
60
+ }
61
+
62
+ // Pattern 4: Loose pedimento detection
63
+ if (!pedimento) {
64
+ pedimento = this.#detectLoosePedimento(pathParts);
65
+ }
66
+
67
+ return {
68
+ year,
69
+ pedimento,
70
+ detected: !!(year && pedimento),
71
+ };
72
+ } catch (error) {
73
+ return {
74
+ year: null,
75
+ pedimento: null,
76
+ detected: false,
77
+ error: error.message,
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Detect direct year/pedimento pattern
84
+ * @private
85
+ * @param {string[]} pathParts - Path components
86
+ * @returns {string|null} Detected year
87
+ */
88
+ #detectDirectPattern(pathParts) {
89
+ for (let i = 0; i < pathParts.length - 1; i++) {
90
+ const yearMatch = pathParts[i].match(/^(202[0-9])$/);
91
+ if (yearMatch) {
92
+ return yearMatch[1];
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Find pedimento for a detected year
100
+ * @private
101
+ * @param {string[]} pathParts - Path components
102
+ * @param {string} year - Detected year
103
+ * @returns {string|null} Detected pedimento
104
+ */
105
+ #findPedimentoForYear(pathParts, year) {
106
+ const yearIndex = pathParts.findIndex((part) => part === year);
107
+ if (yearIndex >= 0 && yearIndex < pathParts.length - 1) {
108
+ const nextPart = pathParts[yearIndex + 1];
109
+ const pedimentoMatch = nextPart.match(/^(\d{4,8})$/);
110
+ if (pedimentoMatch) {
111
+ return pedimentoMatch[1];
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Detect named patterns (año2024, ped4023260)
119
+ * @private
120
+ * @param {string[]} pathParts - Path components
121
+ * @returns {Object} Detection result with year and pedimento
122
+ */
123
+ #detectNamedPatterns(pathParts) {
124
+ let year = null;
125
+ let pedimento = null;
126
+
127
+ for (const part of pathParts) {
128
+ if (!year) {
129
+ const namedYearMatch = part.match(/(?:año|year|anio)(\d{4})/i);
130
+ if (namedYearMatch) {
131
+ year = namedYearMatch[1];
132
+ }
133
+ }
134
+
135
+ if (!pedimento) {
136
+ const namedPedimentoMatch = part.match(
137
+ /(?:ped|pedimento|pedi)(\d{4,8})/i,
138
+ );
139
+ if (namedPedimentoMatch) {
140
+ pedimento = namedPedimentoMatch[1];
141
+ }
142
+ }
143
+ }
144
+
145
+ return { year, pedimento };
146
+ }
147
+
148
+ /**
149
+ * Detect loose year pattern
150
+ * @private
151
+ * @param {string[]} pathParts - Path components
152
+ * @returns {string|null} Detected year
153
+ */
154
+ #detectLooseYear(pathParts) {
155
+ for (const part of pathParts) {
156
+ const yearMatch = part.match(/(202[0-9])/);
157
+ if (yearMatch) {
158
+ return yearMatch[1];
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Detect loose pedimento pattern
166
+ * @private
167
+ * @param {string[]} pathParts - Path components
168
+ * @returns {string|null} Detected pedimento
169
+ */
170
+ #detectLoosePedimento(pathParts) {
171
+ for (const part of pathParts) {
172
+ const pedimentoMatch = part.match(/(\d{4,8})/);
173
+ if (pedimentoMatch && pedimentoMatch[1].length >= 4) {
174
+ return pedimentoMatch[1];
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Clear the detection cache
182
+ */
183
+ clearCache() {
184
+ this.cache.clear();
185
+ }
186
+
187
+ /**
188
+ * Get cache size for monitoring
189
+ * @returns {number} Number of cached entries
190
+ */
191
+ getCacheSize() {
192
+ return this.cache.size;
193
+ }
194
+ }
195
+
196
+ // Export singleton instance
197
+ export const pathDetector = new PathDetector();
198
+ export default pathDetector;
@@ -0,0 +1,274 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ /**
6
+ * PathNormalizer - Absolute path resolution and table name generation
7
+ *
8
+ * Strategy:
9
+ * 1. Always resolve paths to absolute paths first (resolves ../sample vs ./sample)
10
+ * 2. Store absolute paths in base_path_full
11
+ * 3. Normalize ONLY for table name generation (sanitize special chars)
12
+ * 4. Keep full path structure in table names (hash if too long)
13
+ *
14
+ * Cross-platform support:
15
+ * - Recognizes Windows paths (C:\, D:\, etc.) even when running on macOS/Linux
16
+ * - Preserves Windows paths as-is when detected (for remote server scenarios)
17
+ * - This allows CLI running on macOS to work with paths stored from Windows servers
18
+ */
19
+ export class PathNormalizer {
20
+ /**
21
+ * Check if a path is a Windows-style absolute path (has drive letter)
22
+ * This works on any OS, not just Windows
23
+ *
24
+ * Examples:
25
+ * - C:\Users\Documents -> true
26
+ * - O:\expediente\archivos -> true
27
+ * - /home/user/docs -> false
28
+ * - ./relative/path -> false
29
+ *
30
+ * @param {string} inputPath - Path to check
31
+ * @returns {boolean} True if path has a Windows drive letter
32
+ */
33
+ static isWindowsAbsolutePath(inputPath) {
34
+ if (!inputPath || typeof inputPath !== 'string') return false;
35
+ // Match drive letter followed by colon and backslash or forward slash
36
+ // e.g., C:\, D:/, O:\
37
+ return /^[A-Za-z]:[/\\]/.test(inputPath);
38
+ }
39
+
40
+ /**
41
+ * Check if a path is absolute (works cross-platform)
42
+ * Recognizes both Unix and Windows absolute paths regardless of current OS
43
+ *
44
+ * @param {string} inputPath - Path to check
45
+ * @returns {boolean} True if path is absolute (Unix or Windows style)
46
+ */
47
+ static isAbsolutePath(inputPath) {
48
+ if (!inputPath) return false;
49
+ // Check native absolute path first, then Windows-style
50
+ return path.isAbsolute(inputPath) || this.isWindowsAbsolutePath(inputPath);
51
+ }
52
+
53
+ /**
54
+ * Resolve a path to an absolute path
55
+ * This ensures ../sample and ./sample are treated as different paths
56
+ *
57
+ * Cross-platform behavior:
58
+ * - Windows paths (C:\, O:\, etc.) are preserved as-is, even on macOS/Linux
59
+ * - Unix paths are normalized using native path.resolve
60
+ * - Relative paths are resolved against basePath or cwd
61
+ *
62
+ * Examples:
63
+ * - ../sample (from /data/project) -> /data/sample
64
+ * - ./sample (from /data/project) -> /data/project/sample
65
+ * - C:\Users\Documents -> C:\Users\Documents (preserved on any OS)
66
+ * - O:\expediente\archivos -> O:\expediente\archivos (preserved on any OS)
67
+ * - /home/user/docs -> /home/user/docs (on Unix)
68
+ *
69
+ * @param {string} inputPath - Path to resolve
70
+ * @param {string} basePath - Base path for resolving relative paths (defaults to cwd)
71
+ * @returns {string} Absolute path (native format for Unix, preserved for Windows)
72
+ */
73
+ static toAbsolutePath(inputPath, basePath = null) {
74
+ if (!inputPath) return '';
75
+
76
+ // Check for Windows absolute path first (works on any OS)
77
+ // This prevents macOS from treating "O:\path" as relative
78
+ if (this.isWindowsAbsolutePath(inputPath)) {
79
+ // Normalize Windows path but preserve the format
80
+ // Replace forward slashes with backslashes for consistency
81
+ return inputPath.replace(/\//g, '\\');
82
+ }
83
+
84
+ // If native absolute path, return normalized
85
+ if (path.isAbsolute(inputPath)) {
86
+ return path.normalize(inputPath);
87
+ }
88
+
89
+ // Resolve relative path against base
90
+ const base = basePath || process.cwd();
91
+ return path.resolve(base, inputPath);
92
+ }
93
+
94
+ /**
95
+ * Normalize path separators to forward slashes for consistency
96
+ * Does NOT remove drive letters or convert to POSIX - just normalizes separators
97
+ *
98
+ * @param {string} inputPath - Path to normalize
99
+ * @returns {string} Path with forward slashes
100
+ */
101
+ static normalizeSeparators(inputPath) {
102
+ if (!inputPath) return '';
103
+ return inputPath.replace(/\\/g, '/');
104
+ }
105
+
106
+ /**
107
+ * Get relative path from base with normalized separators
108
+ *
109
+ * @param {string} fullPath - Full absolute path
110
+ * @param {string} basePath - Base absolute path to subtract
111
+ * @returns {string} Relative path with forward slashes
112
+ */
113
+ static getRelativePath(fullPath, basePath) {
114
+ const relativePath = path.relative(basePath, fullPath);
115
+ return this.normalizeSeparators(relativePath);
116
+ }
117
+
118
+ /**
119
+ * Sanitize an absolute path for use in database table names
120
+ * Converts path separators and special characters to underscores
121
+ * Preserves the path structure for uniqueness
122
+ *
123
+ * Examples:
124
+ * - /data/2023 -> data_2023
125
+ * - C:\Users\Documents -> c_users_documents
126
+ * - /home/user/project/sample -> home_user_project_sample
127
+ *
128
+ * @param {string} absolutePath - Absolute path to sanitize
129
+ * @returns {string} Sanitized string safe for table names
130
+ */
131
+ static sanitizeForTableName(absolutePath) {
132
+ if (!absolutePath) return '';
133
+
134
+ return absolutePath
135
+ .toLowerCase()
136
+ .replace(/^[a-z]:/i, '') // Remove Windows drive letter (C:, D:, etc.)
137
+ .replace(/[^a-z0-9]/g, '_') // Replace all non-alphanumeric with underscore
138
+ .replace(/_+/g, '_') // Replace multiple underscores with single
139
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
140
+ }
141
+
142
+ /**
143
+ * Generate a PostgreSQL-compatible table name from scan configuration
144
+ * Format: scan_<company>_<server>_<sanitized_absolute_path>
145
+ *
146
+ * Uses the absolute path to ensure uniqueness:
147
+ * - ../sample -> /data/sample
148
+ * - ./sample -> /data/project/sample
149
+ * These create different table names!
150
+ *
151
+ * Handles the 63-character PostgreSQL limit by:
152
+ * 1. Trying the full name first
153
+ * 2. If too long, truncating and adding an 8-character hash for uniqueness
154
+ *
155
+ * @param {Object} config - Scan configuration
156
+ * @param {string} config.companySlug - Company identifier
157
+ * @param {string} config.serverId - Server identifier
158
+ * @param {string} config.basePathLabel - Absolute base path
159
+ * @returns {string} PostgreSQL-compatible table name
160
+ */
161
+ static generateTableName(config) {
162
+ const { companySlug, serverId, basePathLabel } = config;
163
+
164
+ // Sanitize each component (basePathLabel should already be absolute)
165
+ const sanitizedCompany = companySlug
166
+ .toLowerCase()
167
+ .replace(/[^a-z0-9]/g, '_');
168
+ const sanitizedServer = serverId.toLowerCase().replace(/[^a-z0-9]/g, '_');
169
+ const sanitizedPath = this.sanitizeForTableName(basePathLabel);
170
+
171
+ // Build raw name for hashing (preserve original for uniqueness)
172
+ const rawName = `${companySlug}_${serverId}_${basePathLabel}`;
173
+
174
+ // Build sanitized table name
175
+ let tableName = `scan_${sanitizedCompany}_${sanitizedServer}`;
176
+
177
+ if (sanitizedPath) {
178
+ tableName += `_${sanitizedPath}`;
179
+ }
180
+
181
+ // PostgreSQL table name limit is 63 characters
182
+ if (tableName.length > 63) {
183
+ // Generate a hash for uniqueness
184
+ const hash = crypto
185
+ .createHash('md5')
186
+ .update(rawName)
187
+ .digest('hex')
188
+ .substring(0, 8);
189
+
190
+ const basePrefix = 'scan_';
191
+ const prefix = `${basePrefix}${sanitizedCompany}_${sanitizedServer}_`;
192
+
193
+ // Check if even prefix + hash exceeds limit
194
+ if (prefix.length + hash.length > 63) {
195
+ // Company/server names are too long, need to truncate them too
196
+ const maxCompanyServerLength = 63 - basePrefix.length - hash.length - 2; // -2 for underscores
197
+ const halfLength = Math.floor(maxCompanyServerLength / 2);
198
+ const companyLength = halfLength;
199
+ const serverLength = maxCompanyServerLength - companyLength;
200
+
201
+ const truncatedCompany = sanitizedCompany.substring(0, companyLength);
202
+ const truncatedServer = sanitizedServer.substring(0, serverLength);
203
+
204
+ tableName = `${basePrefix}${truncatedCompany}_${truncatedServer}_${hash}`;
205
+ } else {
206
+ // Preserve start and end of path, put hash in middle
207
+ const availableSpace = 63 - prefix.length - hash.length - 2; // -2 for underscores around hash
208
+
209
+ if (availableSpace <= 0 || !sanitizedPath) {
210
+ // If no space for path or path is empty, just use hash
211
+ tableName = `${prefix}${hash}`;
212
+ } else if (sanitizedPath.length <= availableSpace) {
213
+ // Path fits in available space, use it all
214
+ tableName = `${prefix}${sanitizedPath}_${hash}`;
215
+ } else {
216
+ // Need to split: preserve start and end
217
+ const halfSpace = Math.floor(availableSpace / 2);
218
+ const startLength = halfSpace;
219
+ const endLength = availableSpace - startLength;
220
+
221
+ // Extract start and end portions of the sanitized path
222
+ const pathStart = sanitizedPath.substring(0, startLength);
223
+ const pathEnd = sanitizedPath.substring(
224
+ sanitizedPath.length - endLength,
225
+ );
226
+
227
+ // Build table name with start, hash, and end
228
+ tableName = `${prefix}${pathStart}_${hash}_${pathEnd}`;
229
+ }
230
+ }
231
+ }
232
+
233
+ return tableName;
234
+ }
235
+
236
+ /**
237
+ * Build an absolute path from base and relative component
238
+ *
239
+ * @param {string} basePath - Absolute base path
240
+ * @param {string} relativePath - Relative path from base (can be empty)
241
+ * @returns {string} Absolute path
242
+ */
243
+ static buildAbsolutePath(basePath, relativePath = '') {
244
+ if (!relativePath || relativePath === '.') {
245
+ return basePath;
246
+ }
247
+ return path.resolve(basePath, relativePath);
248
+ }
249
+
250
+ /**
251
+ * Validate that a path can be used for table name generation
252
+ *
253
+ * @param {string} pathStr - Path to validate
254
+ * @returns {Object} Validation result { valid: boolean, error?: string }
255
+ */
256
+ static validatePath(pathStr) {
257
+ if (!pathStr || typeof pathStr !== 'string') {
258
+ return { valid: false, error: 'Path is required and must be a string' };
259
+ }
260
+
261
+ if (pathStr.trim() === '') {
262
+ return { valid: false, error: 'Path cannot be empty' };
263
+ }
264
+
265
+ // Check for null bytes (security)
266
+ if (pathStr.includes('\0')) {
267
+ return { valid: false, error: 'Path cannot contain null bytes' };
268
+ }
269
+
270
+ return { valid: true };
271
+ }
272
+ }
273
+
274
+ export default PathNormalizer;