kira-crud
Version:
Intelligent CRUD Generator for Laravel and Angular
643 lines (555 loc) • 18.3 kB
JavaScript
#!/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
};