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
JavaScript
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;