@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.
- 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/index.js +55 -0
- 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,676 @@
|
|
|
1
|
+
import { Blob } from 'buffer';
|
|
2
|
+
import { FormData } from 'formdata-node';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { Agent } from 'http';
|
|
5
|
+
import { Agent as HttpsAgent } from 'https';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
import appConfig from '../../config/config.js';
|
|
10
|
+
import logger from '../LoggingService.js';
|
|
11
|
+
import { BaseUploadService } from './BaseUploadService.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* API Upload Service
|
|
15
|
+
* Handles uploads to the Arela API with automatic processing
|
|
16
|
+
* Supports multiple API targets (agencia, cliente, default)
|
|
17
|
+
*/
|
|
18
|
+
export class ApiUploadService extends BaseUploadService {
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
// Flag to indicate if config is externally set (cross-tenant mode)
|
|
22
|
+
this._isExternalConfig = false;
|
|
23
|
+
// Get initial API config (can be overridden at runtime via setApiTarget)
|
|
24
|
+
this.#updateApiConfig();
|
|
25
|
+
|
|
26
|
+
// Get API connection settings from config/environment
|
|
27
|
+
const maxApiConnections = parseInt(process.env.MAX_API_CONNECTIONS) || 10;
|
|
28
|
+
const connectionTimeout =
|
|
29
|
+
parseInt(process.env.API_CONNECTION_TIMEOUT) || 60000;
|
|
30
|
+
|
|
31
|
+
// Initialize HTTP agents optimized for multiple API replicas
|
|
32
|
+
this.httpAgent = new Agent({
|
|
33
|
+
keepAlive: true,
|
|
34
|
+
keepAliveMsecs: 30000,
|
|
35
|
+
maxSockets: maxApiConnections, // Match your API replica count
|
|
36
|
+
maxFreeSockets: Math.ceil(maxApiConnections / 2),
|
|
37
|
+
maxTotalSockets: maxApiConnections + 5, // Buffer for peak usage
|
|
38
|
+
timeout: connectionTimeout,
|
|
39
|
+
scheduling: 'fifo', // First-in-first-out scheduling
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.httpsAgent = new HttpsAgent({
|
|
43
|
+
keepAlive: true,
|
|
44
|
+
keepAliveMsecs: 30000,
|
|
45
|
+
maxSockets: maxApiConnections, // Match your API replica count
|
|
46
|
+
maxFreeSockets: Math.ceil(maxApiConnections / 2),
|
|
47
|
+
maxTotalSockets: maxApiConnections + 5, // Buffer for peak usage
|
|
48
|
+
timeout: connectionTimeout,
|
|
49
|
+
scheduling: 'fifo', // First-in-first-out scheduling
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
`🔗 HTTP Agent configured for ${maxApiConnections} concurrent API connections`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update API configuration from appConfig
|
|
59
|
+
* Called on initialization and when API target changes
|
|
60
|
+
* Skip if externally configured (cross-tenant mode)
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
#updateApiConfig() {
|
|
64
|
+
// Skip update if config is externally set
|
|
65
|
+
if (this._isExternalConfig) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const apiConfig = appConfig.getApiConfig();
|
|
69
|
+
this.baseUrl = apiConfig.baseUrl;
|
|
70
|
+
this.token = apiConfig.token;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set external configuration (for cross-tenant mode)
|
|
75
|
+
* Prevents automatic config refresh from overwriting these values
|
|
76
|
+
* @param {string} baseUrl - API base URL
|
|
77
|
+
* @param {string} token - API token
|
|
78
|
+
*/
|
|
79
|
+
setExternalConfig(baseUrl, token) {
|
|
80
|
+
this._isExternalConfig = true;
|
|
81
|
+
this.baseUrl = baseUrl;
|
|
82
|
+
this.token = token;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get current API configuration (refreshes from appConfig)
|
|
87
|
+
* This ensures we always use the latest target configuration
|
|
88
|
+
* @returns {Object} Current API config with baseUrl and token
|
|
89
|
+
*/
|
|
90
|
+
getApiConfig() {
|
|
91
|
+
this.#updateApiConfig();
|
|
92
|
+
return {
|
|
93
|
+
baseUrl: this.baseUrl,
|
|
94
|
+
token: this.token,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Upload files to Arela API with automatic detection and organization
|
|
100
|
+
* @param {Array} files - Array of file objects
|
|
101
|
+
* @param {Object} options - Upload options
|
|
102
|
+
* @returns {Promise<Object>} API response
|
|
103
|
+
*/
|
|
104
|
+
async upload(files, options) {
|
|
105
|
+
// Refresh config to get current API target
|
|
106
|
+
this.#updateApiConfig();
|
|
107
|
+
// Validate files parameter
|
|
108
|
+
if (!files || !Array.isArray(files)) {
|
|
109
|
+
logger.warn(`Invalid files parameter: ${typeof files}`);
|
|
110
|
+
throw new Error('Files must be an array');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const formData = new FormData();
|
|
114
|
+
|
|
115
|
+
// Filter out system files (macOS, Windows, etc.)
|
|
116
|
+
const systemFilePattern =
|
|
117
|
+
/^\.|__pycache__|\.pyc|\.swp|\.swo|Thumbs\.db|desktop\.ini|DS_Store|\$RECYCLE\.BIN|System Volume Information|~\$|\.tmp/i;
|
|
118
|
+
const filteredFiles = files.filter((file) => {
|
|
119
|
+
const fileName = file.name || path.basename(file.path);
|
|
120
|
+
if (systemFilePattern.test(fileName)) {
|
|
121
|
+
logger.warn(`Skipping system file from upload: ${fileName}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (filteredFiles.length === 0) {
|
|
128
|
+
throw new Error('No valid files to upload after filtering system files');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add files to form data asynchronously
|
|
132
|
+
for (const file of filteredFiles) {
|
|
133
|
+
try {
|
|
134
|
+
// Check file size for streaming vs buffer approach
|
|
135
|
+
let size = file.size;
|
|
136
|
+
if (size === undefined || size === null) {
|
|
137
|
+
const stats = await fs.promises.stat(file.path);
|
|
138
|
+
size = stats.size;
|
|
139
|
+
}
|
|
140
|
+
const fileSizeThreshold = 10 * 1024 * 1024; // 10MB threshold
|
|
141
|
+
|
|
142
|
+
if (size > fileSizeThreshold) {
|
|
143
|
+
// Use streaming for large files
|
|
144
|
+
const fileStream = fs.createReadStream(file.path);
|
|
145
|
+
formData.append('files', fileStream, {
|
|
146
|
+
filename: file.name,
|
|
147
|
+
contentType: file.contentType,
|
|
148
|
+
knownLength: size,
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
// Use buffer for smaller files
|
|
152
|
+
const fileBuffer = await fs.promises.readFile(file.path);
|
|
153
|
+
const blob = new Blob([fileBuffer], { type: file.contentType });
|
|
154
|
+
formData.append('files', blob, file.name);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(`Failed to read file ${file.path}: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add configuration parameters
|
|
162
|
+
if (appConfig.supabase.bucket) {
|
|
163
|
+
formData.append('bucket', appConfig.supabase.bucket);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (options.prefix) {
|
|
167
|
+
formData.append('prefix', options.prefix);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle folder structure configuration
|
|
171
|
+
const folderStructure = this.#buildFolderStructure(files, options);
|
|
172
|
+
if (folderStructure) {
|
|
173
|
+
formData.append('folderStructure', folderStructure);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Add client path if specified
|
|
177
|
+
if (options.clientPath) {
|
|
178
|
+
formData.append('clientPath', options.clientPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add RFC for multi-database routing (required for cross-tenant uploads)
|
|
182
|
+
if (options.rfc) {
|
|
183
|
+
formData.append('rfc', options.rfc);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Add processing options
|
|
187
|
+
formData.append('autoDetect', String(options.autoDetect ?? true));
|
|
188
|
+
formData.append('autoOrganize', String(options.autoOrganize ?? false));
|
|
189
|
+
formData.append('batchSize', String(options.batchSize || 10));
|
|
190
|
+
formData.append('clientVersion', appConfig.packageVersion);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
194
|
+
const response = await fetch(
|
|
195
|
+
`${this.baseUrl}/api/storage/batch-upload-and-process`,
|
|
196
|
+
{
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'x-api-key': this.token,
|
|
200
|
+
},
|
|
201
|
+
body: formData,
|
|
202
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
const errorText = await response.text();
|
|
208
|
+
throw new Error(
|
|
209
|
+
`API request failed: ${response.status} ${response.statusText} - ${errorText}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = await response.json();
|
|
214
|
+
|
|
215
|
+
// Determine if the operation was successful
|
|
216
|
+
// Success means at least one file was uploaded successfully, even if some failed
|
|
217
|
+
const hasUploads = result.uploaded && result.uploaded.length > 0;
|
|
218
|
+
const hasErrors = result.errors && result.errors.length > 0;
|
|
219
|
+
// Consider it a success if there are uploads, or if there are no errors at all
|
|
220
|
+
const success =
|
|
221
|
+
hasUploads || (!hasErrors && result.stats?.totalFiles > 0);
|
|
222
|
+
|
|
223
|
+
// Normalize response format to match DatabaseService expectations
|
|
224
|
+
return { success, data: result };
|
|
225
|
+
} catch (fetchError) {
|
|
226
|
+
// Return normalized error format
|
|
227
|
+
return { success: false, error: fetchError.message };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if API service is available
|
|
233
|
+
* @returns {Promise<boolean>} True if available
|
|
234
|
+
*/
|
|
235
|
+
async isAvailable() {
|
|
236
|
+
// Refresh config to get current API target
|
|
237
|
+
this.#updateApiConfig();
|
|
238
|
+
|
|
239
|
+
if (!this.baseUrl || !this.token) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
245
|
+
const response = await fetch(`${this.baseUrl}/api/health`, {
|
|
246
|
+
headers: {
|
|
247
|
+
'x-api-key': this.token,
|
|
248
|
+
},
|
|
249
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return response.ok;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Batch upsert file stats to uploader table
|
|
260
|
+
* @param {Array} records - Array of record objects to upsert
|
|
261
|
+
* @returns {Promise<Object>} Upsert results { inserted, updated, total }
|
|
262
|
+
*/
|
|
263
|
+
async batchUpsertStats(records) {
|
|
264
|
+
if (!records || !Array.isArray(records) || records.length === 0) {
|
|
265
|
+
return { inserted: 0, updated: 0, total: 0 };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
270
|
+
const response = await fetch(
|
|
271
|
+
`${this.baseUrl}/api/uploader/batch-upsert`,
|
|
272
|
+
{
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: {
|
|
275
|
+
'x-api-key': this.token,
|
|
276
|
+
'Content-Type': 'application/json',
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify(records),
|
|
279
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
const errorText = await response.text();
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Batch upsert failed: ${response.status} ${response.statusText} - ${errorText}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const result = await response.json();
|
|
291
|
+
return result;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
logger.error(`Batch upsert API error: ${error.message}`);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Fetch PDF records for pedimento detection
|
|
300
|
+
* @param {Object} options - Query options
|
|
301
|
+
* @param {number} options.offset - Pagination offset
|
|
302
|
+
* @param {number} options.limit - Number of records to fetch
|
|
303
|
+
* @returns {Promise<Object>} { data: Array, error: Error|null }
|
|
304
|
+
*/
|
|
305
|
+
async fetchPdfRecordsForDetection(options = {}) {
|
|
306
|
+
const { offset = 0, limit = 100 } = options;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
310
|
+
const url = new URL(`${this.baseUrl}/api/uploader/pdf-records`);
|
|
311
|
+
url.searchParams.append('offset', offset);
|
|
312
|
+
url.searchParams.append('limit', limit);
|
|
313
|
+
url.searchParams.append('status', 'fs-stats');
|
|
314
|
+
url.searchParams.append('file_extension', 'pdf');
|
|
315
|
+
url.searchParams.append('is_like_simplificado', 'true');
|
|
316
|
+
|
|
317
|
+
const response = await fetch(url.toString(), {
|
|
318
|
+
method: 'GET',
|
|
319
|
+
headers: {
|
|
320
|
+
'x-api-key': this.token,
|
|
321
|
+
'Content-Type': 'application/json',
|
|
322
|
+
},
|
|
323
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
const errorText = await response.text();
|
|
328
|
+
return {
|
|
329
|
+
data: null,
|
|
330
|
+
error: new Error(
|
|
331
|
+
`Failed to fetch PDF records: ${response.status} ${response.statusText} - ${errorText}`,
|
|
332
|
+
),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const data = await response.json();
|
|
337
|
+
return { data, error: null };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger.error(`API fetch PDF records error: ${error.message}`);
|
|
340
|
+
return { data: null, error };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Batch update detection results
|
|
346
|
+
* @param {Array} updates - Array of update objects with { id, ...updateData }
|
|
347
|
+
* @returns {Promise<Object>} Update result { success: boolean, updated: number, errors: Array }
|
|
348
|
+
*/
|
|
349
|
+
async batchUpdateDetectionResults(updates) {
|
|
350
|
+
if (!updates || !Array.isArray(updates) || updates.length === 0) {
|
|
351
|
+
return { success: true, updated: 0, errors: [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
356
|
+
const response = await fetch(
|
|
357
|
+
`${this.baseUrl}/api/uploader/batch-update-detection`,
|
|
358
|
+
{
|
|
359
|
+
method: 'PATCH',
|
|
360
|
+
headers: {
|
|
361
|
+
'x-api-key': this.token,
|
|
362
|
+
'Content-Type': 'application/json',
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify({ updates }),
|
|
365
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const errorText = await response.text();
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Batch update failed: ${response.status} ${response.statusText} - ${errorText}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const result = await response.json();
|
|
377
|
+
return result;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger.error(`Batch update API error: ${error.message}`);
|
|
380
|
+
return {
|
|
381
|
+
success: false,
|
|
382
|
+
updated: 0,
|
|
383
|
+
errors: [{ message: error.message }],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Execute arela_path propagation on the backend
|
|
390
|
+
* This triggers a server-side process that propagates arela_path from pedimentos to related files
|
|
391
|
+
* @param {Object} options - Propagation options
|
|
392
|
+
* @param {Array} options.years - Optional year filter
|
|
393
|
+
* @returns {Promise<Object>} Propagation result { success: boolean, processedCount, updatedCount, errorCount }
|
|
394
|
+
*/
|
|
395
|
+
async propagateArelaPath(options = {}) {
|
|
396
|
+
const { years = [] } = options;
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
400
|
+
|
|
401
|
+
// Longer timeout for propagation (can take several minutes for large datasets)
|
|
402
|
+
const propagationTimeout = 5 * 60 * 1000; // 5 minutes
|
|
403
|
+
const controller = new AbortController();
|
|
404
|
+
const timeoutId = setTimeout(
|
|
405
|
+
() => controller.abort(),
|
|
406
|
+
propagationTimeout,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const response = await fetch(
|
|
410
|
+
`${this.baseUrl}/api/uploader/propagate-arela-path`,
|
|
411
|
+
{
|
|
412
|
+
method: 'POST',
|
|
413
|
+
headers: {
|
|
414
|
+
'x-api-key': this.token,
|
|
415
|
+
'Content-Type': 'application/json',
|
|
416
|
+
},
|
|
417
|
+
body: JSON.stringify({ years }),
|
|
418
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
419
|
+
signal: controller.signal,
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
clearTimeout(timeoutId);
|
|
424
|
+
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
const errorText = await response.text();
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Arela path propagation failed: ${response.status} ${response.statusText} - ${errorText}`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const result = await response.json();
|
|
433
|
+
return result;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
logger.error(`Propagate arela_path API error: ${error.message}`);
|
|
436
|
+
return {
|
|
437
|
+
success: false,
|
|
438
|
+
processedCount: 0,
|
|
439
|
+
updatedCount: 0,
|
|
440
|
+
errorCount: 1,
|
|
441
|
+
error: error.message,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Fetch RFC file count
|
|
448
|
+
* @param {Object} options - Query options
|
|
449
|
+
* @param {Array} options.rfcs - Array of RFC values to filter
|
|
450
|
+
* @returns {Promise<Object>} { count: number, error: Error|null }
|
|
451
|
+
*/
|
|
452
|
+
async fetchRfcFileCount(options = {}) {
|
|
453
|
+
const { rfcs = [] } = options;
|
|
454
|
+
|
|
455
|
+
if (!rfcs || rfcs.length === 0) {
|
|
456
|
+
return { count: 0, error: null };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
461
|
+
const url = new URL(`${this.baseUrl}/api/uploader/rfc-file-count`);
|
|
462
|
+
url.searchParams.append('rfcs', rfcs.join(','));
|
|
463
|
+
|
|
464
|
+
const response = await fetch(url.toString(), {
|
|
465
|
+
method: 'GET',
|
|
466
|
+
headers: {
|
|
467
|
+
'x-api-key': this.token,
|
|
468
|
+
'Content-Type': 'application/json',
|
|
469
|
+
},
|
|
470
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
const errorText = await response.text();
|
|
475
|
+
return {
|
|
476
|
+
count: 0,
|
|
477
|
+
error: new Error(
|
|
478
|
+
`Failed to fetch RFC file count: ${response.status} ${response.statusText} - ${errorText}`,
|
|
479
|
+
),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const data = await response.json();
|
|
484
|
+
return { count: data.count || 0, error: null };
|
|
485
|
+
} catch (error) {
|
|
486
|
+
logger.error(`API fetch RFC file count error: ${error.message}`);
|
|
487
|
+
return { count: 0, error };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Fetch pedimento records by RFC
|
|
493
|
+
* @param {Object} options - Query options
|
|
494
|
+
* @param {Array} options.rfcs - Array of RFC values to filter
|
|
495
|
+
* @param {Array} options.years - Optional year filter
|
|
496
|
+
* @param {number} options.offset - Pagination offset
|
|
497
|
+
* @param {number} options.limit - Number of records to fetch
|
|
498
|
+
* @returns {Promise<Object>} { data: Array, error: Error|null }
|
|
499
|
+
*/
|
|
500
|
+
async fetchPedimentosByRfc(options = {}) {
|
|
501
|
+
const { rfcs = [], years = [], offset = 0, limit = 500 } = options;
|
|
502
|
+
|
|
503
|
+
if (!rfcs || rfcs.length === 0) {
|
|
504
|
+
return { data: [], error: null };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
509
|
+
const url = new URL(`${this.baseUrl}/api/uploader/pedimentos-by-rfc`);
|
|
510
|
+
url.searchParams.append('rfcs', rfcs.join(','));
|
|
511
|
+
if (years && years.length > 0) {
|
|
512
|
+
url.searchParams.append('years', years.join(','));
|
|
513
|
+
}
|
|
514
|
+
url.searchParams.append('offset', offset);
|
|
515
|
+
url.searchParams.append('limit', limit);
|
|
516
|
+
|
|
517
|
+
const response = await fetch(url.toString(), {
|
|
518
|
+
method: 'GET',
|
|
519
|
+
headers: {
|
|
520
|
+
'x-api-key': this.token,
|
|
521
|
+
'Content-Type': 'application/json',
|
|
522
|
+
},
|
|
523
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
const errorText = await response.text();
|
|
528
|
+
return {
|
|
529
|
+
data: null,
|
|
530
|
+
error: new Error(
|
|
531
|
+
`Failed to fetch pedimentos by RFC: ${response.status} ${response.statusText} - ${errorText}`,
|
|
532
|
+
),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const data = await response.json();
|
|
537
|
+
return { data, error: null };
|
|
538
|
+
} catch (error) {
|
|
539
|
+
logger.error(`API fetch pedimentos by RFC error: ${error.message}`);
|
|
540
|
+
return { data: null, error };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Fetch files for upload by arela_path
|
|
546
|
+
* @param {Object} options - Query options
|
|
547
|
+
* @param {Array} options.arelaPaths - Array of arela_path values to filter
|
|
548
|
+
* @param {number} options.offset - Pagination offset
|
|
549
|
+
* @param {number} options.limit - Number of records to fetch
|
|
550
|
+
* @returns {Promise<Object>} { data: Array, error: Error|null }
|
|
551
|
+
*/
|
|
552
|
+
async fetchFilesForUpload(options = {}) {
|
|
553
|
+
const { arelaPaths = [], offset = 0, limit = 1000 } = options;
|
|
554
|
+
|
|
555
|
+
if (!arelaPaths || arelaPaths.length === 0) {
|
|
556
|
+
return { data: [], error: null };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
561
|
+
const url = new URL(`${this.baseUrl}/api/uploader/files-for-upload`);
|
|
562
|
+
url.searchParams.append('arela_paths', arelaPaths.join('|'));
|
|
563
|
+
url.searchParams.append('offset', offset);
|
|
564
|
+
url.searchParams.append('limit', limit);
|
|
565
|
+
|
|
566
|
+
const response = await fetch(url.toString(), {
|
|
567
|
+
method: 'GET',
|
|
568
|
+
headers: {
|
|
569
|
+
'x-api-key': this.token,
|
|
570
|
+
'Content-Type': 'application/json',
|
|
571
|
+
},
|
|
572
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
if (!response.ok) {
|
|
576
|
+
const errorText = await response.text();
|
|
577
|
+
return {
|
|
578
|
+
data: null,
|
|
579
|
+
error: new Error(
|
|
580
|
+
`Failed to fetch files for upload: ${response.status} ${response.statusText} - ${errorText}`,
|
|
581
|
+
),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const data = await response.json();
|
|
586
|
+
return { data, error: null };
|
|
587
|
+
} catch (error) {
|
|
588
|
+
logger.error(`API fetch files for upload error: ${error.message}`);
|
|
589
|
+
return { data: null, error };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Update file status after upload
|
|
595
|
+
* @param {Array} updates - Array of update objects with { id, status, message, processing_status }
|
|
596
|
+
* @returns {Promise<Object>} Update result { success: boolean, updated: number, errors: Array }
|
|
597
|
+
*/
|
|
598
|
+
async updateFileStatus(updates) {
|
|
599
|
+
if (!updates || !Array.isArray(updates) || updates.length === 0) {
|
|
600
|
+
return { success: true, updated: 0, errors: [] };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const isHttps = this.baseUrl.startsWith('https');
|
|
605
|
+
const response = await fetch(
|
|
606
|
+
`${this.baseUrl}/api/uploader/batch-update-status`,
|
|
607
|
+
{
|
|
608
|
+
method: 'PATCH',
|
|
609
|
+
headers: {
|
|
610
|
+
'x-api-key': this.token,
|
|
611
|
+
'Content-Type': 'application/json',
|
|
612
|
+
},
|
|
613
|
+
body: JSON.stringify({ updates }),
|
|
614
|
+
agent: isHttps ? this.httpsAgent : this.httpAgent,
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
if (!response.ok) {
|
|
619
|
+
const errorText = await response.text();
|
|
620
|
+
throw new Error(
|
|
621
|
+
`Batch status update failed: ${response.status} ${response.statusText} - ${errorText}`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const result = await response.json();
|
|
626
|
+
return result;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
logger.error(`Batch status update API error: ${error.message}`);
|
|
629
|
+
return {
|
|
630
|
+
success: false,
|
|
631
|
+
updated: 0,
|
|
632
|
+
errors: [{ message: error.message }],
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Get service name
|
|
639
|
+
* @returns {string} Service name
|
|
640
|
+
*/
|
|
641
|
+
getServiceName() {
|
|
642
|
+
return 'Arela API';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Build folder structure from options and auto-detection
|
|
647
|
+
* @private
|
|
648
|
+
* @param {Array} files - Files being uploaded
|
|
649
|
+
* @param {Object} options - Upload options
|
|
650
|
+
* @returns {string|null} Folder structure or null
|
|
651
|
+
*/
|
|
652
|
+
#buildFolderStructure(files, options) {
|
|
653
|
+
let combinedStructure = null;
|
|
654
|
+
|
|
655
|
+
if (
|
|
656
|
+
options.folderStructure &&
|
|
657
|
+
options.autoDetectStructure &&
|
|
658
|
+
files.length > 0
|
|
659
|
+
) {
|
|
660
|
+
// This would require the path detection utility - we'll implement this after creating that module
|
|
661
|
+
console.log(`📁 Custom folder structure: ${options.folderStructure}`);
|
|
662
|
+
return options.folderStructure;
|
|
663
|
+
} else if (options.folderStructure) {
|
|
664
|
+
console.log(
|
|
665
|
+
`📁 Using custom folder structure: ${options.folderStructure}`,
|
|
666
|
+
);
|
|
667
|
+
return options.folderStructure;
|
|
668
|
+
} else if (options.autoDetectStructure && files.length > 0) {
|
|
669
|
+
// Auto-detection logic would go here
|
|
670
|
+
console.log('📁 Auto-detection requested but not yet implemented');
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import FormData from 'form-data';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base upload service interface
|
|
7
|
+
* Defines the contract for upload implementations
|
|
8
|
+
*/
|
|
9
|
+
export class BaseUploadService {
|
|
10
|
+
/**
|
|
11
|
+
* Upload files using the specific implementation
|
|
12
|
+
* @param {Array} files - Array of file objects with path, name, and contentType
|
|
13
|
+
* @param {Object} options - Upload options
|
|
14
|
+
* @returns {Promise<Object>} Upload result
|
|
15
|
+
* @throws {Error} If upload fails
|
|
16
|
+
*/
|
|
17
|
+
async upload(files, options) {
|
|
18
|
+
throw new Error('upload() method must be implemented by subclass');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if the service is available and properly configured
|
|
23
|
+
* @returns {Promise<boolean>} True if service is available
|
|
24
|
+
*/
|
|
25
|
+
async isAvailable() {
|
|
26
|
+
throw new Error('isAvailable() method must be implemented by subclass');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the service name for logging purposes
|
|
31
|
+
* @returns {string} Service name
|
|
32
|
+
*/
|
|
33
|
+
getServiceName() {
|
|
34
|
+
throw new Error('getServiceName() method must be implemented by subclass');
|
|
35
|
+
}
|
|
36
|
+
}
|