UNPKG

ucg

Version:

Universal CRUD Generator - Express.js plugin and CLI tool for generating complete Node.js REST APIs with database models, controllers, routes, validators, and admin interface. Supports PostgreSQL, MySQL, SQLite with Sequelize, TypeORM, and Knex.js. Develo

527 lines (424 loc) 12.1 kB
const fs = require('fs-extra'); const path = require('path'); class FileManager { constructor(configDir) { this.configDir = configDir; this.historyFile = path.join(configDir, 'generation-history.json'); } async logGeneration(type, result) { const history = await this.getGenerationHistory(); const entry = { id: Date.now().toString(), type, timestamp: new Date().toISOString(), ...result }; history.push(entry); await fs.ensureDir(this.configDir); await fs.writeJson(this.historyFile, history, { spaces: 2 }); return entry; } async getGenerationHistory() { try { if (await fs.pathExists(this.historyFile)) { return await fs.readJson(this.historyFile); } return []; } catch (error) { return []; } } async rollback(generation) { const filesToRemove = generation.files || []; for (const filePath of filesToRemove) { try { if (await fs.pathExists(filePath)) { await fs.remove(filePath); } } catch (error) { console.warn(`Warning: Could not remove file ${filePath}: ${error.message}`); } } // Remove from history const history = await this.getGenerationHistory(); const updatedHistory = history.filter(entry => entry.id !== generation.id); await fs.writeJson(this.historyFile, updatedHistory, { spaces: 2 }); return { removed: filesToRemove.length, files: filesToRemove }; } async getGeneratedModels() { const history = await this.getGenerationHistory(); const modelGenerations = history.filter(entry => entry.type === 'model'); return modelGenerations.map(gen => ({ name: gen.modelName, tableName: gen.tableName, filePath: gen.filePath, timestamp: gen.timestamp })); } async findModelFiles(basePath = process.cwd()) { const modelsPaths = [ path.join(basePath, 'src', 'models'), path.join(basePath, 'models'), path.join(basePath, 'app', 'models') ]; const models = []; for (const modelsPath of modelsPaths) { try { if (await fs.pathExists(modelsPath)) { const files = await fs.readdir(modelsPath); const modelFiles = files.filter(file => file.endsWith('.js') || file.endsWith('.ts') ); for (const file of modelFiles) { const name = path.basename(file, path.extname(file)); models.push({ name, filePath: path.join(modelsPath, file), directory: modelsPath }); } } } catch (error) { // Ignore errors for non-existent directories } } return models; } async ensureDirectoryStructure(basePath) { const directories = [ 'src/models', 'src/controllers', 'src/services', 'src/routes', 'src/validators', 'src/migrations', 'src/tests', 'src/docs/swagger', 'src/config' ]; for (const dir of directories) { await fs.ensureDir(path.join(basePath, dir)); } } async createGitignore(basePath) { const gitignoreContent = `# Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Coverage directory used by tools like istanbul coverage/ # NYC test coverage .nyc_output # Grunt intermediate storage .grunt # Bower dependency directory bower_components # node-waf configuration .lock-wscript # Compiled binary addons build/Release # Dependency directories jspm_packages/ # Optional npm cache directory .npm # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test .env.local .env.production # parcel-bundler cache .cache .parcel-cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # IDE .vscode/ .idea/ *.swp *.swo *~ # OS .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # Logs logs *.log # Database database.sqlite *.db *.sqlite # Generated files dist/ build/ `; const gitignorePath = path.join(basePath, '.gitignore'); if (!await fs.pathExists(gitignorePath)) { await fs.writeFile(gitignorePath, gitignoreContent); } } async createAppTemplate(basePath, dbConfig) { const appPath = path.join(basePath, 'app.js'); if (await fs.pathExists(appPath)) { return; // Don't overwrite existing app.js } const appTemplate = `const express = require('express'); const swaggerUi = require('swagger-ui-express'); const swaggerJsdoc = require('swagger-jsdoc'); const path = require('path'); const app = express(); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Swagger configuration const swaggerOptions = { definition: { openapi: '3.0.0', info: { title: 'UCG Generated API', version: '1.0.0', description: 'API generated by UCG (Universal CRUD Generator)', }, servers: [ { url: 'http://localhost:3000', description: 'Development server', }, ], }, apis: ['./src/docs/swagger/*.js', './src/routes/*.js'], }; const swaggerSpec = swaggerJsdoc(swaggerOptions); app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // Routes // Add your routes here // Example: app.use('/api/users', require('./src/routes/userRoutes')); // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ success: false, message: 'Internal server error', ...(process.env.NODE_ENV === 'development' && { error: err.message }) }); }); // 404 handler app.use((req, res) => { res.status(404).json({ success: false, message: 'Route not found' }); }); const PORT = process.env.PORT || 3000; if (require.main === module) { app.listen(PORT, () => { console.log(\`Server running on http://localhost:\${PORT}\`); console.log(\`Swagger docs available at http://localhost:\${PORT}/docs\`); }); } module.exports = app; `; await fs.writeFile(appPath, appTemplate); } async createPackageJson(basePath, projectName = 'my-ucg-project') { const packagePath = path.join(basePath, 'package.json'); if (await fs.pathExists(packagePath)) { return; // Don't overwrite existing package.json } const packageJson = { name: projectName, version: '1.0.0', description: 'API generated by UCG (Universal CRUD Generator)', main: 'app.js', scripts: { start: 'node app.js', dev: 'nodemon app.js', test: 'jest', 'test:watch': 'jest --watch', lint: 'eslint .', 'lint:fix': 'eslint . --fix' }, dependencies: { express: '^4.18.2', joi: '^17.9.2', 'swagger-ui-express': '^4.6.3', 'swagger-jsdoc': '^6.2.8', pg: '^8.11.1' }, devDependencies: { nodemon: '^3.0.1', jest: '^29.6.1', supertest: '^6.3.3', eslint: '^8.44.0' }, keywords: ['api', 'node', 'express', 'crud', 'ucg'], author: '', license: 'MIT' }; await fs.writeJson(packagePath, packageJson, { spaces: 2 }); } async createReadme(basePath, projectName = 'My UCG Project') { const readmePath = path.join(basePath, 'README.md'); if (await fs.pathExists(readmePath)) { return; // Don't overwrite existing README } const readmeContent = `# ${projectName} This project was generated using UCG (Universal CRUD Generator). ## Getting Started 1. Install dependencies: \`\`\`bash npm install \`\`\` 2. Configure your database connection in \`src/config/database.js\` 3. Run the development server: \`\`\`bash npm run dev \`\`\` 4. View API documentation at: http://localhost:3000/docs ## Project Structure \`\`\` src/ ├── models/ # Database models ├── controllers/ # Request handlers ├── services/ # Business logic ├── routes/ # Express routes ├── validators/ # Request validation schemas ├── migrations/ # Database migrations ├── tests/ # Unit and integration tests └── docs/ # API documentation \`\`\` ## Scripts - \`npm start\` - Start production server - \`npm run dev\` - Start development server with auto-reload - \`npm test\` - Run tests - \`npm run test:watch\` - Run tests in watch mode - \`npm run lint\` - Run ESLint - \`npm run lint:fix\` - Fix ESLint issues automatically ## Generated by UCG This project structure and boilerplate code were generated using UCG (Universal CRUD Generator). Visit the [UCG repository](https://github.com/your-repo/ucg) for more information. `; await fs.writeFile(readmePath, readmeContent); } // Method to clear generation history async clearHistory() { try { const historyFile = path.join(this.configDir, 'generation-history.json'); if (await fs.pathExists(historyFile)) { await fs.remove(historyFile); } return true; } catch (error) { throw new Error(`Failed to clear history: ${error.message}`); } } // Enhanced rollback method that can target specific generation async rollbackGeneration(index = 0) { try { const history = await this.getGenerationHistory(); if (history.length === 0) { throw new Error('No generations to rollback'); } if (index >= history.length) { throw new Error(`Invalid index: ${index}. Only ${history.length} generations available.`); } const generation = history[index]; const removedFiles = []; // Remove files for (const filePath of generation.files) { if (await fs.pathExists(filePath)) { await fs.remove(filePath); removedFiles.push(filePath); } } // Remove from history history.splice(index, 1); await this.saveGenerationHistory(history); return { generation, removedFiles, message: `Rolled back ${generation.type} generation: ${generation.name}` }; } catch (error) { throw new Error(`Rollback failed: ${error.message}`); } } // Method to save generation history async saveGenerationHistory(history) { const historyFile = path.join(this.configDir, 'generation-history.json'); await fs.writeJson(historyFile, history, { spaces: 2 }); } // Method to validate project structure async validateProjectStructure(projectPath) { const requiredDirs = ['src', 'src/models', 'src/controllers', 'src/routes', 'src/services']; const missingDirs = []; for (const dir of requiredDirs) { const fullPath = path.join(projectPath, dir); if (!await fs.pathExists(fullPath)) { missingDirs.push(dir); } } return { isValid: missingDirs.length === 0, missingDirs }; } // Method to backup files before generation async createBackup(filePaths, backupDir = null) { if (!backupDir) { backupDir = path.join(this.configDir, 'backups', Date.now().toString()); } await fs.ensureDir(backupDir); const backedUpFiles = []; for (const filePath of filePaths) { if (await fs.pathExists(filePath)) { const relativePath = path.relative(process.cwd(), filePath); const backupPath = path.join(backupDir, relativePath); await fs.ensureDir(path.dirname(backupPath)); await fs.copy(filePath, backupPath); backedUpFiles.push({ original: filePath, backup: backupPath }); } } return { backupDir, backedUpFiles }; } } module.exports = FileManager;