@arela/uploader 1.1.3 → 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.
- package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
- package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
- package/.claude/worktrees/agent-multi-profile/README.md +405 -0
- package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
- package/.claude/worktrees/agent-multi-profile/package.json +78 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
- package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
- package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
- package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
- package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
- package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
- package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
- package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
- package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
- package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/commands/AgentCommand.js +210 -0
- package/src/commands/AgentInitCommand.js +316 -0
- package/src/commands/PollWorkerCommand.js +11 -322
- package/src/config/config.js +44 -6
- package/src/document-type-shared.js +1 -1
- package/src/document-types/pedimento-completo.js +8 -1
- package/src/file-detection.js +9 -0
- package/src/index.js +55 -0
- package/src/scoring/scoring-engine.js +1 -1
- package/src/services/LoggingService.js +35 -0
- package/src/services/PipelineApiService.js +7 -1
- package/src/services/PipelineJobRunner.js +389 -0
- 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;
|