UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

440 lines (382 loc) 14.1 kB
/** * Polymorphic Relationship Generator * Generates Angular components for models with polymorphic relationships */ const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); const ora = require('ora'); const yaml = require('js-yaml'); const { execSync } = require('child_process'); const { pascalCase, kebabCase, camelCase } = require('../utils/string-utils'); /** * Generate Angular components for a model with polymorphic relationships * @param {Object} config - The model configuration * @param {Object} options - Generator options * @returns {Promise<void>} */ async function generatePolymorphicComponents(config, options) { const spinner = ora('Generating polymorphic components...').start(); try { // Validate the configuration validatePolymorphicConfig(config); // Extract configuration values const modelName = config.model.name; const modelNameKebab = kebabCase(modelName); const modelNameCamel = camelCase(modelName); // Find polymorphic relationships const polymorphicRelationships = findPolymorphicRelationships(config.model.relationships); if (polymorphicRelationships.length === 0) { spinner.warn('No polymorphic relationships found in the configuration'); return; } spinner.text = `Generating components for ${modelName} with ${polymorphicRelationships.length} polymorphic relationships...`; // Create directories const componentDir = path.join(options.outputDir, modelNameKebab); await fs.mkdir(componentDir, { recursive: true }); // Generate component files await generateComponentFiles(componentDir, config, polymorphicRelationships, options); spinner.succeed(`Polymorphic components generated successfully for ${modelName}`); } catch (error) { spinner.fail(`Failed to generate polymorphic components: ${error.message}`); throw error; } } /** * Validate the configuration for polymorphic relationships * @param {Object} config - The model configuration */ function validatePolymorphicConfig(config) { if (!config.model) { throw new Error('Invalid configuration: model section is missing'); } if (!config.model.name) { throw new Error('Invalid configuration: model name is missing'); } if (!config.model.relationships || !Array.isArray(config.model.relationships)) { throw new Error('Invalid configuration: relationships array is missing'); } } /** * Find polymorphic relationships in the model configuration * @param {Array} relationships - The relationships array from the model configuration * @returns {Array} - Array of polymorphic relationships */ function findPolymorphicRelationships(relationships) { return relationships.filter(rel => rel.type === 'morphTo' || rel.type === 'morphMany' ); } /** * Generate component files for the polymorphic model * @param {string} componentDir - The component directory * @param {Object} config - The model configuration * @param {Array} polymorphicRelationships - The polymorphic relationships * @param {Object} options - Generator options */ async function generateComponentFiles(componentDir, config, polymorphicRelationships, options) { const modelName = config.model.name; const modelNameKebab = kebabCase(modelName); const modelNamePascal = pascalCase(modelName); const modelNameCamel = camelCase(modelName); // Read template files const templatesDir = path.join(process.cwd(), 'cli/templates/polymorphic'); // Read component templates const tsTemplatePath = path.join(templatesDir, 'angular-component.ts.template'); const htmlTemplatePath = path.join(templatesDir, 'angular-component.html.template'); const tsTemplate = await fs.readFile(tsTemplatePath, 'utf8'); const htmlTemplate = await fs.readFile(htmlTemplatePath, 'utf8'); // Prepare template data const templateData = { modelName: modelName, kebabCase: modelNameKebab, pascalCase: modelNamePascal, camelCase: modelNameCamel, resourceName: modelNameCamel, displayName: config.model.displayName || modelName, itemsPerPage: config.ui?.itemsPerPage || 10, // Handle first polymorphic relationship (currently only supporting one) polymorphicRelationship: polymorphicRelationships[0], morphName: polymorphicRelationships[0].name, morphLabel: polymorphicRelationships[0].ui?.label || polymorphicRelationships[0].name, defaultDisplayField: 'name', // Prepare fields for form and table formFields: prepareFormFields(config.model.fields), tableColumns: prepareTableColumns(config.model.fields, config.ui?.tableFields), // Prepare morphable types information morphableTypes: prepareMorphableTypes(polymorphicRelationships[0]) }; // Replace template variables const tsContent = replaceTemplateVariables(tsTemplate, templateData); const htmlContent = replaceTemplateVariables(htmlTemplate, templateData); // Write component files await fs.writeFile(path.join(componentDir, `${modelNameKebab}.component.ts`), tsContent); await fs.writeFile(path.join(componentDir, `${modelNameKebab}.component.html`), htmlContent); // Create basic SCSS file await fs.writeFile( path.join(componentDir, `${modelNameKebab}.component.scss`), `.${modelNameKebab}-container {\n padding: 20px;\n}` ); // Generate module file if needed if (options.generateModule) { await generateModuleFile(componentDir, modelName); } } /** * Prepare form fields configuration * @param {Array} fields - Model fields * @returns {Array} - Prepared form fields */ function prepareFormFields(fields) { return fields.map(field => { const formField = { name: field.name, label: field.label || field.name, required: field.validations?.includes('required') || false }; // Determine input type switch (field.type) { case 'text': formField.isTextarea = true; break; case 'boolean': formField.isSelect = true; formField.optionsName = 'booleanOptions'; formField.optionLabel = 'label'; formField.optionValue = 'value'; break; case 'enum': formField.isSelect = true; formField.optionsName = `${field.name}Options`; formField.optionLabel = 'label'; formField.optionValue = 'value'; break; default: formField.isInput = true; formField.inputType = mapTypeToInputType(field.type); } return formField; }); } /** * Prepare table columns configuration * @param {Array} fields - Model fields * @param {Array} tableFields - Fields to include in the table * @returns {Array} - Prepared table columns */ function prepareTableColumns(fields, tableFields) { const fieldMap = fields.reduce((map, field) => { map[field.name] = field; return map; }, {}); // If no tableFields specified, use the first few fields const columnsToUse = tableFields || fields.slice(0, 4).map(f => f.name); return columnsToUse.map(fieldName => { const field = fieldMap[fieldName]; if (!field) { return { name: fieldName, label: fieldName, fieldAccessor: `{{ item.${fieldName} }}` }; } return { name: fieldName, label: field.label || fieldName, fieldAccessor: getFieldAccessor(field) }; }); } /** * Prepare morphable types for the template * @param {Object} relationship - The polymorphic relationship * @returns {Array} - Prepared morphable types */ function prepareMorphableTypes(relationship) { if (!relationship.polymorphicTypes || !Array.isArray(relationship.polymorphicTypes)) { return [ { label: 'Post', value: 'Post', apiRoute: 'posts', modelName: 'Post', displayField: 'title' }, { label: 'Product', value: 'Product', apiRoute: 'products', modelName: 'Product', displayField: 'name' } ]; } return relationship.polymorphicTypes.map(type => ({ label: type.model, value: type.model, apiRoute: kebabCase(type.model) + 's', modelName: type.model, displayField: type.displayField || 'name' })); } /** * Map data type to input type * @param {string} type - Data type * @returns {string} - HTML input type */ function mapTypeToInputType(type) { const typeMap = { 'string': 'text', 'integer': 'number', 'decimal': 'number', 'date': 'date', 'datetime': 'datetime-local', 'email': 'email', 'password': 'password', 'boolean': 'checkbox' }; return typeMap[type] || 'text'; } /** * Get field accessor template string * @param {Object} field - Field configuration * @returns {string} - Template accessor */ function getFieldAccessor(field) { switch (field.type) { case 'boolean': return `{{ item.${field.name} ? 'Yes' : 'No' }}`; case 'date': case 'datetime': return `{{ item.${field.name} | date }}`; default: return `{{ item.${field.name} }}`; } } /** * Generate module file for the component * @param {string} componentDir - Component directory * @param {string} modelName - Model name */ async function generateModuleFile(componentDir, modelName) { const modelNameKebab = kebabCase(modelName); const modelNamePascal = pascalCase(modelName); const moduleContent = `import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; // PrimeNG Components import { TableModule } from 'primeng/table'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; import { InputTextModule } from 'primeng/inputtext'; import { InputTextareaModule } from 'primeng/inputtextarea'; import { DropdownModule } from 'primeng/dropdown'; import { MultiSelectModule } from 'primeng/multiselect'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { ToastModule } from 'primeng/toast'; import { ${modelNamePascal}Component } from './${modelNameKebab}.component'; @NgModule({ declarations: [ ${modelNamePascal}Component ], imports: [ CommonModule, FormsModule, ReactiveFormsModule, RouterModule.forChild([ { path: '', component: ${modelNamePascal}Component } ]), TableModule, ButtonModule, DialogModule, InputTextModule, InputTextareaModule, DropdownModule, MultiSelectModule, ConfirmDialogModule, ToastModule ], exports: [ ${modelNamePascal}Component ] }) export class ${modelNamePascal}Module { }`; await fs.writeFile(path.join(componentDir, `${modelNameKebab}.module.ts`), moduleContent); } /** * Replace template variables with actual values * @param {string} template - Template string * @param {Object} data - Data for replacement * @returns {string} - Processed template */ function replaceTemplateVariables(template, data) { let content = template; // Simple variable replacements for (const [key, value] of Object.entries(data)) { if (typeof value === 'string' || typeof value === 'number') { const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g'); content = content.replace(regex, value); } } // Array-based replacements with {{#section}} ... {{/section}} blocks for (const [key, value] of Object.entries(data)) { if (Array.isArray(value) && value.length > 0) { // Handle array sections const pattern = new RegExp(`{{#${key}}}([\\s\\S]*?){{\\/${key}}}`, 'g'); let match; while ((match = pattern.exec(template)) !== null) { const fullMatch = match[0]; const sectionTemplate = match[1]; let replacement = ''; for (const item of value) { let itemReplacement = sectionTemplate; if (typeof item === 'object') { for (const [itemKey, itemValue] of Object.entries(item)) { if (typeof itemValue === 'string' || typeof itemValue === 'number' || typeof itemValue === 'boolean') { const itemRegex = new RegExp(`{{\\s*${itemKey}\\s*}}`, 'g'); itemReplacement = itemReplacement.replace(itemRegex, itemValue); } } } else { const dotRegex = new RegExp(`{{\\s*\\.\\s*}}`, 'g'); itemReplacement = itemReplacement.replace(dotRegex, item); } replacement += itemReplacement; } content = content.replace(fullMatch, replacement); } } else if (typeof value === 'boolean') { // Handle boolean sections const truePattern = new RegExp(`{{#${key}}}([\\s\\S]*?){{\\/${key}}}`, 'g'); const falsePattern = new RegExp(`{{^${key}}}([\\s\\S]*?){{\\/${key}}}`, 'g'); // Handle true case if (value) { let match; while ((match = truePattern.exec(template)) !== null) { const fullMatch = match[0]; const sectionTemplate = match[1]; content = content.replace(fullMatch, sectionTemplate); } // Remove false cases content = content.replace(falsePattern, ''); } else { // Handle false case let match; while ((match = falsePattern.exec(template)) !== null) { const fullMatch = match[0]; const sectionTemplate = match[1]; content = content.replace(fullMatch, sectionTemplate); } // Remove true cases content = content.replace(truePattern, ''); } } } return content; } /** * Load a YAML configuration file * @param {string} filePath - Path to configuration file * @returns {Promise<Object>} - Parsed configuration */ async function loadConfig(filePath) { try { const content = await fs.readFile(filePath, 'utf8'); return yaml.load(content); } catch (error) { throw new Error(`Failed to load configuration: ${error.message}`); } } module.exports = { generatePolymorphicComponents, loadConfig };