UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

643 lines (555 loc) 18.3 kB
#!/usr/bin/env node /** * Polymorphic Relationship Generator CLI * Command line interface for generating polymorphic relationship components */ const inquirer = require('inquirer'); const chalk = require('chalk'); const ora = require('ora'); const figlet = require('figlet'); const boxen = require('boxen'); const path = require('path'); const fs = require('fs').promises; const { exec } = require('child_process'); const util = require('util'); const execPromise = util.promisify(exec); const gradient = require('gradient-string'); const yaml = require('js-yaml'); // Import generators const { generatePolymorphicComponents, loadConfig } = require('./generators/polymorphic-generator'); // Import utilities const { pascalCase, camelCase, kebabCase } = require('./utils/string-utils'); // Constants for styling const titleGradient = gradient(['#8731E8', '#4285F4']); /** * Display the banner */ function displayBanner() { console.clear(); console.log( titleGradient.multiline( figlet.textSync('Polymorphic', { font: 'Slant', horizontalLayout: 'default' }) ) ); console.log( boxen( `${chalk.bold('Polymorphic Relationship Generator')} ${chalk.dim('v1.0.0')}\n` + `Generate Laravel & Angular components for polymorphic relationships`, { padding: 1, margin: { top: 1, bottom: 1 }, borderStyle: 'round', borderColor: 'blue' } ) ); } /** * Display a styled section header * @param {string} title - The section title */ function displaySectionHeader(title) { console.log('\n' + chalk.bold.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); console.log(chalk.bold.blue('✨ ') + chalk.bold.white(title)); console.log(chalk.bold.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + '\n'); } /** * Find existing configuration files * @returns {Promise<Array>} Array of configuration files */ async function findConfigFiles() { try { // Look in the examples directory and the current directory const directories = ['examples', '.']; let configFiles = []; for (const dir of directories) { try { const files = await fs.readdir(dir); const configs = files .filter(file => file.endsWith('.yml') || file.endsWith('.yaml') || file.endsWith('.json')) .map(file => ({ name: `${file} (${dir})`, value: path.join(dir, file) })); configFiles = [...configFiles, ...configs]; } catch (error) { // Directory might not exist, that's fine } } return configFiles; } catch (error) { console.error(chalk.red(`Error finding configuration files: ${error.message}`)); return []; } } /** * Generate polymorphic relationships from configuration */ async function generateFromConfig() { displaySectionHeader('Generate from Configuration'); const configFiles = await findConfigFiles(); if (configFiles.length === 0) { console.log(chalk.yellow('No configuration files found.')); return; } const { selectedConfig } = await inquirer.prompt({ type: 'list', name: 'selectedConfig', message: 'Select a configuration file:', choices: configFiles }); try { const spinner = ora('Loading configuration...').start(); // Load the configuration file const config = await loadConfig(selectedConfig); spinner.text = 'Analyzing configuration...'; // Check if the configuration has polymorphic relationships const hasPolymorphic = config.model?.relationships?.some(rel => rel.type === 'morphTo' || rel.type === 'morphMany' ); if (!hasPolymorphic) { spinner.warn('No polymorphic relationships found in the configuration'); const { proceed } = await inquirer.prompt({ type: 'confirm', name: 'proceed', message: 'This configuration does not contain polymorphic relationships. Do you want to continue anyway?', default: false }); if (!proceed) { return; } } else { spinner.succeed('Configuration loaded successfully'); // Display polymorphic relationships const polymorphicRelationships = config.model.relationships.filter(rel => rel.type === 'morphTo' || rel.type === 'morphMany' ); console.log(chalk.green('\nFound the following polymorphic relationships:')); polymorphicRelationships.forEach(rel => { console.log(`- ${chalk.cyan(rel.name)} (${rel.type})`); }); } // Ask for component generation options const options = await inquirer.prompt([ { type: 'input', name: 'outputDir', message: 'Output directory for Angular components:', default: `front/src/app/modules/${kebabCase(config.model.name)}` }, { type: 'confirm', name: 'generateModule', message: 'Generate Angular module file?', default: true }, { type: 'confirm', name: 'generateBackend', message: 'Generate Laravel backend components?', default: true } ]); // Generate Angular components spinner.text = 'Generating frontend components...'; spinner.start(); await generatePolymorphicComponents(config, options); spinner.succeed('Frontend components generated successfully'); // Generate Laravel backend if requested if (options.generateBackend) { spinner.text = 'Generating backend components...'; spinner.start(); await generateLaravelComponents(config); spinner.succeed('Backend components generated successfully'); } console.log(boxen( `${chalk.green.bold('✓')} Polymorphic components generated successfully for ${chalk.cyan(config.model.name)}\n\n` + `${chalk.bold('Frontend:')} Components generated in ${options.outputDir}\n` + `${options.generateBackend ? chalk.bold('Backend:') + ' Laravel models and controllers generated\n' : ''}` + `${chalk.gray('Be sure to register the generated components in your routing.')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green' } )); } catch (error) { console.error(chalk.red(`\nError: ${error.message}`)); } } /** * Generate Laravel backend components for polymorphic relationships * @param {Object} config - The model configuration */ async function generateLaravelComponents(config) { try { const modelName = config.model.name; // Find the first polymorphic relationship const polymorphicRel = config.model.relationships.find(rel => rel.type === 'morphTo' || rel.type === 'morphMany' ); if (!polymorphicRel) { throw new Error('No polymorphic relationship found in the configuration'); } const morphName = polymorphicRel.name; // Determine related models const relatedModels = []; if (polymorphicRel.type === 'morphTo' && polymorphicRel.polymorphicTypes) { relatedModels.push(...polymorphicRel.polymorphicTypes.map(type => type.model)); } // Build the artisan command let command = `cd back && php artisan make:polymorphic ${modelName} ${morphName}`; if (relatedModels.length > 0) { command += ` --related=${relatedModels.join(' --related=')}`; } if (polymorphicRel.type === 'morphTo') { command += ' --morphMany'; } command += ' --migration --force'; // Execute the command const { stdout, stderr } = await execPromise(command); if (stderr) { console.error(chalk.red(stderr)); } console.log(chalk.green(stdout)); } catch (error) { throw new Error(`Failed to generate Laravel components: ${error.message}`); } } /** * Create a new polymorphic relationship configuration */ async function createNewConfig() { displaySectionHeader('Create New Polymorphic Configuration'); // Model information const modelInfo = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Model name (PascalCase):', validate: input => input && input.length > 0 ? true : 'Model name is required' }, { type: 'input', name: 'tableName', message: 'Database table name (snake_case):', default: input => kebabCase(input.name).replace(/-/g, '_') + 's' }, { type: 'input', name: 'displayName', message: 'Human-readable display name:', default: input => input.name } ]); // Polymorphic relationship information const { morphType } = await inquirer.prompt({ type: 'list', name: 'morphType', message: 'Polymorphic relationship type:', choices: [ { name: 'morphTo (belongs to multiple types)', value: 'morphTo' }, { name: 'morphMany (has many of a polymorphic type)', value: 'morphMany' } ] }); const relationship = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Relationship name (camelCase):', validate: input => input && input.length > 0 ? true : 'Relationship name is required', default: morphType === 'morphTo' ? 'commentable' : 'comments' }, { type: 'input', name: 'label', message: 'Display label for relationship:', default: input => input.name.charAt(0).toUpperCase() + input.name.slice(1).replace(/([A-Z])/g, ' $1') } ]); // If morphTo, ask about related types let relatedTypes = []; if (morphType === 'morphTo') { let addMoreTypes = true; while (addMoreTypes) { const relatedType = await inquirer.prompt([ { type: 'input', name: 'model', message: 'Related model (PascalCase):', validate: input => input && input.length > 0 ? true : 'Model name is required' }, { type: 'input', name: 'displayField', message: 'Display field from related model:', default: 'name' } ]); relatedTypes.push(relatedType); const { addAnother } = await inquirer.prompt({ type: 'confirm', name: 'addAnother', message: 'Add another related model type?', default: relatedTypes.length < 2 }); addMoreTypes = addAnother; } } else { // If morphMany, ask about polymorphic model const relatedType = await inquirer.prompt([ { type: 'input', name: 'model', message: 'Polymorphic model (PascalCase):', validate: input => input && input.length > 0 ? true : 'Model name is required' }, { type: 'input', name: 'morphName', message: 'Morph name on related model:', default: 'commentable' } ]); relationship.relatedModel = relatedType.model; relationship.morphName = relatedType.morphName; } // Basic fields (always include some default fields) const defaultFields = [ { name: 'id', type: 'integer', label: 'ID', nullable: false }, { name: 'name', type: 'string', label: 'Name', nullable: false, validations: ['required', 'max:255'] } ]; // Add more fields const { addMoreFields } = await inquirer.prompt({ type: 'confirm', name: 'addMoreFields', message: 'Add more fields to the model?', default: true }); let fields = [...defaultFields]; if (addMoreFields) { let continueAddingFields = true; while (continueAddingFields) { const field = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Field name (camelCase):', validate: input => input && input.length > 0 ? true : 'Field name is required' }, { type: 'list', name: 'type', message: 'Field type:', choices: [ { name: 'String', value: 'string' }, { name: 'Integer', value: 'integer' }, { name: 'Decimal', value: 'decimal' }, { name: 'Boolean', value: 'boolean' }, { name: 'Date', value: 'date' }, { name: 'DateTime', value: 'datetime' }, { name: 'Text', value: 'text' }, { name: 'JSON', value: 'json' }, { name: 'Enum', value: 'enum' } ] }, { type: 'input', name: 'label', message: 'Display label:', default: input => input.name.charAt(0).toUpperCase() + input.name.slice(1).replace(/([A-Z])/g, ' $1') }, { type: 'confirm', name: 'nullable', message: 'Allow null values?', default: false }, { type: 'checkbox', name: 'validations', message: 'Validation rules:', choices: [ { name: 'Required', value: 'required' }, { name: 'Email', value: 'email' }, { name: 'Min Length', value: 'min' }, { name: 'Max Length', value: 'max' }, { name: 'Numeric', value: 'numeric' }, { name: 'Unique', value: 'unique' } ] } ]); fields.push(field); const { addAnother } = await inquirer.prompt({ type: 'confirm', name: 'addAnother', message: 'Add another field?', default: true }); continueAddingFields = addAnother; } } // Build the configuration object const config = { model: { name: modelInfo.name, tableName: modelInfo.tableName, displayName: modelInfo.displayName, fields: fields, relationships: [ { name: relationship.name, type: morphType, label: relationship.label } ] }, ui: { tableFields: ['id', 'name'], itemsPerPage: 10, enableSearch: true, enableFilters: true }, routes: { apiPrefix: `api/${modelInfo.tableName}`, frontendPath: kebabCase(modelInfo.name) + 's', menuTitle: modelInfo.displayName + 's', menuIcon: 'list' } }; // Add polymorphic-specific properties if (morphType === 'morphTo') { config.model.relationships[0].polymorphicTypes = relatedTypes; } else { config.model.relationships[0].relatedModel = relationship.relatedModel; config.model.relationships[0].morphName = relationship.morphName; } // Save the configuration const { fileName } = await inquirer.prompt({ type: 'input', name: 'fileName', message: 'Save configuration as:', default: `${camelCase(modelInfo.name)}-polymorphic.yml` }); const filePath = path.join('examples', fileName); try { // Create examples directory if it doesn't exist await fs.mkdir('examples', { recursive: true }); // Convert to YAML and save const yamlContent = yaml.dump(config, { lineWidth: 120 }); await fs.writeFile(filePath, yamlContent); console.log(chalk.green(`\nConfiguration saved to ${filePath}`)); // Ask if user wants to generate components now const { generateNow } = await inquirer.prompt({ type: 'confirm', name: 'generateNow', message: 'Do you want to generate components from this configuration now?', default: true }); if (generateNow) { // Generate components with default options const options = { outputDir: `front/src/app/modules/${kebabCase(modelInfo.name)}`, generateModule: true, generateBackend: true }; const spinner = ora('Generating components...').start(); try { await generatePolymorphicComponents(config, options); if (options.generateBackend) { await generateLaravelComponents(config); } spinner.succeed('Components generated successfully'); } catch (error) { spinner.fail(`Failed to generate components: ${error.message}`); } } } catch (error) { console.error(chalk.red(`\nError saving configuration: ${error.message}`)); } } /** * Main menu choices */ const mainMenuChoices = [ { name: `${chalk.blue('📝')} Generate from Existing Configuration`, value: 'generateFromConfig' }, { name: `${chalk.green('🧙‍♂️')} Create New Polymorphic Configuration`, value: 'createNewConfig' }, { name: `${chalk.red('👋')} Exit`, value: 'exit' } ]; /** * Display the main menu and handle selection */ async function showMainMenu() { displayBanner(); const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: mainMenuChoices } ]); switch (action) { case 'generateFromConfig': await generateFromConfig(); break; case 'createNewConfig': await createNewConfig(); break; case 'exit': console.log(chalk.blue('\nThank you for using the Polymorphic Relationship Generator! 👋\n')); process.exit(0); } // Ask if the user wants to return to the main menu const { returnToMenu } = await inquirer.prompt({ type: 'confirm', name: 'returnToMenu', message: 'Return to main menu?', default: true }); if (returnToMenu) { setTimeout(showMainMenu, 500); } else { console.log(chalk.blue('\nThank you for using the Polymorphic Relationship Generator! 👋\n')); process.exit(0); } } /** * Application entry point */ async function main() { try { await showMainMenu(); } catch (error) { console.error(chalk.red(`\nAn error occurred: ${error.message}\n`)); process.exit(1); } } // Start the application if (require.main === module) { main(); } module.exports = { generatePolymorphic: main };