482 lines
14 KiB
JavaScript
482 lines
14 KiB
JavaScript
#!/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();
|