refactor: restructure API routes from v1/v2/v3 to domain-based (skydive/cms/herowars)
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user