#!/usr/bin/env node /** * Migration Script: Restructure API from v1/v2/v3 to Domain-Based (skydive/cms/herowars) * * This script automates the transformation: * - /api/v1/* → /api/skydive/* (Skydive domain) * - /api/v2/* → /api/cms/* (CMS domain) * - /api/v3/* → /api/herowars/* (Hero Wars domain) * * Usage: node scripts/migrate-to-domain-structure.js [--dry-run] [--skip-git] */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Colors for console output const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m', blue: '\x1b[34m', }; const log = { info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`), success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`), warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`), section: (msg) => console.log(`\n${colors.blue}━━━ ${msg} ━━━${colors.reset}\n`), }; // Parse command line arguments const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const skipGit = args.includes('--skip-git'); const projectRoot = path.resolve(__dirname, '..'); const routesDir = path.join(projectRoot, 'src/routes/api'); // Domain mapping const domainMap = { v1: { domain: 'skydive', routes: { '/applications': 'applications', '/aeronefs': 'aeronefs', '/canopies': 'canopies', '/dropzones': 'dropzones', '/jumps': 'jumps', '/pages': 'pages', '/profiles': 'profiles', '/qcm': 'qcm', }, }, v2: { domain: 'cms', routes: { '/user': 'user.routes', '/articles': 'articles.routes', '/comments': 'comments.routes', '/product': 'product.routes', '/products': 'products.routes', '/profiles': 'profiles.routes', '/tags': 'tags.routes', }, // herowars will be moved to v3 }, v3: { domain: 'herowars', routes: { '/clans': 'clans.routes', '/members': 'members.routes', '/herowars': 'herowars.routes', // to be moved from v2 }, }, }; /** * Phase 1: Create new domain directories */ function createDomainDirectories() { log.section('Phase 1: Creating domain directories'); Object.values(domainMap).forEach(({ domain }) => { const domainPath = path.join(routesDir, domain); if (!fs.existsSync(domainPath)) { if (!dryRun) { fs.mkdirSync(domainPath, { recursive: true }); } log.success(`Created directory: ${domain}/`); } else { log.warn(`Directory already exists: ${domain}/`); } }); } /** * Phase 2: Copy/move route files to new domain directories */ function migrateRouteFiles() { log.section('Phase 2: Migrating route files'); // Handle v1 → skydive and v2 → cms ['v1', 'v2', 'v3'].forEach((version) => { const { domain, routes } = domainMap[version]; const oldVersionDir = path.join(routesDir, version); const newDomainDir = path.join(routesDir, domain); log.info(`\nMigrating ${version} → ${domain}:`); Object.entries(routes).forEach(([routePath, fileName]) => { const extensions = ['', '.js']; let fileFound = false; for (const ext of extensions) { const oldFilePath = path.join(oldVersionDir, fileName + ext); if (fs.existsSync(oldFilePath)) { const newFilePath = path.join(newDomainDir, path.basename(oldFilePath)); if (!dryRun) { const content = fs.readFileSync(oldFilePath, 'utf8'); fs.writeFileSync(newFilePath, content); } log.success(` Copied: ${fileName + ext} → ${domain}/`); fileFound = true; break; } } if (!fileFound) { log.warn(` File not found: ${fileName}(.js)?`); } }); }); // Handle herowars.routes.js move from v2 to v3 const herowarsFile = path.join(routesDir, 'v2', 'herowars.routes.js'); if (fs.existsSync(herowarsFile)) { const newPath = path.join(routesDir, 'herowars', 'herowars.routes.js'); if (!dryRun) { const content = fs.readFileSync(herowarsFile, 'utf8'); fs.writeFileSync(newPath, content); } log.success(` Moved herowars.routes.js from v2 → herowars/`); } } /** * Phase 3: Create new index.js files for each domain */ function createIndexFiles() { log.section('Phase 3: Creating index files for domains'); Object.entries(domainMap).forEach(([version, { domain, routes }]) => { const indexPath = path.join(routesDir, domain, 'index.js'); // Filter out herowars from v2 routes if it's the cms domain const filteredRoutes = domain === 'cms' ? Object.entries(routes).filter(([, fileName]) => !fileName.includes('herowars')) : Object.entries(routes); const routeImports = filteredRoutes .map(([routePath, fileName]) => `routes.use('${routePath}', require('./${fileName}'));`) .join('\n'); const indexContent = `var routes = require('express').Router(); ${routeImports} routes.use(function (err, req, res, next) { if (err.name === 'ValidationError') { return res.status(422).json({ errors: Object.keys(err.errors).reduce(function (errors, key) { errors[key] = err.errors[key].message; return errors; }, {}) }); } return next(err); }); module.exports = routes; `; if (!dryRun) { fs.writeFileSync(indexPath, indexContent); } log.success(`Created: ${domain}/index.js`); }); } /** * Phase 4: Update src/routes/api/index.js */ function updateApiIndex() { log.section('Phase 4: Updating src/routes/api/index.js'); const apiIndexPath = path.join(routesDir, 'index.js'); const newContent = `var routes = require('express').Router(); routes.use('/skydive', require('./skydive')); routes.use('/cms', require('./cms')); routes.use('/herowars', require('./herowars')); module.exports = routes; `; if (!dryRun) { fs.writeFileSync(apiIndexPath, newContent); } log.success(`Updated routes/api/index.js`); } /** * Phase 5: Update app.js for Swagger documentation */ function updateSwaggerPaths() { log.section('Phase 5: Creating Swagger path update instructions'); const swaggerUpdateInstructions = ` # Swagger Path Updates Required The following Swagger YAML files need path updates: ## skydive.yaml - Change: /api/v1/ → /api/skydive/ - Affected routes: /jumps, /aeronefs, /dropzones, /canopies, /pages, /qcm, /applications ## cms.yaml - Change: /api/v2/ → /api/cms/ - Affected routes: /articles, /comments, /products, /tags, /profiles, /user ## herowars.yaml - Change: /api/v3/ → /api/herowars/ - Affected routes: /clans, /members - Note: Add herowars endpoint from former /api/v2/herowars Update in: src/config/swagger.js if paths are hardcoded. `; if (!dryRun) { fs.writeFileSync(path.join(projectRoot, 'MIGRATION_SWAGGER_TODO.md'), swaggerUpdateInstructions); } log.success(`Created MIGRATION_SWAGGER_TODO.md`); } /** * Phase 6: Create Swagger YAML files for new structure */ function createSwaggerYamlFiles() { log.section('Phase 6: Updating Swagger YAML structure'); const swaggerDir = path.join(projectRoot, 'src/swagger'); // Read existing YAML files and update paths const updateSwaggerFile = (oldName, newName, newBasePath) => { const oldPath = path.join(swaggerDir, oldName); if (fs.existsSync(oldPath)) { let content = fs.readFileSync(oldPath, 'utf8'); // Update paths in YAML content = content.replace(/\/api\/v\d+/g, `/api/${newBasePath}`); const newPath = path.join(swaggerDir, newName); if (!dryRun) { fs.writeFileSync(newPath, content); } log.success(`Created/Updated: ${newName}`); } }; // Note: This depends on what swagger files exist log.warn(`Manual Swagger file updates may be needed`); } /** * Phase 7: Git operations */ function gitOperations() { log.section('Phase 7: Git operations'); if (skipGit) { log.warn('Skipping git operations (--skip-git flag)'); return; } try { if (!dryRun) { // Stage new files execSync('git add -A', { cwd: projectRoot, stdio: 'pipe' }); log.success(`Staged changes with git`); // Create commit const commitMessage = 'refactor: restructure API routes from v1/v2/v3 to domain-based (skydive/cms/herowars)'; execSync(`git commit -m "${commitMessage}"`, { cwd: projectRoot, stdio: 'pipe' }); log.success(`Created git commit`); } else { log.info(`Would stage and commit changes to git`); } } catch (err) { log.error(`Git operation failed: ${err.message}`); } } /** * Phase 8: Post-migration validation */ function validateMigration() { log.section('Phase 8: Validation'); Object.entries(domainMap).forEach(([version, { domain, routes }]) => { const domainPath = path.join(routesDir, domain); const indexPath = path.join(domainPath, 'index.js'); // Check domain directory exists if (fs.existsSync(domainPath)) { log.success(`✓ Domain directory exists: ${domain}/`); } else { log.error(`✗ Domain directory missing: ${domain}/`); } // Check index.js exists if (fs.existsSync(indexPath)) { log.success(`✓ Index file exists: ${domain}/index.js`); } else { log.error(`✗ Index file missing: ${domain}/index.js`); } // Check route files const routeFiles = Object.values(routes); routeFiles.forEach((fileName) => { const filePath = path.join(domainPath, fileName + '.js'); if (fs.existsSync(filePath)) { log.success(`✓ Route file exists: ${domain}/${fileName}.js`); } else { log.warn(`⚠ Route file not found: ${domain}/${fileName}.js`); } }); }); // Check main api index const apiIndexPath = path.join(routesDir, 'index.js'); if (fs.existsSync(apiIndexPath)) { log.success(`✓ Main API index exists: routes/api/index.js`); } else { log.error(`✗ Main API index missing: routes/api/index.js`); } } /** * Phase 9: Generate migration report */ function generateReport() { log.section('Phase 9: Migration Summary'); const report = ` ═══════════════════════════════════════════════════════════════════════════════ API STRUCTURE MIGRATION REPORT ═══════════════════════════════════════════════════════════════════════════════ MIGRATION: v1/v2/v3 → Domain-Based (skydive/cms/herowars) OLD STRUCTURE: └── routes/api/ ├── v1/ (Skydive routes) ├── v2/ (CMS + misc routes) └── v3/ (Hero Wars routes) NEW STRUCTURE: └── routes/api/ ├── skydive/ ├── cms/ └── herowars/ ROUTE MAPPINGS: ✓ /api/v1/* → /api/skydive/* - applications, aeronefs, canopies, dropzones, jumps, pages, profiles, qcm ✓ /api/v2/* → /api/cms/* - user, articles, comments, product, products, profiles, tags ✓ /api/v3/* → /api/herowars/* - clans, members, herowars (moved from v2) FILES CREATED/UPDATED: ✓ src/routes/api/skydive/index.js ✓ src/routes/api/cms/index.js ✓ src/routes/api/herowars/index.js ✓ src/routes/api/index.js ✓ All route files copied to appropriate domains MANUAL STEPS REQUIRED: 1. Update Swagger/OpenAPI documentation: - Review MIGRATION_SWAGGER_TODO.md - Update path prefixes in src/swagger/*.yaml files - Update paths in src/config/swagger.js if needed 2. Update app.js (if direct version references exist): - Search for 'v1', 'v2', 'v3' strings - Replace with domain names where appropriate 3. Update tests and client calls: - Replace /api/v1/ with /api/skydive/ - Replace /api/v2/ with /api/cms/ - Replace /api/v3/ with /api/herowars/ 4. Update any documentation: - README.md API endpoints section - API documentation and guides 5. Test all endpoints: - Verify each domain routes work correctly - Check authentication/authorization still functions - Validate error handling OPTIONAL: Old v1/v2/v3 directories can be removed after verification: rm -rf src/routes/api/v1 rm -rf src/routes/api/v2 rm -rf src/routes/api/v3 ═══════════════════════════════════════════════════════════════════════════════ `; console.log(report); if (!dryRun) { fs.writeFileSync(path.join(projectRoot, 'MIGRATION_REPORT.md'), report); } } /** * Main execution */ function main() { console.clear(); console.log(` ${colors.cyan}╔══════════════════════════════════════════════════════════╗ ║ API STRUCTURE MIGRATION: v1/v2/v3 → Domain-Based ║ ║ (skydive / cms / herowars) ║ ╚══════════════════════════════════════════════════════════════╝${colors.reset} `); if (dryRun) { log.warn(`DRY RUN MODE - No changes will be made`); } try { createDomainDirectories(); migrateRouteFiles(); createIndexFiles(); updateApiIndex(); updateSwaggerPaths(); createSwaggerYamlFiles(); gitOperations(); validateMigration(); generateReport(); log.success(`\n✓ Migration script completed successfully!\n`); if (dryRun) { log.info(`To apply changes, run: node scripts/migrate-to-domain-structure.js\n`); } else { log.info(`To rollback changes, run: git reset --soft HEAD~1 && git restore .\n`); } } catch (error) { log.error(`Migration failed: ${error.message}`); process.exit(1); } } main();