@tamyla/clodo-framework 1.0.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/CHANGELOG.md +564 -0
- package/LICENSE +21 -0
- package/README.md +1393 -0
- package/bin/README.md +71 -0
- package/bin/clodo-service.js +416 -0
- package/bin/security/security-cli.js +96 -0
- package/bin/service-management/README.md +74 -0
- package/bin/service-management/create-service.js +129 -0
- package/bin/service-management/init-service.js +102 -0
- package/bin/service-management/init-service.js.backup +889 -0
- package/bin/shared/config/customer-cli.js +293 -0
- package/dist/config/ConfigurationManager.js +159 -0
- package/dist/config/CustomerConfigCLI.js +220 -0
- package/dist/config/FeatureManager.js +426 -0
- package/dist/config/customers.js +441 -0
- package/dist/config/domains.js +180 -0
- package/dist/config/features.js +225 -0
- package/dist/config/index.js +6 -0
- package/dist/database/database-orchestrator.js +730 -0
- package/dist/database/index.js +4 -0
- package/dist/deployment/auditor.js +971 -0
- package/dist/deployment/index.js +10 -0
- package/dist/deployment/rollback-manager.js +523 -0
- package/dist/deployment/testers/api-tester.js +80 -0
- package/dist/deployment/testers/auth-tester.js +129 -0
- package/dist/deployment/testers/core.js +217 -0
- package/dist/deployment/testers/database-tester.js +105 -0
- package/dist/deployment/testers/index.js +74 -0
- package/dist/deployment/testers/load-tester.js +120 -0
- package/dist/deployment/testers/performance-tester.js +105 -0
- package/dist/deployment/validator.js +558 -0
- package/dist/deployment/wrangler-deployer.js +574 -0
- package/dist/handlers/GenericRouteHandler.js +532 -0
- package/dist/index.js +39 -0
- package/dist/migration/MigrationAdapters.js +562 -0
- package/dist/modules/ModuleManager.js +668 -0
- package/dist/modules/security.js +98 -0
- package/dist/orchestration/cross-domain-coordinator.js +1083 -0
- package/dist/orchestration/index.js +5 -0
- package/dist/orchestration/modules/DeploymentCoordinator.js +258 -0
- package/dist/orchestration/modules/DomainResolver.js +196 -0
- package/dist/orchestration/modules/StateManager.js +332 -0
- package/dist/orchestration/multi-domain-orchestrator.js +255 -0
- package/dist/routing/EnhancedRouter.js +158 -0
- package/dist/schema/SchemaManager.js +778 -0
- package/dist/security/ConfigurationValidator.js +490 -0
- package/dist/security/DeploymentManager.js +208 -0
- package/dist/security/SecretGenerator.js +142 -0
- package/dist/security/SecurityCLI.js +228 -0
- package/dist/security/index.js +51 -0
- package/dist/security/patterns/environment-rules.js +66 -0
- package/dist/security/patterns/insecure-patterns.js +21 -0
- package/dist/service-management/ConfirmationEngine.js +411 -0
- package/dist/service-management/ErrorTracker.js +294 -0
- package/dist/service-management/GenerationEngine.js +3109 -0
- package/dist/service-management/InputCollector.js +237 -0
- package/dist/service-management/ServiceCreator.js +229 -0
- package/dist/service-management/ServiceInitializer.js +448 -0
- package/dist/service-management/ServiceOrchestrator.js +638 -0
- package/dist/service-management/handlers/ConfigMutator.js +130 -0
- package/dist/service-management/handlers/ConfirmationHandler.js +71 -0
- package/dist/service-management/handlers/GenerationHandler.js +80 -0
- package/dist/service-management/handlers/InputHandler.js +59 -0
- package/dist/service-management/handlers/ValidationHandler.js +203 -0
- package/dist/service-management/index.js +7 -0
- package/dist/services/GenericDataService.js +488 -0
- package/dist/shared/cloudflare/domain-discovery.js +562 -0
- package/dist/shared/cloudflare/domain-manager.js +912 -0
- package/dist/shared/cloudflare/index.js +8 -0
- package/dist/shared/cloudflare/ops.js +387 -0
- package/dist/shared/config/cache.js +1167 -0
- package/dist/shared/config/command-config-manager.js +174 -0
- package/dist/shared/config/customer-cli.js +258 -0
- package/dist/shared/config/index.js +9 -0
- package/dist/shared/config/manager.js +289 -0
- package/dist/shared/database/connection-manager.js +338 -0
- package/dist/shared/database/index.js +7 -0
- package/dist/shared/database/orchestrator.js +632 -0
- package/dist/shared/deployment/auditor.js +971 -0
- package/dist/shared/deployment/index.js +10 -0
- package/dist/shared/deployment/rollback-manager.js +523 -0
- package/dist/shared/deployment/validator.js +558 -0
- package/dist/shared/index.js +32 -0
- package/dist/shared/monitoring/health-checker.js +250 -0
- package/dist/shared/monitoring/index.js +8 -0
- package/dist/shared/monitoring/memory-manager.js +382 -0
- package/dist/shared/monitoring/production-monitor.js +390 -0
- package/dist/shared/production-tester/api-tester.js +80 -0
- package/dist/shared/production-tester/auth-tester.js +129 -0
- package/dist/shared/production-tester/core.js +217 -0
- package/dist/shared/production-tester/database-tester.js +105 -0
- package/dist/shared/production-tester/index.js +74 -0
- package/dist/shared/production-tester/load-tester.js +120 -0
- package/dist/shared/production-tester/performance-tester.js +105 -0
- package/dist/shared/security/api-token-manager.js +296 -0
- package/dist/shared/security/index.js +8 -0
- package/dist/shared/security/secret-generator.js +918 -0
- package/dist/shared/security/secure-token-manager.js +379 -0
- package/dist/shared/utils/error-recovery.js +240 -0
- package/dist/shared/utils/graceful-shutdown-manager.js +380 -0
- package/dist/shared/utils/index.js +9 -0
- package/dist/shared/utils/interactive-prompts.js +134 -0
- package/dist/shared/utils/rate-limiter.js +249 -0
- package/dist/utils/ErrorHandler.js +173 -0
- package/dist/utils/deployment/config-cache.js +1160 -0
- package/dist/utils/deployment/index.js +6 -0
- package/dist/utils/deployment/interactive-prompts.js +97 -0
- package/dist/utils/deployment/secret-generator.js +896 -0
- package/dist/utils/dirname-helper.js +35 -0
- package/dist/utils/domain-config.js +159 -0
- package/dist/utils/error-recovery.js +240 -0
- package/dist/utils/esm-helper.js +52 -0
- package/dist/utils/framework-config.js +481 -0
- package/dist/utils/graceful-shutdown-manager.js +379 -0
- package/dist/utils/health-checker.js +114 -0
- package/dist/utils/index.js +36 -0
- package/dist/utils/prompt-handler.js +98 -0
- package/dist/utils/usage-tracker.js +252 -0
- package/dist/utils/validation.js +112 -0
- package/dist/version/VersionDetector.js +723 -0
- package/dist/worker/index.js +4 -0
- package/dist/worker/integration.js +332 -0
- package/docs/FRAMEWORK-ARCHITECTURE-OVERVIEW.md +206 -0
- package/docs/INTEGRATION_GUIDE.md +2045 -0
- package/docs/README.md +82 -0
- package/docs/SECURITY.md +242 -0
- package/docs/deployment/deployment-guide.md +540 -0
- package/docs/overview.md +280 -0
- package/package.json +176 -0
- package/types/index.d.ts +575 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager Module
|
|
3
|
+
* Configuration file management and validation
|
|
4
|
+
*
|
|
5
|
+
* Consolidates config management across 34+ scripts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync, statSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
export function updateWranglerConfig(updates, configPath = 'wrangler.toml') {
|
|
11
|
+
if (!existsSync(configPath)) {
|
|
12
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
13
|
+
}
|
|
14
|
+
let config = readFileSync(configPath, 'utf8');
|
|
15
|
+
let changesMade = [];
|
|
16
|
+
|
|
17
|
+
// Apply updates systematically
|
|
18
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
19
|
+
switch (key) {
|
|
20
|
+
case 'workerName':
|
|
21
|
+
// Update main worker name
|
|
22
|
+
if (config.match(/^name = "[^"]*"/m)) {
|
|
23
|
+
config = config.replace(/^name = "[^"]*"/m, `name = "${value}"`);
|
|
24
|
+
changesMade.push(`Updated main worker name to: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Update production environment name
|
|
28
|
+
if (config.match(/^\[env\.production\]\s*\nname = "[^"]*"/m)) {
|
|
29
|
+
config = config.replace(/^\[env\.production\]\s*\nname = "[^"]*"/m, `[env.production]\nname = "${value}"`);
|
|
30
|
+
changesMade.push(`Updated production worker name to: ${value}`);
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
case 'databaseName':
|
|
34
|
+
{
|
|
35
|
+
const dbNameRegex = /database_name = "[^"]*"/g;
|
|
36
|
+
if (config.match(dbNameRegex)) {
|
|
37
|
+
config = config.replace(dbNameRegex, `database_name = "${value}"`);
|
|
38
|
+
changesMade.push(`Updated database name to: ${value}`);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'databaseId':
|
|
43
|
+
{
|
|
44
|
+
const dbIdRegex = /database_id = "[^"]*"/g;
|
|
45
|
+
if (config.match(dbIdRegex)) {
|
|
46
|
+
config = config.replace(dbIdRegex, `database_id = "${value}"`);
|
|
47
|
+
changesMade.push(`Updated database ID to: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'serviceDomain':
|
|
52
|
+
{
|
|
53
|
+
const domainRegex = /SERVICE_DOMAIN = "[^"]*"/g;
|
|
54
|
+
if (config.match(domainRegex)) {
|
|
55
|
+
config = config.replace(domainRegex, `SERVICE_DOMAIN = "${value}"`);
|
|
56
|
+
changesMade.push(`Updated service domain to: ${value}`);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'compatibilityDate':
|
|
61
|
+
{
|
|
62
|
+
const dateRegex = /^compatibility_date = "[^"]*"/m;
|
|
63
|
+
if (config.match(dateRegex)) {
|
|
64
|
+
config = config.replace(dateRegex, `compatibility_date = "${value}"`);
|
|
65
|
+
changesMade.push(`Updated compatibility date to: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
console.warn(`Unknown config update key: ${key}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(configPath, config);
|
|
74
|
+
return {
|
|
75
|
+
configPath,
|
|
76
|
+
changesMade,
|
|
77
|
+
success: changesMade.length > 0
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function backupConfig(configPath = 'wrangler.toml', suffix = null) {
|
|
81
|
+
if (!existsSync(configPath)) {
|
|
82
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
83
|
+
}
|
|
84
|
+
const timestamp = suffix || new Date().toISOString().replace(/[:.]/g, '-');
|
|
85
|
+
const backupPath = `${configPath}.backup.${timestamp}`;
|
|
86
|
+
copyFileSync(configPath, backupPath);
|
|
87
|
+
return {
|
|
88
|
+
originalPath: configPath,
|
|
89
|
+
backupPath,
|
|
90
|
+
timestamp: new Date().toISOString()
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function validateConfig(configPath = 'wrangler.toml') {
|
|
94
|
+
if (!existsSync(configPath)) {
|
|
95
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
96
|
+
}
|
|
97
|
+
const config = readFileSync(configPath, 'utf8');
|
|
98
|
+
const issues = [];
|
|
99
|
+
const warnings = [];
|
|
100
|
+
|
|
101
|
+
// Required fields validation
|
|
102
|
+
const requiredFields = [{
|
|
103
|
+
pattern: /^name = "/m,
|
|
104
|
+
description: 'Worker name'
|
|
105
|
+
}, {
|
|
106
|
+
pattern: /^compatibility_date = "/m,
|
|
107
|
+
description: 'Compatibility date'
|
|
108
|
+
}, {
|
|
109
|
+
pattern: /^main = "/m,
|
|
110
|
+
description: 'Main entry point'
|
|
111
|
+
}];
|
|
112
|
+
requiredFields.forEach(field => {
|
|
113
|
+
if (!config.match(field.pattern)) {
|
|
114
|
+
issues.push(`Missing required field: ${field.description}`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Database configuration validation
|
|
119
|
+
const hasDatabaseBinding = config.includes('[[d1_databases]]');
|
|
120
|
+
if (hasDatabaseBinding) {
|
|
121
|
+
if (!config.includes('database_name =')) {
|
|
122
|
+
issues.push('Database binding found but missing database_name');
|
|
123
|
+
}
|
|
124
|
+
if (!config.includes('database_id =')) {
|
|
125
|
+
issues.push('Database binding found but missing database_id');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Environment validation
|
|
130
|
+
const hasProductionEnv = config.includes('[env.production]');
|
|
131
|
+
if (hasProductionEnv) {
|
|
132
|
+
const prodSection = config.split('[env.production]')[1];
|
|
133
|
+
if (prodSection && !prodSection.includes('name =')) {
|
|
134
|
+
warnings.push('Production environment missing explicit name');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Compatibility date validation
|
|
139
|
+
const compatMatch = config.match(/compatibility_date = "([^"]+)"/);
|
|
140
|
+
if (compatMatch) {
|
|
141
|
+
const date = new Date(compatMatch[1]);
|
|
142
|
+
const now = new Date();
|
|
143
|
+
const daysDiff = (now - date) / (1000 * 60 * 60 * 24);
|
|
144
|
+
if (daysDiff > 365) {
|
|
145
|
+
warnings.push(`Compatibility date is ${Math.floor(daysDiff)} days old`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
isValid: issues.length === 0,
|
|
150
|
+
issues,
|
|
151
|
+
warnings,
|
|
152
|
+
summary: {
|
|
153
|
+
errors: issues.length,
|
|
154
|
+
warnings: warnings.length,
|
|
155
|
+
status: issues.length === 0 ? 'valid' : 'invalid'
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function parseWranglerConfig(configPath = 'wrangler.toml') {
|
|
160
|
+
if (!existsSync(configPath)) {
|
|
161
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
162
|
+
}
|
|
163
|
+
const config = readFileSync(configPath, 'utf8');
|
|
164
|
+
const parsed = {};
|
|
165
|
+
|
|
166
|
+
// Extract main fields
|
|
167
|
+
const extractField = (pattern, key) => {
|
|
168
|
+
const match = config.match(pattern);
|
|
169
|
+
if (match) parsed[key] = match[1];
|
|
170
|
+
};
|
|
171
|
+
extractField(/^name = "([^"]+)"/m, 'name');
|
|
172
|
+
extractField(/^compatibility_date = "([^"]+)"/m, 'compatibilityDate');
|
|
173
|
+
extractField(/^main = "([^"]+)"/m, 'main');
|
|
174
|
+
|
|
175
|
+
// Extract database configuration
|
|
176
|
+
if (config.includes('[[d1_databases]]')) {
|
|
177
|
+
parsed.database = {};
|
|
178
|
+
extractField(/database_name = "([^"]+)"/m, 'name');
|
|
179
|
+
extractField(/database_id = "([^"]+)"/m, 'id');
|
|
180
|
+
if (parsed.name) parsed.database.name = parsed.name;
|
|
181
|
+
if (parsed.id) parsed.database.id = parsed.id;
|
|
182
|
+
delete parsed.name;
|
|
183
|
+
delete parsed.id;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Extract environment configurations
|
|
187
|
+
parsed.environments = {};
|
|
188
|
+
const envMatches = config.matchAll(/\[env\.([^\]]+)\]/g);
|
|
189
|
+
for (const match of envMatches) {
|
|
190
|
+
const envName = match[1];
|
|
191
|
+
parsed.environments[envName] = {};
|
|
192
|
+
|
|
193
|
+
// Extract env-specific settings (simplified parsing)
|
|
194
|
+
const envSection = config.split(`[env.${envName}]`)[1];
|
|
195
|
+
if (envSection) {
|
|
196
|
+
const nextEnv = envSection.indexOf('[env.');
|
|
197
|
+
const sectionContent = nextEnv > 0 ? envSection.substring(0, nextEnv) : envSection;
|
|
198
|
+
const nameMatch = sectionContent.match(/name = "([^"]+)"/);
|
|
199
|
+
if (nameMatch) parsed.environments[envName].name = nameMatch[1];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return parsed;
|
|
203
|
+
}
|
|
204
|
+
export function generateWranglerConfig(options) {
|
|
205
|
+
const {
|
|
206
|
+
name,
|
|
207
|
+
main = 'src/worker/index.js',
|
|
208
|
+
compatibilityDate = new Date().toISOString().split('T')[0],
|
|
209
|
+
database = null,
|
|
210
|
+
environments = {},
|
|
211
|
+
vars = {},
|
|
212
|
+
secrets = []
|
|
213
|
+
} = options;
|
|
214
|
+
let config = `# Generated Wrangler Configuration
|
|
215
|
+
# Timestamp: ${new Date().toISOString()}
|
|
216
|
+
|
|
217
|
+
name = "${name}"
|
|
218
|
+
main = "${main}"
|
|
219
|
+
compatibility_date = "${compatibilityDate}"
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
// Add variables
|
|
223
|
+
if (Object.keys(vars).length > 0) {
|
|
224
|
+
config += '\n[vars]\n';
|
|
225
|
+
Object.entries(vars).forEach(([key, value]) => {
|
|
226
|
+
config += `${key} = "${value}"\n`;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Add database configuration
|
|
231
|
+
if (database) {
|
|
232
|
+
config += `
|
|
233
|
+
[[d1_databases]]
|
|
234
|
+
binding = "DB"
|
|
235
|
+
database_name = "${database.name}"
|
|
236
|
+
database_id = "${database.id}"
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add environments
|
|
241
|
+
Object.entries(environments).forEach(([envName, envConfig]) => {
|
|
242
|
+
config += `\n[env.${envName}]\n`;
|
|
243
|
+
if (envConfig.name) config += `name = "${envConfig.name}"\n`;
|
|
244
|
+
if (envConfig.vars) {
|
|
245
|
+
Object.entries(envConfig.vars).forEach(([key, value]) => {
|
|
246
|
+
config += `${key} = "${value}"\n`;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (envConfig.database) {
|
|
250
|
+
config += `
|
|
251
|
+
[[env.${envName}.d1_databases]]
|
|
252
|
+
binding = "DB"
|
|
253
|
+
database_name = "${envConfig.database.name}"
|
|
254
|
+
database_id = "${envConfig.database.id}"
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return config;
|
|
259
|
+
}
|
|
260
|
+
export function listConfigFiles(directory = '.') {
|
|
261
|
+
const configFiles = [];
|
|
262
|
+
const patterns = ['wrangler.toml', 'package.json', '.env*'];
|
|
263
|
+
try {
|
|
264
|
+
const items = readdirSync(directory);
|
|
265
|
+
items.forEach(item => {
|
|
266
|
+
const path = join(directory, item);
|
|
267
|
+
const stat = statSync(path);
|
|
268
|
+
if (stat.isFile()) {
|
|
269
|
+
const matches = patterns.some(pattern => {
|
|
270
|
+
if (pattern.includes('*')) {
|
|
271
|
+
return item.startsWith(pattern.replace('*', ''));
|
|
272
|
+
}
|
|
273
|
+
return item === pattern;
|
|
274
|
+
});
|
|
275
|
+
if (matches) {
|
|
276
|
+
configFiles.push({
|
|
277
|
+
name: item,
|
|
278
|
+
path,
|
|
279
|
+
size: stat.size,
|
|
280
|
+
modified: stat.mtime
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Connection Manager
|
|
3
|
+
* Implements connection pooling, timeout handling, and retry logic for D1 operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { executeSql } from '../cloudflare/ops.js';
|
|
7
|
+
import { ErrorRecoveryManager } from '../utils/error-recovery.js';
|
|
8
|
+
export class DatabaseConnectionManager {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.config = null;
|
|
12
|
+
this.connectionPool = new Map();
|
|
13
|
+
this.activeConnections = 0;
|
|
14
|
+
this.errorRecovery = null; // Will be initialized after framework config is loaded
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize with framework configuration
|
|
19
|
+
*/
|
|
20
|
+
async initialize() {
|
|
21
|
+
// Import framework config for consistent database connection settings
|
|
22
|
+
const {
|
|
23
|
+
frameworkConfig
|
|
24
|
+
} = await import('../../../src/utils/framework-config.js');
|
|
25
|
+
const timing = frameworkConfig.getTiming();
|
|
26
|
+
const database = frameworkConfig.getDatabaseConfig();
|
|
27
|
+
this.config = {
|
|
28
|
+
maxRetries: this.options.maxRetries || timing.retryAttempts,
|
|
29
|
+
retryDelay: this.options.retryDelay || timing.retryDelay,
|
|
30
|
+
connectionTimeout: this.options.connectionTimeout || database.connectionTimeout,
|
|
31
|
+
queryTimeout: this.options.queryTimeout || database.queryTimeout,
|
|
32
|
+
enableConnectionPooling: this.options.enableConnectionPooling !== false,
|
|
33
|
+
maxPoolSize: this.options.maxPoolSize || database.maxPoolSize,
|
|
34
|
+
connectionIdleTimeout: this.options.connectionIdleTimeout || database.connectionIdleTimeout,
|
|
35
|
+
...this.options
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Initialize error recovery with loaded config
|
|
39
|
+
const {
|
|
40
|
+
ErrorRecoveryManager
|
|
41
|
+
} = await import('../utils/error-recovery.js');
|
|
42
|
+
this.errorRecovery = new ErrorRecoveryManager({
|
|
43
|
+
maxRetries: this.config.maxRetries,
|
|
44
|
+
retryDelay: this.config.retryDelay,
|
|
45
|
+
gracefulDegradation: true
|
|
46
|
+
});
|
|
47
|
+
await this.errorRecovery.initialize();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Execute a database query with connection management
|
|
52
|
+
*/
|
|
53
|
+
async executeQuery(databaseName, sql, options = {}) {
|
|
54
|
+
if (!this.config) {
|
|
55
|
+
throw new Error('ConnectionManager must be initialized before use. Call initialize() first.');
|
|
56
|
+
}
|
|
57
|
+
const config = {
|
|
58
|
+
env: 'production',
|
|
59
|
+
timeout: this.config.queryTimeout,
|
|
60
|
+
usePool: this.config.enableConnectionPooling,
|
|
61
|
+
...options
|
|
62
|
+
};
|
|
63
|
+
return await this.errorRecovery.executeWithRecovery(async () => {
|
|
64
|
+
return await this.executeQueryInternal(databaseName, sql, config);
|
|
65
|
+
}, {
|
|
66
|
+
operationId: `db_query_${databaseName}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute query with timeout and connection handling
|
|
72
|
+
*/
|
|
73
|
+
async executeQueryInternal(databaseName, sql, config) {
|
|
74
|
+
const startTime = Date.now();
|
|
75
|
+
try {
|
|
76
|
+
// Get or create connection
|
|
77
|
+
const connection = await this.getConnection(databaseName, config);
|
|
78
|
+
|
|
79
|
+
// Execute query with timeout
|
|
80
|
+
const result = await this.executeWithTimeout(() => executeSql(databaseName, sql, config.env), config.timeout);
|
|
81
|
+
const duration = Date.now() - startTime;
|
|
82
|
+
|
|
83
|
+
// Release connection back to pool
|
|
84
|
+
if (config.usePool) {
|
|
85
|
+
this.releaseConnection(databaseName, connection);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
result,
|
|
90
|
+
duration,
|
|
91
|
+
connectionId: connection.id
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const duration = Date.now() - startTime;
|
|
95
|
+
|
|
96
|
+
// Handle connection errors
|
|
97
|
+
if (this.isConnectionError(error)) {
|
|
98
|
+
await this.handleConnectionError(databaseName, error);
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Database query failed: ${error.message} (duration: ${duration}ms)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute multiple queries in a transaction
|
|
106
|
+
*/
|
|
107
|
+
async executeTransaction(databaseName, queries, options = {}) {
|
|
108
|
+
const config = {
|
|
109
|
+
env: 'production',
|
|
110
|
+
timeout: this.config.queryTimeout * queries.length,
|
|
111
|
+
// Longer timeout for transactions
|
|
112
|
+
...options
|
|
113
|
+
};
|
|
114
|
+
return await this.errorRecovery.executeWithRecovery(async () => {
|
|
115
|
+
return await this.executeTransactionInternal(databaseName, queries, config);
|
|
116
|
+
}, {
|
|
117
|
+
operationId: `db_transaction_${databaseName}`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Execute transaction with proper error handling
|
|
123
|
+
*/
|
|
124
|
+
async executeTransactionInternal(databaseName, queries, config) {
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
const results = [];
|
|
127
|
+
try {
|
|
128
|
+
const connection = await this.getConnection(databaseName, config);
|
|
129
|
+
for (let i = 0; i < queries.length; i++) {
|
|
130
|
+
const query = queries[i];
|
|
131
|
+
try {
|
|
132
|
+
const result = await this.executeWithTimeout(() => executeSql(databaseName, query, config.env), config.timeout / queries.length // Divide timeout among queries
|
|
133
|
+
);
|
|
134
|
+
results.push({
|
|
135
|
+
index: i,
|
|
136
|
+
success: true,
|
|
137
|
+
result,
|
|
138
|
+
query
|
|
139
|
+
});
|
|
140
|
+
} catch (queryError) {
|
|
141
|
+
// Transaction failed, all previous queries should be rolled back
|
|
142
|
+
// Note: D1 doesn't support explicit transactions, so we need to handle this at application level
|
|
143
|
+
results.push({
|
|
144
|
+
index: i,
|
|
145
|
+
success: false,
|
|
146
|
+
error: queryError.message,
|
|
147
|
+
query
|
|
148
|
+
});
|
|
149
|
+
throw new Error(`Transaction failed at query ${i}: ${queryError.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const duration = Date.now() - startTime;
|
|
153
|
+
if (config.usePool) {
|
|
154
|
+
this.releaseConnection(databaseName, connection);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
results,
|
|
159
|
+
duration,
|
|
160
|
+
transactionId: `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const duration = Date.now() - startTime;
|
|
164
|
+
throw new Error(`Database transaction failed: ${error.message} (duration: ${duration}ms)`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get a connection from the pool or create a new one
|
|
170
|
+
*/
|
|
171
|
+
async getConnection(databaseName, config) {
|
|
172
|
+
if (!config.usePool) {
|
|
173
|
+
return this.createConnection(databaseName);
|
|
174
|
+
}
|
|
175
|
+
const poolKey = databaseName;
|
|
176
|
+
let pool = this.connectionPool.get(poolKey);
|
|
177
|
+
if (!pool) {
|
|
178
|
+
pool = [];
|
|
179
|
+
this.connectionPool.set(poolKey, pool);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Find an available connection
|
|
183
|
+
let connection = pool.find(conn => !conn.inUse && !this.isConnectionExpired(conn));
|
|
184
|
+
if (!connection && pool.length < this.config.maxPoolSize) {
|
|
185
|
+
connection = this.createConnection(databaseName);
|
|
186
|
+
pool.push(connection);
|
|
187
|
+
}
|
|
188
|
+
if (!connection) {
|
|
189
|
+
// Wait for an available connection
|
|
190
|
+
connection = await this.waitForAvailableConnection(pool, config.timeout);
|
|
191
|
+
}
|
|
192
|
+
if (!connection) {
|
|
193
|
+
throw new Error('No available database connections');
|
|
194
|
+
}
|
|
195
|
+
connection.inUse = true;
|
|
196
|
+
connection.lastUsed = Date.now();
|
|
197
|
+
this.activeConnections++;
|
|
198
|
+
return connection;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Release a connection back to the pool
|
|
203
|
+
*/
|
|
204
|
+
releaseConnection(databaseName, connection) {
|
|
205
|
+
connection.inUse = false;
|
|
206
|
+
connection.lastUsed = Date.now();
|
|
207
|
+
this.activeConnections--;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a new database connection
|
|
212
|
+
*/
|
|
213
|
+
createConnection(databaseName) {
|
|
214
|
+
return {
|
|
215
|
+
id: `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
216
|
+
databaseName,
|
|
217
|
+
created: Date.now(),
|
|
218
|
+
lastUsed: Date.now(),
|
|
219
|
+
inUse: false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Wait for an available connection
|
|
225
|
+
*/
|
|
226
|
+
async waitForAvailableConnection(pool, timeout) {
|
|
227
|
+
const startTime = Date.now();
|
|
228
|
+
while (Date.now() - startTime < timeout) {
|
|
229
|
+
const connection = pool.find(conn => !conn.inUse && !this.isConnectionExpired(conn));
|
|
230
|
+
if (connection) {
|
|
231
|
+
return connection;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Wait a bit before checking again
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if a connection is expired
|
|
242
|
+
*/
|
|
243
|
+
isConnectionExpired(connection) {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
return now - connection.lastUsed > this.config.connectionIdleTimeout;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Handle connection errors
|
|
250
|
+
*/
|
|
251
|
+
async handleConnectionError(databaseName, error) {
|
|
252
|
+
// Clean up any bad connections from the pool
|
|
253
|
+
const poolKey = databaseName;
|
|
254
|
+
const pool = this.connectionPool.get(poolKey);
|
|
255
|
+
if (pool) {
|
|
256
|
+
const validConnections = pool.filter(conn => !this.isConnectionExpired(conn));
|
|
257
|
+
this.connectionPool.set(poolKey, validConnections);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Could implement connection health checks here
|
|
261
|
+
console.warn(`Database connection error for ${databaseName}:`, error.message);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if error is connection-related
|
|
266
|
+
*/
|
|
267
|
+
isConnectionError(error) {
|
|
268
|
+
const connectionErrorPatterns = ['connection', 'timeout', 'network', 'ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT'];
|
|
269
|
+
const errorMessage = error.message.toLowerCase();
|
|
270
|
+
return connectionErrorPatterns.some(pattern => errorMessage.includes(pattern));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Execute a function with timeout
|
|
275
|
+
*/
|
|
276
|
+
async executeWithTimeout(fn, timeout) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const timeoutId = setTimeout(() => {
|
|
279
|
+
reject(new Error(`Operation timed out after ${timeout}ms`));
|
|
280
|
+
}, timeout);
|
|
281
|
+
fn().then(result => {
|
|
282
|
+
clearTimeout(timeoutId);
|
|
283
|
+
resolve(result);
|
|
284
|
+
}).catch(error => {
|
|
285
|
+
clearTimeout(timeoutId);
|
|
286
|
+
reject(error);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get connection pool statistics
|
|
293
|
+
*/
|
|
294
|
+
getPoolStats(databaseName) {
|
|
295
|
+
const pool = this.connectionPool.get(databaseName) || [];
|
|
296
|
+
return {
|
|
297
|
+
databaseName,
|
|
298
|
+
totalConnections: pool.length,
|
|
299
|
+
activeConnections: pool.filter(conn => conn.inUse).length,
|
|
300
|
+
idleConnections: pool.filter(conn => !conn.inUse).length,
|
|
301
|
+
expiredConnections: pool.filter(conn => this.isConnectionExpired(conn)).length,
|
|
302
|
+
globalActiveConnections: this.activeConnections
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get all pool statistics
|
|
308
|
+
*/
|
|
309
|
+
getAllPoolStats() {
|
|
310
|
+
const stats = {};
|
|
311
|
+
for (const [databaseName, pool] of this.connectionPool) {
|
|
312
|
+
stats[databaseName] = this.getPoolStats(databaseName);
|
|
313
|
+
}
|
|
314
|
+
return stats;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Clean up expired connections
|
|
319
|
+
*/
|
|
320
|
+
cleanupExpiredConnections() {
|
|
321
|
+
for (const [databaseName, pool] of this.connectionPool) {
|
|
322
|
+
const validConnections = pool.filter(conn => !this.isConnectionExpired(conn));
|
|
323
|
+
this.connectionPool.set(databaseName, validConnections);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Close all connections
|
|
329
|
+
*/
|
|
330
|
+
async closeAllConnections() {
|
|
331
|
+
for (const [databaseName, pool] of this.connectionPool) {
|
|
332
|
+
// In a real implementation, you'd close actual connections
|
|
333
|
+
pool.length = 0;
|
|
334
|
+
}
|
|
335
|
+
this.connectionPool.clear();
|
|
336
|
+
this.activeConnections = 0;
|
|
337
|
+
}
|
|
338
|
+
}
|