UNPKG

sb-crud-gen

Version:

A simple CLI tool to **generate CRUD boilerplate code** for a given model with customizable fields.

241 lines (202 loc) 8.48 kB
#!/usr/bin/env node import { program } from 'commander'; import fs from 'fs/promises'; import path from 'path'; import ejs from 'ejs'; import readline from 'readline'; function parseFields(fieldArgs) { return fieldArgs.map(fld => { const [name, type = 'string'] = fld.split(':'); return { name, type }; }); } async function createModel(modelNameRaw, fieldArgs = []) { const modelName = modelNameRaw.toLowerCase(); const ModelName = modelNameRaw[0].toUpperCase() + modelNameRaw.slice(1); const modelPlural = modelName + 's'; const fields = parseFields(fieldArgs); const baseDir = process.cwd(); const paths = { routes: path.join(baseDir, 'src/routes', `${modelPlural}.js`), controller: path.join(baseDir, 'src/controllers', `${modelName}Controller.js`), query: path.join(baseDir, 'src/db/queries', `${modelName}.js`), db: path.join(baseDir, 'src/db/db.js'), indexRoutes: path.join(baseDir, 'src/routes/index.js'), pool: path.join(baseDir, 'src/db/pool.js'), }; for (const filePath of [paths.routes, paths.controller, paths.query]) { try { await fs.access(filePath); console.error(`❌ File already exists: ${filePath}`); process.exit(1); } catch { } } for (const dir of Object.values(paths).map(p => path.dirname(p))) { await fs.mkdir(dir, { recursive: true }); } async function renderTemplate(templateName, data) { const templatePath = path.join(new URL(import.meta.url).pathname, '..', 'templates', templateName); const template = await fs.readFile(templatePath, 'utf-8'); return ejs.render(template, data); } const fieldNames = fields.map(f => f.name).join(', '); const placeholders = fields.map((_, i) => `$${i + 1}`).join(', '); const updateSets = fields.map((f, i) => `${f.name} = $${i + 1}`).join(', '); const updateIndex = fields.length + 1; const controllerDestruct = `{ ${fields.map(f => f.name).join(', ')} }`; const fieldList = fieldNames.split(',').map(f => f.trim()); const data = { modelName, ModelName, modelPlural, fieldNames, placeholders, updateSets, updateIndex, fieldList, controllerDestruct }; await fs.writeFile(paths.query, await renderTemplate('query_prisma.ejs', data)); console.log(`✔ Created query file`); await fs.writeFile(paths.controller, await renderTemplate('controller_prisma.ejs', data)); console.log(`✔ Created controller file`); await fs.writeFile(paths.routes, await renderTemplate('routes.ejs', data)); console.log(`✔ Created routes file`); try { let dbContent = await fs.readFile(paths.db, 'utf-8'); if (!dbContent.includes(`import ${modelName} from './queries/${modelName}.js'`)) { dbContent = `import ${modelName} from './queries/${modelName}.js';\n` + dbContent; } if (!dbContent.includes(`${modelName},`)) { dbContent = dbContent.replace(/const db = \{/, `const db = {\n ${modelName},`); } if (!dbContent.includes('export default db;')) { dbContent += `\nexport default db;\n`; } await fs.writeFile(paths.db, dbContent); console.log(`✔ Updated db.js`); } catch { const dbContent = `import ${modelName} from './queries/${modelName}.js';\n\nconst db = {\n ${modelName},\n};\n\nexport default db;\n`; await fs.writeFile(paths.db, dbContent); console.log(`✔ Created db.js`); } try { let indexContent = await fs.readFile(paths.indexRoutes, 'utf-8'); const importLine = `import ${modelPlural}Routes from './${modelPlural}.js';`; const routeLine = ` app.use(\`/api/\${apiV}/${modelPlural}\`, ${modelPlural}Routes);`; if (!indexContent.includes(importLine)) { indexContent = importLine + '\n' + indexContent; } if (!indexContent.includes(routeLine)) { indexContent = indexContent.replace(/function registerRoutes\(app, apiV\) \{/, `function registerRoutes(app, apiV) {\n${routeLine}`); } if (!indexContent.includes('export default registerRoutes;')) { indexContent += `\nexport default registerRoutes;\n`; } await fs.writeFile(paths.indexRoutes, indexContent); console.log(`✔ Updated routes/index.js`); } catch { const indexContent = `import ${modelPlural}Routes from './${modelPlural}.js';\n\nfunction registerRoutes(app, apiV) {\n app.use(\`/api/\${apiV}/${modelPlural}\`, ${modelPlural}Routes);\n}\n\nexport default registerRoutes;\n`; await fs.writeFile(paths.indexRoutes, indexContent); console.log(`✔ Created routes/index.js`); } console.log(`✅ CRUD for model "${ModelName}" generated successfully!`); } program .name('sb-crud-gen') .description('CRUD generator CLI tool') .version('1.0.0'); program .command('create <modelName> [fields...]') .description('Generate CRUD files for a model\n\nExample:\n create author name:string age:int\n') .action((modelNameRaw, fieldsArgs) => { if (!fieldsArgs || fieldsArgs.length === 0) { console.error('❌ Error: You must provide at least one field. Example: create author name:string age:int'); process.exit(1); } createModel(modelNameRaw, fieldsArgs).catch(err => { console.error('❌ Error:', err.message); process.exit(1); }); }); async function promptYesNo(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { rl.question(question, answer => { rl.close(); resolve(answer.toLowerCase() === 'y'); }); }); } async function removeModel(modelNameRaw) { const modelName = modelNameRaw.toLowerCase(); const ModelName = modelNameRaw[0].toUpperCase() + modelNameRaw.slice(1); const modelPlural = modelName + 's'; const baseDir = process.cwd(); // Paths to delete const paths = { routes: path.join(baseDir, 'src/routes', `${modelPlural}.js`), controller: path.join(baseDir, 'src/controllers', `${modelName}Controller.js`), query: path.join(baseDir, 'src/db/queries', `${modelName}.js`), db: path.join(baseDir, 'src/db/db.js'), indexRoutes: path.join(baseDir, 'src/routes/index.js'), }; // Confirm deletion const confirmed = await promptYesNo(`🗑 Are you sure you want to delete model "${ModelName}" and all related files? (y/N): `); if (!confirmed) { console.log('❌ Cancelled.'); process.exit(0); } // Delete files if they exist for (const [key, filePath] of Object.entries(paths)) { if (key === 'db' || key === 'indexRoutes') continue; // handled below try { await fs.unlink(filePath); console.log(`✔ Deleted ${filePath}`); } catch { console.log(`⚠ File not found (skipped): ${filePath}`); } } // Remove from db.js try { let dbContent = await fs.readFile(paths.db, 'utf-8'); // Remove import line const importRegex = new RegExp(`import\\s+${modelName}\\s+from\\s+'.\\/queries\\/${modelName}\\.js';?\\n`, 'g'); dbContent = dbContent.replace(importRegex, ''); // Remove registration line inside db object const registrationRegex = new RegExp(`\\s*${modelName},?\\n`, 'g'); dbContent = dbContent.replace(registrationRegex, ''); await fs.writeFile(paths.db, dbContent); console.log(`✔ Updated ${paths.db}`); } catch { console.log(`⚠ ${paths.db} not found or could not update.`); } // Remove from routes/index.js try { let indexContent = await fs.readFile(paths.indexRoutes, 'utf-8'); // Remove import const importRegex = new RegExp(`import\\s+${modelPlural}Routes\\s+from\\s+'.\\/${modelPlural}\\.js';?\\n`, 'g'); indexContent = indexContent.replace(importRegex, ''); // Remove route registration line const routeRegex = new RegExp(`\\s*app\\.use\\(\\\`\\/api\\/\\$\\{apiV\\}\\/\\${modelPlural}\\\`,\\s*${modelPlural}Routes\\);?\\n`, 'g'); indexContent = indexContent.replace(routeRegex, ''); await fs.writeFile(paths.indexRoutes, indexContent); console.log(`✔ Updated ${paths.indexRoutes}`); } catch { console.log(`⚠ ${paths.indexRoutes} not found or could not update.`); } console.log(`✅ Model "${ModelName}" and related files removed.`); } program .command('remove <modelName>') .description('Remove generated CRUD files and references for a model') .action((modelNameRaw) => { removeModel(modelNameRaw).catch(err => { console.error('❌ Error:', err.message); process.exit(1); }); }); program.parse(process.argv);