Files
adastra_api/scripts/migrate-to-domain-structure.js
T

482 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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();