- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
/**
|
|
* Validate API Contract: Backend ↔ Frontend
|
|
*
|
|
* @description Valida que routes.constants.ts (Backend) y api-endpoints.ts (Frontend) coincidan.
|
|
* @usage npm run validate:api-contract
|
|
*
|
|
* IMPORTANTE:
|
|
* - Ejecutar antes de merge
|
|
* - Integrado en CI/CD
|
|
* - Backend y Frontend deben tener mismas rutas
|
|
*
|
|
* @see /docs/03-desarrollo/CONSTANTS-ARCHITECTURE.md
|
|
*/
|
|
|
|
import { API_ROUTES } from '../../backend/src/shared/constants/routes.constants';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
/**
|
|
* Aplanar objeto de rutas a array de strings
|
|
*/
|
|
function flattenRoutes(obj: any, prefix = ''): string[] {
|
|
let routes: string[] = [];
|
|
|
|
for (const key in obj) {
|
|
const value = obj[key];
|
|
|
|
if (typeof value === 'string') {
|
|
routes.push(value);
|
|
} else if (typeof value === 'function') {
|
|
// Ejecutar función con placeholders
|
|
try {
|
|
const result = value(':id', ':moduleId', ':userId'); // Pasar múltiples params
|
|
routes.push(result);
|
|
} catch {
|
|
// Si falla, intentar con diferentes arities
|
|
try {
|
|
routes.push(value(':id'));
|
|
} catch {}
|
|
}
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
routes = routes.concat(flattenRoutes(value, prefix));
|
|
}
|
|
}
|
|
|
|
return routes;
|
|
}
|
|
|
|
/**
|
|
* Extraer rutas de Frontend (parsing del archivo)
|
|
*/
|
|
function extractFrontendRoutes(): string[] {
|
|
const frontendFile = path.resolve(__dirname, '../../frontend/src/shared/constants/api-endpoints.ts');
|
|
|
|
if (!fs.existsSync(frontendFile)) {
|
|
console.error('❌ No existe Frontend api-endpoints.ts');
|
|
process.exit(1);
|
|
}
|
|
|
|
const content = fs.readFileSync(frontendFile, 'utf-8');
|
|
|
|
// Regex para extraer rutas (mejorado)
|
|
const regex = /['"]\$\{API_BASE_URL\}([^'"]+)['"]/g;
|
|
const matches = content.matchAll(regex);
|
|
|
|
const routes: string[] = [];
|
|
for (const match of matches) {
|
|
if (match[1]) {
|
|
routes.push(match[1]);
|
|
}
|
|
}
|
|
|
|
return routes;
|
|
}
|
|
|
|
/**
|
|
* Normalizar ruta (remover placeholders)
|
|
*/
|
|
function normalizeRoute(route: string): string {
|
|
return route
|
|
.replace(/:\w+/g, ':id') // :userId → :id
|
|
.replace(/\$\{[^}]+\}/g, ':id'); // ${id} → :id
|
|
}
|
|
|
|
async function validateApiContract() {
|
|
console.log('🔍 Validando API Contract: Backend ↔ Frontend...\n');
|
|
|
|
try {
|
|
// 1. Extraer rutas Backend
|
|
const backendRoutes = flattenRoutes(API_ROUTES);
|
|
const backendNormalized = backendRoutes.map(normalizeRoute);
|
|
|
|
console.log(`📋 Backend: ${backendRoutes.length} rutas encontradas`);
|
|
|
|
// 2. Extraer rutas Frontend
|
|
const frontendRoutes = extractFrontendRoutes();
|
|
const frontendNormalized = frontendRoutes.map(normalizeRoute);
|
|
|
|
console.log(`📋 Frontend: ${frontendRoutes.length} rutas encontradas\n`);
|
|
|
|
// 3. Comparar
|
|
const backendOnly = backendNormalized.filter(r => !frontendNormalized.includes(r));
|
|
const frontendOnly = frontendNormalized.filter(r => !backendNormalized.includes(r));
|
|
|
|
// 4. Reportar discrepancias
|
|
if (backendOnly.length > 0) {
|
|
console.error('❌ Rutas en BACKEND pero NO en FRONTEND:');
|
|
backendOnly.forEach((route, index) => {
|
|
const original = backendRoutes[backendNormalized.indexOf(route)];
|
|
console.error(` ${index + 1}. ${original}`);
|
|
});
|
|
console.error('');
|
|
}
|
|
|
|
if (frontendOnly.length > 0) {
|
|
console.error('❌ Rutas en FRONTEND pero NO en BACKEND:');
|
|
frontendOnly.forEach((route, index) => {
|
|
const original = frontendRoutes[frontendNormalized.indexOf(route)];
|
|
console.error(` ${index + 1}. ${original}`);
|
|
});
|
|
console.error('');
|
|
}
|
|
|
|
// 5. Resultado final
|
|
if (backendOnly.length > 0 || frontendOnly.length > 0) {
|
|
console.error('═'.repeat(80));
|
|
console.error('RESUMEN:');
|
|
console.error(` Backend Only: ${backendOnly.length}`);
|
|
console.error(` Frontend Only: ${frontendOnly.length}`);
|
|
console.error(` TOTAL DISCREPANCIAS: ${backendOnly.length + frontendOnly.length}`);
|
|
console.error('═'.repeat(80));
|
|
console.error('\n❌ FALLÓ: Existen discrepancias en el API Contract.\n');
|
|
console.error('📋 ACCIÓN REQUERIDA:');
|
|
console.error('1. Revisar rutas faltantes arriba');
|
|
console.error('2. Sincronizar Backend routes.constants.ts y Frontend api-endpoints.ts');
|
|
console.error('3. Ejecutar: npm run validate:api-contract nuevamente\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('✅ ¡EXCELENTE! API Contract validado: Backend ↔ Frontend sincronizados.\n');
|
|
console.log(` ${backendRoutes.length} rutas verificadas sin discrepancias.`);
|
|
process.exit(0);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error al validar API Contract:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
validateApiContract();
|