@tamyla/clodo-framework 2.0.20 → 3.0.3

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