@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,233 @@
|
|
|
1
|
+
import { Agent } from 'http';
|
|
2
|
+
import { Agent as HttpsAgent } from 'https';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
|
|
5
|
+
import appConfig from '../../config/config.js';
|
|
6
|
+
import logger from '../LoggingService.js';
|
|
7
|
+
import { ApiUploadService } from './ApiUploadService.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Multi-API Upload Service
|
|
11
|
+
*
|
|
12
|
+
* Extends ApiUploadService to support uploading to multiple API instances
|
|
13
|
+
* based on RFC/client routing. Uses service discovery from Global API.
|
|
14
|
+
*/
|
|
15
|
+
export class MultiApiUploadService extends ApiUploadService {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
// Service discovery configuration
|
|
20
|
+
this.globalApiUrl = process.env.API_GLOBAL_URL || appConfig.api.baseUrl;
|
|
21
|
+
this.serviceCache = new Map(); // RFC -> { url, cachedAt }
|
|
22
|
+
this.cacheTtlMs = parseInt(process.env.SERVICE_CACHE_TTL) || 300000; // 5 minutes
|
|
23
|
+
|
|
24
|
+
logger.info('🌐 MultiApiUploadService initialized');
|
|
25
|
+
logger.info(` Global API: ${this.globalApiUrl}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Discover service URL for a specific RFC
|
|
30
|
+
* @param {string} rfc - The client RFC
|
|
31
|
+
* @returns {Promise<string|null>} Service URL or null
|
|
32
|
+
*/
|
|
33
|
+
async discoverServiceByRfc(rfc) {
|
|
34
|
+
const rfcUpper = rfc.toUpperCase();
|
|
35
|
+
|
|
36
|
+
// Check cache first
|
|
37
|
+
const cached = this.serviceCache.get(rfcUpper);
|
|
38
|
+
if (cached && Date.now() - cached.cachedAt < this.cacheTtlMs) {
|
|
39
|
+
logger.debug(
|
|
40
|
+
`📋 Using cached service URL for ${rfcUpper}: ${cached.url}`,
|
|
41
|
+
);
|
|
42
|
+
return cached.url;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fetch from Global API
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(
|
|
48
|
+
`${this.globalApiUrl}/api/service-registry/by-rfc/${rfcUpper}`,
|
|
49
|
+
{
|
|
50
|
+
method: 'GET',
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${this.token}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
agent: this.globalApiUrl.startsWith('https')
|
|
56
|
+
? this.httpsAgent
|
|
57
|
+
: this.httpAgent,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
logger.warn(
|
|
63
|
+
`⚠️ Service discovery failed for ${rfcUpper}: ${response.status}`,
|
|
64
|
+
);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
if (data.url) {
|
|
71
|
+
// Cache the result
|
|
72
|
+
this.serviceCache.set(rfcUpper, {
|
|
73
|
+
url: data.url,
|
|
74
|
+
cachedAt: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
logger.info(`🔍 Discovered service for ${rfcUpper}: ${data.url}`);
|
|
78
|
+
return data.url;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(
|
|
84
|
+
`❌ Service discovery error for ${rfcUpper}: ${error.message}`,
|
|
85
|
+
);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clear service cache
|
|
92
|
+
*/
|
|
93
|
+
clearCache() {
|
|
94
|
+
this.serviceCache.clear();
|
|
95
|
+
logger.info('🧹 Service cache cleared');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear cache for specific RFC
|
|
100
|
+
* @param {string} rfc - The RFC to clear
|
|
101
|
+
*/
|
|
102
|
+
clearRfcCache(rfc) {
|
|
103
|
+
this.serviceCache.delete(rfc.toUpperCase());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Upload files to the appropriate API based on RFC
|
|
108
|
+
* @param {Array} files - Array of file objects
|
|
109
|
+
* @param {Object} options - Upload options including rfc
|
|
110
|
+
* @returns {Promise<Object>} API response
|
|
111
|
+
*/
|
|
112
|
+
async upload(files, options) {
|
|
113
|
+
const { rfc } = options;
|
|
114
|
+
|
|
115
|
+
if (!rfc) {
|
|
116
|
+
// No RFC specified, use default API
|
|
117
|
+
return super.upload(files, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Discover the service URL for this RFC
|
|
121
|
+
const serviceUrl = await this.discoverServiceByRfc(rfc);
|
|
122
|
+
|
|
123
|
+
if (serviceUrl) {
|
|
124
|
+
// Override baseUrl for this upload
|
|
125
|
+
const originalBaseUrl = this.baseUrl;
|
|
126
|
+
this.baseUrl = serviceUrl;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
logger.info(
|
|
130
|
+
`📤 Uploading ${files.length} files to ${serviceUrl} (RFC: ${rfc})`,
|
|
131
|
+
);
|
|
132
|
+
const result = await super.upload(files, options);
|
|
133
|
+
return result;
|
|
134
|
+
} finally {
|
|
135
|
+
// Restore original baseUrl
|
|
136
|
+
this.baseUrl = originalBaseUrl;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fallback to default API if service discovery fails
|
|
141
|
+
logger.warn(`⚠️ No specific service found for ${rfc}, using default API`);
|
|
142
|
+
return super.upload(files, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Upload files to multiple RFCs in parallel
|
|
147
|
+
* @param {Object} filesByRfc - Map of RFC -> files array
|
|
148
|
+
* @param {Object} options - Upload options
|
|
149
|
+
* @returns {Promise<Map<string, Object>>} Results by RFC
|
|
150
|
+
*/
|
|
151
|
+
async uploadToMultipleRfcs(filesByRfc, options = {}) {
|
|
152
|
+
const results = new Map();
|
|
153
|
+
const rfcs = Object.keys(filesByRfc);
|
|
154
|
+
|
|
155
|
+
logger.info(`📤 Starting multi-RFC upload for ${rfcs.length} RFCs`);
|
|
156
|
+
|
|
157
|
+
// Process RFCs in parallel (with concurrency limit)
|
|
158
|
+
const concurrencyLimit = parseInt(process.env.MULTI_RFC_CONCURRENCY) || 3;
|
|
159
|
+
const chunks = [];
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < rfcs.length; i += concurrencyLimit) {
|
|
162
|
+
chunks.push(rfcs.slice(i, i + concurrencyLimit));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
await Promise.all(
|
|
167
|
+
chunk.map(async (rfc) => {
|
|
168
|
+
try {
|
|
169
|
+
const files = filesByRfc[rfc];
|
|
170
|
+
const result = await this.upload(files, { ...options, rfc });
|
|
171
|
+
results.set(rfc, { success: true, result });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error(`❌ Upload failed for RFC ${rfc}: ${error.message}`);
|
|
174
|
+
results.set(rfc, { success: false, error: error.message });
|
|
175
|
+
}
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Log summary
|
|
181
|
+
const successful = [...results.values()].filter((r) => r.success).length;
|
|
182
|
+
const failed = results.size - successful;
|
|
183
|
+
logger.info(
|
|
184
|
+
`✅ Multi-RFC upload complete: ${successful} successful, ${failed} failed`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get all available services from Global API
|
|
192
|
+
* @returns {Promise<Array>} List of services
|
|
193
|
+
*/
|
|
194
|
+
async getAllServices() {
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(
|
|
197
|
+
`${this.globalApiUrl}/api/service-registry/services`,
|
|
198
|
+
{
|
|
199
|
+
method: 'GET',
|
|
200
|
+
headers: {
|
|
201
|
+
Authorization: `Bearer ${this.token}`,
|
|
202
|
+
'Content-Type': 'application/json',
|
|
203
|
+
},
|
|
204
|
+
agent: this.globalApiUrl.startsWith('https')
|
|
205
|
+
? this.httpsAgent
|
|
206
|
+
: this.httpAgent,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(`Failed to fetch services: ${response.status}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
return data.services || [];
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.error(`❌ Failed to get services: ${error.message}`);
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Pre-warm cache for a list of RFCs
|
|
224
|
+
* @param {Array<string>} rfcs - List of RFCs to cache
|
|
225
|
+
*/
|
|
226
|
+
async prewarmCache(rfcs) {
|
|
227
|
+
logger.info(`🔥 Pre-warming cache for ${rfcs.length} RFCs`);
|
|
228
|
+
|
|
229
|
+
await Promise.all(rfcs.map((rfc) => this.discoverServiceByRfc(rfc)));
|
|
230
|
+
|
|
231
|
+
logger.info('✅ Cache pre-warm complete');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import mime from 'mime-types';
|
|
4
|
+
|
|
5
|
+
import appConfig from '../../config/config.js';
|
|
6
|
+
import { BaseUploadService } from './BaseUploadService.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Supabase Upload Service
|
|
10
|
+
* Handles direct uploads to Supabase Storage
|
|
11
|
+
*/
|
|
12
|
+
export class SupabaseUploadService extends BaseUploadService {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.client = null;
|
|
16
|
+
this.bucket = appConfig.supabase.bucket;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize Supabase client if not already initialized
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
async #initializeClient() {
|
|
24
|
+
if (this.client) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!appConfig.supabase.url || !appConfig.supabase.key || !this.bucket) {
|
|
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(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`🔌 Connecting to Supabase: ${appConfig.supabase.url}`);
|
|
37
|
+
console.log(`📦 Using bucket: ${this.bucket}`);
|
|
38
|
+
|
|
39
|
+
this.client = createClient(appConfig.supabase.url, appConfig.supabase.key, {
|
|
40
|
+
db: {
|
|
41
|
+
schema: 'public',
|
|
42
|
+
},
|
|
43
|
+
auth: {
|
|
44
|
+
autoRefreshToken: false,
|
|
45
|
+
persistSession: false,
|
|
46
|
+
},
|
|
47
|
+
global: {
|
|
48
|
+
headers: {
|
|
49
|
+
'x-client-info': 'arela-uploader',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
// Increase timeout settings to handle large queries
|
|
53
|
+
realtime: {
|
|
54
|
+
timeout: 60000, // 60 seconds
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
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 });
|
|
63
|
+
if (error) {
|
|
64
|
+
console.error(`❌ Bucket access test failed:`, error.message);
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Error connecting to Supabase bucket '${this.bucket}': ${error.message}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`✅ Successfully connected to Supabase bucket: ${this.bucket}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Upload file directly to Supabase
|
|
75
|
+
* @param {Array} files - Array with single file object
|
|
76
|
+
* @param {Object} options - Upload options with uploadPath
|
|
77
|
+
* @returns {Promise<Object>} Upload result
|
|
78
|
+
*/
|
|
79
|
+
async upload(files, options) {
|
|
80
|
+
await this.#initializeClient();
|
|
81
|
+
|
|
82
|
+
if (files.length !== 1) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'SupabaseUploadService only supports single file uploads',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const file = files[0];
|
|
89
|
+
const uploadPath = options.uploadPath;
|
|
90
|
+
|
|
91
|
+
if (!uploadPath) {
|
|
92
|
+
throw new Error('uploadPath is required for Supabase uploads');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content = fs.readFileSync(file.path);
|
|
96
|
+
const contentType = mime.lookup(file.path) || 'application/octet-stream';
|
|
97
|
+
|
|
98
|
+
const normalizedPath = uploadPath.replace(/\\/g, '/');
|
|
99
|
+
|
|
100
|
+
const { data, error } = await this.client.storage
|
|
101
|
+
.from(this.bucket)
|
|
102
|
+
.upload(normalizedPath, content, {
|
|
103
|
+
upsert: true,
|
|
104
|
+
contentType,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (error) {
|
|
108
|
+
console.error(
|
|
109
|
+
`❌ Supabase upload failed for ${normalizedPath}:`,
|
|
110
|
+
error.message,
|
|
111
|
+
);
|
|
112
|
+
return { success: false, error: error.message };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`✅ Supabase upload successful: ${normalizedPath}`);
|
|
116
|
+
return { success: true, data, uploadPath: normalizedPath };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if Supabase service is available
|
|
121
|
+
* @returns {Promise<boolean>} True if available
|
|
122
|
+
*/
|
|
123
|
+
async isAvailable() {
|
|
124
|
+
try {
|
|
125
|
+
await this.#initializeClient();
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get service name
|
|
134
|
+
* @returns {string} Service name
|
|
135
|
+
*/
|
|
136
|
+
getServiceName() {
|
|
137
|
+
return 'Supabase';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get Supabase client for database operations
|
|
142
|
+
* @returns {Object} Supabase client
|
|
143
|
+
*/
|
|
144
|
+
async getClient() {
|
|
145
|
+
await this.#initializeClient();
|
|
146
|
+
return this.client;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ApiUploadService } from './ApiUploadService.js';
|
|
2
|
+
import { SupabaseUploadService } from './SupabaseUploadService.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Upload Service Factory
|
|
6
|
+
* Manages upload service selection and initialization
|
|
7
|
+
*/
|
|
8
|
+
export class UploadServiceFactory {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.apiService = new ApiUploadService();
|
|
11
|
+
this.supabaseService = new SupabaseUploadService();
|
|
12
|
+
// Cache for target-specific API services
|
|
13
|
+
this._targetServices = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the appropriate upload service based on availability and preferences
|
|
18
|
+
* @param {boolean} forceSupabase - Force Supabase mode
|
|
19
|
+
* @returns {Promise<BaseUploadService>} Upload service instance
|
|
20
|
+
* @throws {Error} If no service is available
|
|
21
|
+
*/
|
|
22
|
+
async getUploadService(forceSupabase = false) {
|
|
23
|
+
if (forceSupabase) {
|
|
24
|
+
console.log('🔧 Force Supabase mode enabled - skipping API');
|
|
25
|
+
|
|
26
|
+
if (await this.supabaseService.isAvailable()) {
|
|
27
|
+
console.log(
|
|
28
|
+
`✅ Connected to ${this.supabaseService.getServiceName()} (direct mode)`,
|
|
29
|
+
);
|
|
30
|
+
return this.supabaseService;
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error('Supabase service is not available');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Try API service first
|
|
37
|
+
if (await this.apiService.isAvailable()) {
|
|
38
|
+
console.log(
|
|
39
|
+
'🌐 API mode enabled - files will be uploaded to Arela API with automatic processing',
|
|
40
|
+
);
|
|
41
|
+
console.log(`✅ Connected to ${this.apiService.getServiceName()}`);
|
|
42
|
+
return this.apiService;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback to Supabase
|
|
46
|
+
if (await this.supabaseService.isAvailable()) {
|
|
47
|
+
console.warn(
|
|
48
|
+
'⚠️ API connection failed, falling back to direct Supabase upload',
|
|
49
|
+
);
|
|
50
|
+
console.log(
|
|
51
|
+
`✅ Connected to ${this.supabaseService.getServiceName()} (direct mode)`,
|
|
52
|
+
);
|
|
53
|
+
return this.supabaseService;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error('No upload service is available');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get an API service configured for a specific target
|
|
61
|
+
* @param {string} target - API target: 'agencia', 'cliente', 'ktj931117p55', or 'default'
|
|
62
|
+
* @returns {Promise<ApiUploadService>} API service for the specified target
|
|
63
|
+
*/
|
|
64
|
+
async getApiServiceForTarget(target) {
|
|
65
|
+
// Import config dynamically to avoid circular dependency
|
|
66
|
+
const { appConfig } = await import('../../config/config.js');
|
|
67
|
+
|
|
68
|
+
const targetConfig = appConfig.getApiConfig(target);
|
|
69
|
+
if (!targetConfig.baseUrl) {
|
|
70
|
+
throw new Error(`No API URL configured for target '${target}'`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create a new service instance with specific config
|
|
74
|
+
// Use setExternalConfig to prevent automatic config refresh from overwriting
|
|
75
|
+
const service = new ApiUploadService();
|
|
76
|
+
service.setExternalConfig(targetConfig.baseUrl, targetConfig.token);
|
|
77
|
+
|
|
78
|
+
return service;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if API mode is currently active
|
|
83
|
+
* @returns {Promise<boolean>} True if API service is available
|
|
84
|
+
*/
|
|
85
|
+
async isApiModeAvailable() {
|
|
86
|
+
return await this.apiService.isAvailable();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get Supabase service for database operations
|
|
91
|
+
* @returns {SupabaseUploadService} Supabase service instance
|
|
92
|
+
*/
|
|
93
|
+
getSupabaseService() {
|
|
94
|
+
return this.supabaseService;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Export singleton instance
|
|
99
|
+
export const uploadServiceFactory = new UploadServiceFactory();
|
|
100
|
+
export default uploadServiceFactory;
|