@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,632 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Database Orchestrator Module
|
|
5
|
+
* Enterprise-grade database management across multiple environments
|
|
6
|
+
*
|
|
7
|
+
* Extracted from manage-migrations.ps1 and manage-data-cleanup.ps1 with enhancements
|
|
8
|
+
*/
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import { readFile, writeFile, access, mkdir, readdir, stat, appendFile } from 'fs/promises';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { promisify } from 'util';
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Advanced Database Orchestrator
|
|
21
|
+
* Manages database operations across development, staging, and production environments
|
|
22
|
+
*/
|
|
23
|
+
export class DatabaseOrchestrator {
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
// Detect if running as a dependency (in node_modules)
|
|
26
|
+
const isDependency = __dirname.includes('node_modules');
|
|
27
|
+
this.projectRoot = options.projectRoot || (isDependency ? null : join(__dirname, '..', '..'));
|
|
28
|
+
this.dryRun = options.dryRun || false;
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.config = null;
|
|
31
|
+
|
|
32
|
+
// Environment configurations
|
|
33
|
+
this.environments = {
|
|
34
|
+
development: {
|
|
35
|
+
name: 'development',
|
|
36
|
+
isRemote: false,
|
|
37
|
+
description: 'Local development database',
|
|
38
|
+
defaultDatabase: 'local-db'
|
|
39
|
+
},
|
|
40
|
+
staging: {
|
|
41
|
+
name: 'staging',
|
|
42
|
+
isRemote: true,
|
|
43
|
+
description: 'Staging environment database',
|
|
44
|
+
requiresConfirmation: true
|
|
45
|
+
},
|
|
46
|
+
production: {
|
|
47
|
+
name: 'production',
|
|
48
|
+
isRemote: true,
|
|
49
|
+
description: 'PRODUCTION environment database - USE WITH EXTREME CAUTION',
|
|
50
|
+
requiresConfirmation: true,
|
|
51
|
+
requiresBackup: true
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Backup and audit configuration - only set paths if not running as dependency
|
|
56
|
+
if (this.projectRoot) {
|
|
57
|
+
this.backupPaths = {
|
|
58
|
+
root: join(this.projectRoot, 'backups', 'database'),
|
|
59
|
+
migrations: join(this.projectRoot, 'backups', 'migrations'),
|
|
60
|
+
audit: join(this.projectRoot, 'logs', 'database-audit.log')
|
|
61
|
+
};
|
|
62
|
+
this.migrationPaths = {
|
|
63
|
+
root: join(this.projectRoot, 'migrations'),
|
|
64
|
+
templates: join(this.projectRoot, 'migration-templates')
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
// When used as dependency, disable file-based logging
|
|
68
|
+
this.backupPaths = null;
|
|
69
|
+
this.migrationPaths = null;
|
|
70
|
+
console.log('đŚ Running as dependency - file logging disabled');
|
|
71
|
+
}
|
|
72
|
+
this.initializeOrchestrator();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize database orchestrator
|
|
77
|
+
*/
|
|
78
|
+
initializeOrchestrator() {
|
|
79
|
+
console.log('đď¸ Database Orchestrator v1.0');
|
|
80
|
+
console.log('==============================');
|
|
81
|
+
if (this.projectRoot) {
|
|
82
|
+
console.log(`đ Project Root: ${this.projectRoot}`);
|
|
83
|
+
} else {
|
|
84
|
+
console.log('đŚ Running as dependency - limited functionality');
|
|
85
|
+
}
|
|
86
|
+
console.log(`đ Mode: ${this.dryRun ? 'DRY RUN' : 'LIVE OPERATIONS'}`);
|
|
87
|
+
console.log(`đ Retry Attempts: ${this.config ? this.config.retryAttempts : 3}`);
|
|
88
|
+
console.log('');
|
|
89
|
+
|
|
90
|
+
// Create necessary directories (only if not running as dependency)
|
|
91
|
+
if (this.backupPaths && this.migrationPaths) {
|
|
92
|
+
Object.values(this.backupPaths).forEach(path => {
|
|
93
|
+
if (!path.endsWith('.log')) {
|
|
94
|
+
this.ensureDirectory(path);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.ensureDirectory(this.migrationPaths.root);
|
|
98
|
+
this.ensureDirectory(dirname(this.backupPaths.audit));
|
|
99
|
+
}
|
|
100
|
+
this.logAuditEvent('ORCHESTRATOR_INITIALIZED', 'SYSTEM', {
|
|
101
|
+
mode: this.dryRun ? 'DRY_RUN' : 'LIVE',
|
|
102
|
+
environments: Object.keys(this.environments)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Initialize with framework configuration
|
|
108
|
+
*/
|
|
109
|
+
async initialize() {
|
|
110
|
+
// Import framework config for consistent timing and database settings
|
|
111
|
+
const {
|
|
112
|
+
frameworkConfig
|
|
113
|
+
} = await import('../../../src/utils/framework-config.js');
|
|
114
|
+
const timing = frameworkConfig.getTiming();
|
|
115
|
+
const database = frameworkConfig.getDatabaseConfig();
|
|
116
|
+
this.config = {
|
|
117
|
+
retryAttempts: this.options.retryAttempts || timing.retryAttempts,
|
|
118
|
+
retryDelay: this.options.retryDelay || timing.retryDelay,
|
|
119
|
+
executionTimeout: this.options.executionTimeout || database.executionTimeout,
|
|
120
|
+
...this.options
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply migrations across multiple environments with coordination
|
|
126
|
+
* @param {Object} options - Migration options
|
|
127
|
+
* @returns {Promise<Object>} Migration results
|
|
128
|
+
*/
|
|
129
|
+
async applyMigrationsAcrossEnvironments(options = {}) {
|
|
130
|
+
const {
|
|
131
|
+
environments = ['development', 'staging', 'production'],
|
|
132
|
+
domainConfigs = [],
|
|
133
|
+
skipBackup = false,
|
|
134
|
+
continueOnError = false
|
|
135
|
+
} = options;
|
|
136
|
+
console.log('đ Cross-Environment Migration Orchestration');
|
|
137
|
+
console.log('===========================================');
|
|
138
|
+
console.log(`đ Environments: ${environments.join(', ')}`);
|
|
139
|
+
console.log(`đ Domains: ${domainConfigs.length} configured`);
|
|
140
|
+
console.log('');
|
|
141
|
+
const results = {
|
|
142
|
+
orchestrationId: this.generateOrchestrationId(),
|
|
143
|
+
environments: {},
|
|
144
|
+
summary: {
|
|
145
|
+
total: 0,
|
|
146
|
+
successful: 0,
|
|
147
|
+
failed: 0,
|
|
148
|
+
skipped: 0
|
|
149
|
+
},
|
|
150
|
+
startTime: new Date()
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
for (const env of environments) {
|
|
154
|
+
if (!this.environments[env]) {
|
|
155
|
+
console.log(`â ď¸ Unknown environment: ${env}, skipping`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
console.log(`\nđ Processing ${env} environment...`);
|
|
159
|
+
results.summary.total++;
|
|
160
|
+
try {
|
|
161
|
+
// Create backup if required
|
|
162
|
+
if (this.environments[env].requiresBackup && !skipBackup) {
|
|
163
|
+
await this.createEnvironmentBackup(env, domainConfigs);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Apply migrations for environment
|
|
167
|
+
const envResult = await this.applyEnvironmentMigrations(env, domainConfigs, options);
|
|
168
|
+
results.environments[env] = {
|
|
169
|
+
status: 'completed',
|
|
170
|
+
...envResult
|
|
171
|
+
};
|
|
172
|
+
results.summary.successful++;
|
|
173
|
+
console.log(`â
${env} environment completed successfully`);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`â ${env} environment failed: ${error.message}`);
|
|
176
|
+
results.environments[env] = {
|
|
177
|
+
status: 'failed',
|
|
178
|
+
error: error.message,
|
|
179
|
+
timestamp: new Date()
|
|
180
|
+
};
|
|
181
|
+
results.summary.failed++;
|
|
182
|
+
if (!continueOnError) {
|
|
183
|
+
throw new Error(`Migration failed in ${env} environment: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
results.endTime = new Date();
|
|
188
|
+
results.summary.duration = (results.endTime - results.startTime) / 1000;
|
|
189
|
+
this.logAuditEvent('MIGRATION_ORCHESTRATION_COMPLETED', 'ALL', results.summary);
|
|
190
|
+
console.log('\nđ MIGRATION ORCHESTRATION SUMMARY');
|
|
191
|
+
console.log('==================================');
|
|
192
|
+
console.log(`â
Successful: ${results.summary.successful}`);
|
|
193
|
+
console.log(`â Failed: ${results.summary.failed}`);
|
|
194
|
+
console.log(`â¸ď¸ Skipped: ${results.summary.skipped}`);
|
|
195
|
+
console.log(`âąď¸ Duration: ${results.summary.duration.toFixed(1)}s`);
|
|
196
|
+
return results;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.logAuditEvent('MIGRATION_ORCHESTRATION_FAILED', 'ALL', {
|
|
199
|
+
error: error.message
|
|
200
|
+
});
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Apply migrations to specific environment
|
|
207
|
+
* @param {string} environment - Environment name
|
|
208
|
+
* @param {Array} domainConfigs - Domain configurations
|
|
209
|
+
* @param {Object} options - Migration options
|
|
210
|
+
* @returns {Promise<Object>} Environment migration result
|
|
211
|
+
*/
|
|
212
|
+
async applyEnvironmentMigrations(environment, domainConfigs, options = {}) {
|
|
213
|
+
const envConfig = this.environments[environment];
|
|
214
|
+
const results = {
|
|
215
|
+
environment,
|
|
216
|
+
databases: {},
|
|
217
|
+
migrationsApplied: 0,
|
|
218
|
+
startTime: new Date()
|
|
219
|
+
};
|
|
220
|
+
console.log(` đ Environment: ${envConfig.description}`);
|
|
221
|
+
console.log(` đ Remote: ${envConfig.isRemote ? 'Yes' : 'No'}`);
|
|
222
|
+
|
|
223
|
+
// Process each domain's databases
|
|
224
|
+
if (domainConfigs.length > 0) {
|
|
225
|
+
for (const domainConfig of domainConfigs) {
|
|
226
|
+
const dbConfig = domainConfig.databases?.[environment];
|
|
227
|
+
if (!dbConfig) {
|
|
228
|
+
console.log(` â ď¸ No ${environment} database config for ${domainConfig.name}`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const dbResult = await this.applyDatabaseMigrations(dbConfig.name, environment, envConfig.isRemote);
|
|
233
|
+
results.databases[dbConfig.name] = dbResult;
|
|
234
|
+
results.migrationsApplied += dbResult.migrationsApplied || 0;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(` â Database ${dbConfig.name} migration failed: ${error.message}`);
|
|
237
|
+
results.databases[dbConfig.name] = {
|
|
238
|
+
status: 'failed',
|
|
239
|
+
error: error.message
|
|
240
|
+
};
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// Apply to default database
|
|
246
|
+
const defaultDb = envConfig.defaultDatabase || 'default-db';
|
|
247
|
+
const dbResult = await this.applyDatabaseMigrations(defaultDb, environment, envConfig.isRemote);
|
|
248
|
+
results.databases[defaultDb] = dbResult;
|
|
249
|
+
results.migrationsApplied = dbResult.migrationsApplied || 0;
|
|
250
|
+
}
|
|
251
|
+
results.endTime = new Date();
|
|
252
|
+
results.duration = (results.endTime - results.startTime) / 1000;
|
|
253
|
+
this.logAuditEvent('ENVIRONMENT_MIGRATION_COMPLETED', environment, {
|
|
254
|
+
databases: Object.keys(results.databases),
|
|
255
|
+
migrationsApplied: results.migrationsApplied,
|
|
256
|
+
duration: results.duration
|
|
257
|
+
});
|
|
258
|
+
return results;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Apply migrations to specific database
|
|
263
|
+
* @param {string} databaseName - Database name
|
|
264
|
+
* @param {string} environment - Environment
|
|
265
|
+
* @param {boolean} isRemote - Whether database is remote
|
|
266
|
+
* @returns {Promise<Object>} Database migration result
|
|
267
|
+
*/
|
|
268
|
+
async applyDatabaseMigrations(databaseName, environment, isRemote) {
|
|
269
|
+
console.log(` đď¸ Applying migrations to ${databaseName}...`);
|
|
270
|
+
if (this.dryRun) {
|
|
271
|
+
console.log(` đ DRY RUN: Would apply migrations to ${databaseName}`);
|
|
272
|
+
return {
|
|
273
|
+
status: 'dry-run',
|
|
274
|
+
databaseName,
|
|
275
|
+
environment,
|
|
276
|
+
migrationsApplied: 0
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const command = this.buildMigrationCommand(databaseName, environment, isRemote);
|
|
281
|
+
const output = await this.executeWithRetry(command, 120000); // 2 minute timeout
|
|
282
|
+
|
|
283
|
+
// Parse migration output
|
|
284
|
+
const migrationsApplied = this.parseMigrationOutput(output);
|
|
285
|
+
console.log(` â
Applied ${migrationsApplied} migrations to ${databaseName}`);
|
|
286
|
+
this.logAuditEvent('DATABASE_MIGRATION_APPLIED', environment, {
|
|
287
|
+
databaseName,
|
|
288
|
+
migrationsApplied,
|
|
289
|
+
isRemote
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
status: 'completed',
|
|
293
|
+
databaseName,
|
|
294
|
+
environment,
|
|
295
|
+
migrationsApplied,
|
|
296
|
+
output: output.substring(0, 500) // Truncate for storage
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.logAuditEvent('DATABASE_MIGRATION_FAILED', environment, {
|
|
300
|
+
databaseName,
|
|
301
|
+
error: error.message
|
|
302
|
+
});
|
|
303
|
+
throw new Error(`Migration failed for ${databaseName}: ${error.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create comprehensive backup across environments
|
|
309
|
+
* @param {string} environment - Environment to backup
|
|
310
|
+
* @param {Array} domainConfigs - Domain configurations
|
|
311
|
+
* @returns {Promise<Object>} Backup results
|
|
312
|
+
*/
|
|
313
|
+
async createEnvironmentBackup(environment, domainConfigs) {
|
|
314
|
+
console.log(` đž Creating ${environment} environment backup...`);
|
|
315
|
+
const backupId = this.generateBackupId(environment);
|
|
316
|
+
const backupDir = join(this.backupPaths.root, environment, backupId);
|
|
317
|
+
this.ensureDirectory(backupDir);
|
|
318
|
+
const backupResults = {
|
|
319
|
+
backupId,
|
|
320
|
+
environment,
|
|
321
|
+
backupDir,
|
|
322
|
+
databases: {},
|
|
323
|
+
startTime: new Date()
|
|
324
|
+
};
|
|
325
|
+
try {
|
|
326
|
+
if (domainConfigs.length > 0) {
|
|
327
|
+
for (const domainConfig of domainConfigs) {
|
|
328
|
+
const dbConfig = domainConfig.databases?.[environment];
|
|
329
|
+
if (dbConfig && dbConfig.id) {
|
|
330
|
+
try {
|
|
331
|
+
const dbBackup = await this.createDatabaseBackup(dbConfig.name, environment, backupDir);
|
|
332
|
+
backupResults.databases[dbConfig.name] = dbBackup;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.log(` â ď¸ Backup failed for ${dbConfig.name}: ${error.message}`);
|
|
335
|
+
backupResults.databases[dbConfig.name] = {
|
|
336
|
+
status: 'failed',
|
|
337
|
+
error: error.message
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
backupResults.endTime = new Date();
|
|
344
|
+
backupResults.duration = (backupResults.endTime - backupResults.startTime) / 1000;
|
|
345
|
+
|
|
346
|
+
// Save backup manifest
|
|
347
|
+
const manifestPath = join(backupDir, 'backup-manifest.json');
|
|
348
|
+
await writeFile(manifestPath, JSON.stringify(backupResults, null, 2));
|
|
349
|
+
this.logAuditEvent('ENVIRONMENT_BACKUP_CREATED', environment, {
|
|
350
|
+
backupId,
|
|
351
|
+
databases: Object.keys(backupResults.databases),
|
|
352
|
+
duration: backupResults.duration
|
|
353
|
+
});
|
|
354
|
+
console.log(` â
Environment backup completed: ${backupId}`);
|
|
355
|
+
return backupResults;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
this.logAuditEvent('ENVIRONMENT_BACKUP_FAILED', environment, {
|
|
358
|
+
backupId,
|
|
359
|
+
error: error.message
|
|
360
|
+
});
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Create backup of specific database
|
|
367
|
+
* @param {string} databaseName - Database name
|
|
368
|
+
* @param {string} environment - Environment
|
|
369
|
+
* @param {string} backupDir - Backup directory
|
|
370
|
+
* @returns {Promise<Object>} Database backup result
|
|
371
|
+
*/
|
|
372
|
+
async createDatabaseBackup(databaseName, environment, backupDir) {
|
|
373
|
+
const backupFile = join(backupDir, `${databaseName}-${environment}.sql`);
|
|
374
|
+
if (this.dryRun) {
|
|
375
|
+
console.log(` đ DRY RUN: Would backup ${databaseName} to ${backupFile}`);
|
|
376
|
+
return {
|
|
377
|
+
status: 'dry-run',
|
|
378
|
+
backupFile
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const isRemote = this.environments[environment].isRemote;
|
|
383
|
+
const command = this.buildBackupCommand(databaseName, environment, backupFile, isRemote);
|
|
384
|
+
await this.executeWithRetry(command, 300000); // 5 minute timeout for backups
|
|
385
|
+
|
|
386
|
+
if (existsSync(backupFile)) {
|
|
387
|
+
const stats = await stat(backupFile);
|
|
388
|
+
console.log(` đž Backup created: ${backupFile} (${(stats.size / 1024).toFixed(1)}KB)`);
|
|
389
|
+
return {
|
|
390
|
+
status: 'completed',
|
|
391
|
+
backupFile,
|
|
392
|
+
sizeKB: (stats.size / 1024).toFixed(1),
|
|
393
|
+
timestamp: new Date()
|
|
394
|
+
};
|
|
395
|
+
} else {
|
|
396
|
+
throw new Error('Backup file was not created');
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
throw new Error(`Database backup failed: ${error.message}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Perform safe data cleanup with backup and confirmation
|
|
405
|
+
* @param {Object} options - Cleanup options
|
|
406
|
+
* @returns {Promise<Object>} Cleanup results
|
|
407
|
+
*/
|
|
408
|
+
async performSafeDataCleanup(options = {}) {
|
|
409
|
+
const {
|
|
410
|
+
environment = 'development',
|
|
411
|
+
domainConfigs = [],
|
|
412
|
+
cleanupType = 'partial',
|
|
413
|
+
// 'partial', 'full', 'logs-only'
|
|
414
|
+
skipBackup = false,
|
|
415
|
+
force = false
|
|
416
|
+
} = options;
|
|
417
|
+
console.log('đ§š Safe Data Cleanup Operation');
|
|
418
|
+
console.log('==============================');
|
|
419
|
+
console.log(`đ Environment: ${environment}`);
|
|
420
|
+
console.log(`đ§˝ Cleanup Type: ${cleanupType}`);
|
|
421
|
+
console.log(`đž Skip Backup: ${skipBackup}`);
|
|
422
|
+
console.log('');
|
|
423
|
+
const envConfig = this.environments[environment];
|
|
424
|
+
if (!envConfig) {
|
|
425
|
+
throw new Error(`Unknown environment: ${environment}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Safety confirmation for production
|
|
429
|
+
if (environment === 'production' && !force) {
|
|
430
|
+
const confirmed = await this.confirmDangerousOperation(`${cleanupType} data cleanup in PRODUCTION`, 'This will permanently delete data and cannot be undone');
|
|
431
|
+
if (!confirmed) {
|
|
432
|
+
console.log('â Operation cancelled by user');
|
|
433
|
+
return {
|
|
434
|
+
status: 'cancelled',
|
|
435
|
+
reason: 'User declined confirmation'
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const cleanupResults = {
|
|
440
|
+
cleanupId: this.generateCleanupId(environment),
|
|
441
|
+
environment,
|
|
442
|
+
cleanupType,
|
|
443
|
+
operations: {},
|
|
444
|
+
startTime: new Date()
|
|
445
|
+
};
|
|
446
|
+
try {
|
|
447
|
+
// Create backup if required
|
|
448
|
+
if (!skipBackup && envConfig.requiresBackup) {
|
|
449
|
+
const backupResult = await this.createEnvironmentBackup(environment, domainConfigs);
|
|
450
|
+
cleanupResults.backup = backupResult;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Perform cleanup operations
|
|
454
|
+
for (const domainConfig of domainConfigs) {
|
|
455
|
+
const dbConfig = domainConfig.databases?.[environment];
|
|
456
|
+
if (dbConfig) {
|
|
457
|
+
try {
|
|
458
|
+
const cleanupResult = await this.performDatabaseCleanup(dbConfig.name, environment, cleanupType);
|
|
459
|
+
cleanupResults.operations[dbConfig.name] = cleanupResult;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
console.error(`â Cleanup failed for ${dbConfig.name}: ${error.message}`);
|
|
462
|
+
cleanupResults.operations[dbConfig.name] = {
|
|
463
|
+
status: 'failed',
|
|
464
|
+
error: error.message
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
cleanupResults.endTime = new Date();
|
|
470
|
+
cleanupResults.duration = (cleanupResults.endTime - cleanupResults.startTime) / 1000;
|
|
471
|
+
this.logAuditEvent('DATA_CLEANUP_COMPLETED', environment, {
|
|
472
|
+
cleanupId: cleanupResults.cleanupId,
|
|
473
|
+
cleanupType,
|
|
474
|
+
operations: Object.keys(cleanupResults.operations),
|
|
475
|
+
duration: cleanupResults.duration
|
|
476
|
+
});
|
|
477
|
+
console.log('\nâ
Data cleanup completed successfully');
|
|
478
|
+
return cleanupResults;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
this.logAuditEvent('DATA_CLEANUP_FAILED', environment, {
|
|
481
|
+
cleanupId: cleanupResults.cleanupId,
|
|
482
|
+
error: error.message
|
|
483
|
+
});
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Perform cleanup on specific database
|
|
490
|
+
* @param {string} databaseName - Database name
|
|
491
|
+
* @param {string} environment - Environment
|
|
492
|
+
* @param {string} cleanupType - Type of cleanup
|
|
493
|
+
* @returns {Promise<Object>} Cleanup result
|
|
494
|
+
*/
|
|
495
|
+
async performDatabaseCleanup(databaseName, environment, cleanupType) {
|
|
496
|
+
console.log(` đ§š Cleaning ${databaseName} (${cleanupType})...`);
|
|
497
|
+
if (this.dryRun) {
|
|
498
|
+
console.log(` đ DRY RUN: Would perform ${cleanupType} cleanup on ${databaseName}`);
|
|
499
|
+
return {
|
|
500
|
+
status: 'dry-run',
|
|
501
|
+
cleanupType
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const commands = this.getCleanupCommands(cleanupType, environment);
|
|
505
|
+
let executedCommands = 0;
|
|
506
|
+
try {
|
|
507
|
+
for (const command of commands) {
|
|
508
|
+
const fullCommand = this.buildDatabaseCommand(command, databaseName, environment);
|
|
509
|
+
await this.executeWithRetry(fullCommand, 60000);
|
|
510
|
+
executedCommands++;
|
|
511
|
+
}
|
|
512
|
+
console.log(` â
Cleanup completed: ${executedCommands} operations`);
|
|
513
|
+
return {
|
|
514
|
+
status: 'completed',
|
|
515
|
+
cleanupType,
|
|
516
|
+
operationsExecuted: executedCommands,
|
|
517
|
+
timestamp: new Date()
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
throw new Error(`Database cleanup failed after ${executedCommands} operations: ${error.message}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Command builders and utility methods
|
|
525
|
+
|
|
526
|
+
buildMigrationCommand(databaseName, environment, isRemote) {
|
|
527
|
+
const remoteFlag = isRemote ? '--remote' : '--local';
|
|
528
|
+
return `npx wrangler d1 migrations apply ${databaseName} --env ${environment} ${remoteFlag}`;
|
|
529
|
+
}
|
|
530
|
+
buildBackupCommand(databaseName, environment, backupFile, isRemote) {
|
|
531
|
+
const remoteFlag = isRemote ? '--remote' : '--local';
|
|
532
|
+
return `npx wrangler d1 export ${databaseName} --env ${environment} ${remoteFlag} --output ${backupFile}`;
|
|
533
|
+
}
|
|
534
|
+
buildDatabaseCommand(sqlCommand, databaseName, environment) {
|
|
535
|
+
const envConfig = this.environments[environment];
|
|
536
|
+
const remoteFlag = envConfig.isRemote ? '--remote' : '--local';
|
|
537
|
+
return `npx wrangler d1 execute ${databaseName} --env ${environment} ${remoteFlag} --command "${sqlCommand}"`;
|
|
538
|
+
}
|
|
539
|
+
getCleanupCommands(cleanupType) {
|
|
540
|
+
const commands = {
|
|
541
|
+
'logs-only': ['DELETE FROM logs WHERE created_at < datetime("now", "-30 days");'],
|
|
542
|
+
'partial': ['DELETE FROM logs WHERE created_at < datetime("now", "-7 days");', 'DELETE FROM sessions WHERE expires_at < datetime("now");', 'UPDATE users SET last_cleanup = datetime("now") WHERE last_cleanup IS NULL;'],
|
|
543
|
+
'full': ['DELETE FROM logs;', 'DELETE FROM sessions;', 'DELETE FROM files;', 'DELETE FROM user_profiles;', 'DELETE FROM users;']
|
|
544
|
+
};
|
|
545
|
+
return commands[cleanupType] || commands['partial'];
|
|
546
|
+
}
|
|
547
|
+
parseMigrationOutput(output) {
|
|
548
|
+
// Parse wrangler migration output to count applied migrations
|
|
549
|
+
const matches = output.match(/Applied (\d+) migration/);
|
|
550
|
+
return matches ? parseInt(matches[1]) : 0;
|
|
551
|
+
}
|
|
552
|
+
async executeWithRetry(command, timeout = null) {
|
|
553
|
+
const actualTimeout = timeout || (this.config ? this.config.executionTimeout : 30000);
|
|
554
|
+
for (let attempt = 1; attempt <= (this.config ? this.config.retryAttempts : 3); attempt++) {
|
|
555
|
+
try {
|
|
556
|
+
const {
|
|
557
|
+
stdout
|
|
558
|
+
} = await execAsync(command, {
|
|
559
|
+
encoding: 'utf8',
|
|
560
|
+
timeout: actualTimeout,
|
|
561
|
+
stdio: 'pipe'
|
|
562
|
+
});
|
|
563
|
+
return stdout;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (attempt === this.retryAttempts) {
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
console.log(` â ď¸ Attempt ${attempt} failed, retrying...`);
|
|
569
|
+
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async confirmDangerousOperation(operation, impact) {
|
|
574
|
+
// In a real implementation, this would use a proper input library
|
|
575
|
+
console.log(`\nâ ď¸ DANGER: ${operation}`);
|
|
576
|
+
console.log(`Impact: ${impact}`);
|
|
577
|
+
console.log('Type "YES" to confirm this operation:');
|
|
578
|
+
|
|
579
|
+
// For now, return false to prevent accidental execution
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Utility methods
|
|
584
|
+
|
|
585
|
+
generateOrchestrationId() {
|
|
586
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
587
|
+
return `orchestration-${timestamp}`;
|
|
588
|
+
}
|
|
589
|
+
generateBackupId(environment) {
|
|
590
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
591
|
+
return `backup-${environment}-${timestamp}`;
|
|
592
|
+
}
|
|
593
|
+
generateCleanupId(environment) {
|
|
594
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
595
|
+
return `cleanup-${environment}-${timestamp}`;
|
|
596
|
+
}
|
|
597
|
+
async ensureDirectory(path) {
|
|
598
|
+
try {
|
|
599
|
+
await access(path);
|
|
600
|
+
} catch {
|
|
601
|
+
await mkdir(path, {
|
|
602
|
+
recursive: true
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
logAuditEvent(event, environment, details = {}) {
|
|
607
|
+
const logEntry = {
|
|
608
|
+
timestamp: new Date().toISOString(),
|
|
609
|
+
event,
|
|
610
|
+
environment,
|
|
611
|
+
details,
|
|
612
|
+
user: process.env.USER || process.env.USERNAME || 'system'
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Skip logging if running as dependency (no file access)
|
|
616
|
+
if (!this.backupPaths) {
|
|
617
|
+
console.log(`đ Audit: ${event} (${environment})`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const logLine = JSON.stringify(logEntry) + '\n';
|
|
622
|
+
if (existsSync(this.backupPaths.audit)) {
|
|
623
|
+
appendFile(this.backupPaths.audit, logLine);
|
|
624
|
+
} else {
|
|
625
|
+
writeFile(this.backupPaths.audit, logLine);
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.warn(`â ď¸ Failed to log audit event: ${error.message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export default DatabaseOrchestrator;
|