UNPKG

@diagramers/cli

Version:

Diagramers CLI - Command-line tools for managing Diagramers projects

397 lines (391 loc) 18.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTable = generateTable; const fs = __importStar(require("fs")); const path = __importStar(require("path")); // Function to convert plural table names to singular for file names function pluralToSingular(word) { // Special cases for common words const specialCases = { 'profiles': 'profile', 'categories': 'category', 'stories': 'story', 'series': 'series', // already singular 'species': 'species', // already singular 'analyses': 'analysis', 'bases': 'base', 'crises': 'crisis', 'diagnoses': 'diagnosis', 'ellipses': 'ellipsis', 'hypotheses': 'hypothesis', 'oases': 'oasis', 'parentheses': 'parenthesis', 'synopses': 'synopsis', 'theses': 'thesis' }; // Check special cases first if (specialCases[word]) { return specialCases[word]; } // Regular pluralization rules if (word.endsWith('ies')) { return word.slice(0, -3) + 'y'; // categories -> category } else if (word.endsWith('es')) { return word.slice(0, -2); // boxes -> box } else if (word.endsWith('s')) { return word.slice(0, -1); // users -> user, products -> product } return word; // already singular } async function generateTable(moduleName, tableName, options = {}) { // Validate module and table names if (!moduleName || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(moduleName)) { throw new Error('Invalid module name. Use only letters, numbers, hyphens, and underscores. Must start with a letter.'); } if (!tableName || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(tableName)) { throw new Error('Invalid table name. Use only letters, numbers, hyphens, and underscores. Must start with a letter.'); } const singularName = pluralToSingular(tableName); const tableNameCapitalized = singularName.charAt(0).toUpperCase() + singularName.slice(1); const currentDir = process.cwd(); // Check for required API project structure const requiredDirs = ['src/modules']; const missingDirs = requiredDirs.filter(dir => !fs.existsSync(path.join(currentDir, dir))); if (missingDirs.length > 0) { throw new Error(`This command should be run from a diagramers API project. Missing directories: ${missingDirs.join(', ')}`); } // Check if the module exists const modulePath = path.join(currentDir, 'src/modules', moduleName); if (!fs.existsSync(modulePath)) { throw new Error(`Module '${moduleName}' does not exist. Please create the module first using: diagramers extend --module ${moduleName}`); } // Check if entities and schemas directories exist, create if not const entitiesPath = path.join(modulePath, 'entities'); const schemasPath = path.join(modulePath, 'schemas'); if (!fs.existsSync(entitiesPath)) { fs.mkdirSync(entitiesPath, { recursive: true }); } if (!fs.existsSync(schemasPath)) { fs.mkdirSync(schemasPath, { recursive: true }); } // Check if table already exists const entityFile = path.join(entitiesPath, `${singularName}.entity.ts`); const schemaFile = path.join(schemasPath, `${singularName}.schema.ts`); if ((fs.existsSync(entityFile) || fs.existsSync(schemaFile)) && !options.force) { throw new Error(`Table '${tableName}' already exists in module '${moduleName}'. Use --force to override existing files.`); } // Parse options const fields = options.fields || ['name', 'description', 'status']; const generateSeeder = options.seeder !== false; const databaseType = options.type || 'mongodb'; console.log(`📝 Creating entity for table '${tableName}' in module '${moduleName}'...`); const entityContent = generateEntityContent(singularName, tableNameCapitalized, { fields }); // Write or replace the entity interface if (fs.existsSync(entityFile) && options.force) { let existingContent = fs.readFileSync(entityFile, 'utf8'); // Find all interface blocks and merge their fields const interfaceRegexGlobal = /export interface I\w+ extends mongoose\.Document \{([\s\S]*?)\}/g; let allFields = []; let match; while ((match = interfaceRegexGlobal.exec(existingContent)) !== null) { const fields = match[1].split('\n').map(line => line.trim()).filter(Boolean); allFields.push(...fields); } // Add new fields from the generated interface const newFields = generateEntityContent(singularName, tableNameCapitalized, { fields }) .split('\n').filter(line => !line.startsWith('export interface') && !line.startsWith('import') && !line.startsWith('}')) .map(f => f.trim()).filter(Boolean); allFields.push(...newFields); // Deduplicate fields by name const seen = new Set(); const dedupedFields = []; for (const line of allFields) { const fieldMatch = line.match(/^(\w+)/); if (fieldMatch) { const fieldName = fieldMatch[1]; if (!seen.has(fieldName)) { seen.add(fieldName); dedupedFields.push(line.replace(/[,;]*$/, ';')); } } else { dedupedFields.push(line); } } // Build the new interface const newInterface = `export interface ${tableNameCapitalized} extends BaseEntity {\n _id?: string;\n ${dedupedFields.join('\n ')}\n}`; // Remove all old interface blocks and insert the new one at the position of the first existingContent = existingContent.replace(interfaceRegexGlobal, ''); existingContent = existingContent.trim() + '\n' + newInterface + '\n'; fs.writeFileSync(entityFile, existingContent); } else { fs.writeFileSync(entityFile, entityContent); } console.log(`📋 Creating schema for table '${tableName}' in module '${moduleName}'...`); const schemaContent = generateSchemaContent(singularName, tableNameCapitalized, tableName, { fields, type: databaseType }); fs.writeFileSync(schemaFile, schemaContent); // Add to seeder if requested if (generateSeeder) { console.log(`🌱 Adding '${tableName}' to database seeder...`); await addToSeeder(currentDir, moduleName, tableName, singularName, tableNameCapitalized, { fields, type: databaseType }); } console.log('✅ Table generation completed!'); console.log(`📁 Table: src/modules/${moduleName}/entities/${singularName}.entity.ts`); console.log(`📁 Schema: src/modules/${moduleName}/schemas/${singularName}.schema.ts`); if (generateSeeder) { console.log(`🌱 Seeder: Added to src/core/database/seeder.ts`); } console.log(`🗄️ Database Type: ${databaseType}`); } function generateEntityContent(singularName, tableNameCapitalized, options) { const fields = options.fields || ['name', 'description']; let fieldDefinitions = ''; fields.forEach((field) => { const fieldType = getFieldType(field); const isOptional = field === 'description' || field.includes('?'); const cleanField = field.replace('?', ''); fieldDefinitions += ` ${cleanField}${isOptional ? '?' : ''}: ${fieldType};\n`; }); const interfaceBlock = `import { BaseEntity } from '../../../shared/types/base-entity'; export interface ${tableNameCapitalized} extends BaseEntity { _id?: string; ${fieldDefinitions} status: number; createdAt: Date; updatedAt: Date; }`; return interfaceBlock; } function generateSchemaContent(singularName, tableNameCapitalized, tableName, options) { const fields = options.fields || ['name', 'description']; let schemaFields = ''; fields.forEach((field) => { const isOptional = field === 'description' || field.includes('?'); const cleanField = field.replace('?', ''); const mongooseType = getMongooseType(cleanField); const required = !isOptional; schemaFields += ` ${cleanField}: { type: ${mongooseType}, required: ${required}, },\n`; }); return `import mongoose, { Document, Schema } from 'mongoose'; import { ${tableNameCapitalized} } from '../entities/${singularName}.entity'; export interface I${tableNameCapitalized}Document extends Omit<${tableNameCapitalized}, '_id'>, Document {} const ${singularName}Schema = new Schema<I${tableNameCapitalized}Document>({ ${schemaFields} status: { type: Number, default: 1 }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); export const ${tableNameCapitalized}Model = mongoose.model<I${tableNameCapitalized}Document>('${tableName}', ${singularName}Schema);`; } function getFieldType(field) { const cleanField = field.replace('?', '').toLowerCase(); if (cleanField.includes('id') || cleanField === '_id') return 'string'; if (cleanField.includes('email')) return 'string'; if (cleanField.includes('password')) return 'string'; if (cleanField.includes('phone')) return 'string'; if (cleanField.includes('url')) return 'string'; if (cleanField.includes('date') || cleanField.includes('time')) return 'Date'; if (cleanField.includes('count') || cleanField.includes('number') || cleanField.includes('age')) return 'number'; if (cleanField.includes('is') || cleanField.includes('has') || cleanField.includes('verified')) return 'boolean'; if (cleanField.includes('status')) return 'number'; return 'string'; // default } function getMongooseType(field) { const cleanField = field.toLowerCase(); if (cleanField.includes('id') || cleanField === '_id') return 'String'; if (cleanField.includes('date') || cleanField.includes('time')) return 'Date'; if (cleanField.includes('count') || cleanField.includes('number') || cleanField.includes('age')) return 'Number'; if (cleanField.includes('is') || cleanField.includes('has') || cleanField.includes('verified')) return 'Boolean'; if (cleanField.includes('status')) return 'Number'; return 'String'; // default } async function addToSeeder(currentDir, moduleName, tableName, singularName, tableNameCapitalized, options) { const seederPath = path.join(currentDir, 'src/core/database/seeder.ts'); if (!fs.existsSync(seederPath)) { console.log('⚠️ Seeder file not found, skipping seeder update'); return; } let seederContent = fs.readFileSync(seederPath, 'utf8'); // Add import const importStatement = `import { ${tableNameCapitalized}Model } from '../../modules/${moduleName}/schemas/${singularName}.schema';`; if (!seederContent.includes(importStatement)) { // Find the last import and add after it const lastImportIndex = seederContent.lastIndexOf('import'); const nextLineIndex = seederContent.indexOf('\n', lastImportIndex); seederContent = seederContent.slice(0, nextLineIndex) + '\n' + importStatement + seederContent.slice(nextLineIndex); } // Add to SeedData interface const seedDataInterface = ` ${tableName}?: any[];`; if (!seederContent.includes(seedDataInterface)) { const interfaceMatch = seederContent.match(/export interface SeedData \{([^}]*)\}/s); if (interfaceMatch) { const beforeClosing = interfaceMatch[0].lastIndexOf('}'); const beforeText = interfaceMatch[0].slice(0, beforeClosing); const afterText = interfaceMatch[0].slice(beforeClosing); const newInterface = beforeText + seedDataInterface + '\n' + afterText; seederContent = seederContent.replace(interfaceMatch[0], newInterface); } } // Note: Seeder calls are handled by the dynamic seeder system, no need to add explicit calls // Add default seed data at root level const defaultData = generateDefaultSeedData(tableName, options); const defaultDataLine = ` ${tableName}: ${JSON.stringify(defaultData, null, 6).replace(/\n/g, '\n ')},`; // Find defaultData object and add the line at root level if (!seederContent.includes(`${tableName}:`)) { // Look for the closing brace of the defaultData object const defaultDataStart = seederContent.indexOf('const defaultData: SeedData = {'); if (defaultDataStart !== -1) { // Find the matching closing brace let braceCount = 0; let index = defaultDataStart + 'const defaultData: SeedData = {'.length; let closingBraceIndex = -1; while (index < seederContent.length) { if (seederContent[index] === '{') { braceCount++; } else if (seederContent[index] === '}') { if (braceCount === 0) { closingBraceIndex = index; break; } braceCount--; } index++; } if (closingBraceIndex !== -1) { // Insert the new data before the closing brace const beforeClosing = seederContent.slice(0, closingBraceIndex); const afterClosing = seederContent.slice(closingBraceIndex); // Check if we need to add a comma const trimmedBefore = beforeClosing.trim(); const needsComma = !trimmedBefore.endsWith('{') && !trimmedBefore.endsWith(','); seederContent = beforeClosing + (needsComma ? ',' : '') + '\n' + defaultDataLine + '\n ' + afterClosing; } } } // Add seeder method - use exact table name for method name to match dynamic seeder system const methodName = tableName.charAt(0).toUpperCase() + tableName.slice(1); const seederMethod = ` private async seed${methodName}(data: any[]): Promise<void> { if (data.length === 0) return; logger.info(\`[DatabaseSeeder] Seeding \${data.length} ${tableName} records...\`); for (const ${singularName}Data of data) { try { const existing = await ${tableNameCapitalized}Model.findOne({ name: ${singularName}Data.name }); if (!existing) { const ${singularName} = new ${tableNameCapitalized}Model({ ...${singularName}Data, status: 1 }); await ${singularName}.save(); logger.info(\`[DatabaseSeeder] Created ${singularName}: \${${singularName}Data.name}\`); } else { logger.info(\`[DatabaseSeeder] ${tableNameCapitalized} \${${singularName}Data.name} already exists, skipping\`); } } catch (error) { logger.error(\`[DatabaseSeeder] Failed to create ${singularName} \${${singularName}Data.name}:\`, error); } } }`; // Add method before closing class brace if it doesn't exist if (!seederContent.includes(`seed${methodName}(`)) { const templateComment = '// TEMPLATE: Add new seeder functions here (the CLI will add new functions automatically)'; if (seederContent.includes(templateComment)) { seederContent = seederContent.replace(templateComment, seederMethod + '\n' + templateComment); } else { const classEndIndex = seederContent.lastIndexOf('}'); seederContent = seederContent.slice(0, classEndIndex) + seederMethod + '\n' + seederContent.slice(classEndIndex); } } fs.writeFileSync(seederPath, seederContent); } function generateDefaultSeedData(tableName, options) { const fields = options.fields || ['name', 'description']; const sampleData = []; // Generate 3 sample records for (let i = 1; i <= 3; i++) { const record = {}; fields.forEach((field) => { const cleanField = field.replace('?', ''); if (cleanField === 'name') { record[cleanField] = `Sample ${tableName.charAt(0).toUpperCase() + tableName.slice(1, -1)} ${i}`; } else if (cleanField === 'description') { record[cleanField] = `Description for sample ${tableName.slice(0, -1)} ${i}`; } else if (cleanField.includes('email')) { record[cleanField] = `sample${i}@example.com`; } else if (cleanField.includes('phone')) { record[cleanField] = `+1234567890${i}`; } else if (cleanField.includes('url')) { record[cleanField] = `https://example.com/${cleanField}${i}`; } else if (cleanField.includes('count') || cleanField.includes('number') || cleanField.includes('age')) { record[cleanField] = i * 10; } else if (cleanField.includes('is') || cleanField.includes('has') || cleanField.includes('verified')) { record[cleanField] = i % 2 === 1; } else { record[cleanField] = `Sample ${cleanField} ${i}`; } }); sampleData.push(record); } return sampleData; } //# sourceMappingURL=table-generator.js.map