@diagramers/cli
Version:
Diagramers CLI - Command-line tools for managing Diagramers projects
397 lines (391 loc) • 18.8 kB
JavaScript
;
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