refactor: restructure API routes from v1/v2/v3 to domain-based (skydive/cms/herowars)

This commit is contained in:
2026-04-24 00:47:58 +02:00
parent b99842f458
commit 9f73ed07ba
36 changed files with 2416 additions and 50 deletions
+481
View File
@@ -0,0 +1,481 @@
#!/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();