UNPKG

mongoose-database-schema

Version:

MongoDB database documentation generator with table schemas and relationships

544 lines (452 loc) 23.7 kB
const fs = require('fs-extra'); const path = require('path'); class DocumentationGenerator { constructor() { this.outputDir = 'docs'; } async generateDocumentation(schemas, relationships, embeddedRelationships, outputFormat = 'markdown') { await fs.ensureDir(this.outputDir); if (outputFormat === 'markdown') { await this.generateMarkdownDocs(schemas, relationships, embeddedRelationships); } else if (outputFormat === 'json') { await this.generateJSONDocs(schemas, relationships, embeddedRelationships); } else if (outputFormat === 'html') { await this.generateHTMLDocs(schemas, relationships, embeddedRelationships); } console.log(`Documentation generated in ${this.outputDir} directory`); } async generateMarkdownDocs(schemas, relationships, embeddedRelationships) { let overview = this.generateOverview(schemas, relationships); await fs.writeFile(path.join(this.outputDir, 'README.md'), overview); for (const schema of schemas) { const collectionDoc = this.generateCollectionMarkdown(schema, relationships, embeddedRelationships); await fs.writeFile(path.join(this.outputDir, `${schema.collectionName}.md`), collectionDoc); } const relationshipsDoc = this.generateRelationshipsMarkdown(relationships, embeddedRelationships); await fs.writeFile(path.join(this.outputDir, 'relationships.md'), relationshipsDoc); // Generate database diagram file const diagramDoc = this.generateDatabaseDiagram(schemas, relationships); await fs.writeFile(path.join(this.outputDir, 'database-diagram.md'), diagramDoc); // Generate comprehensive documentation file const comprehensiveDoc = this.generateComprehensiveMarkdown(schemas, relationships, embeddedRelationships); await fs.writeFile(path.join(this.outputDir, 'complete-database-documentation.md'), comprehensiveDoc); } generateOverview(schemas, relationships) { let overview = `# MongoDB Database Documentation\n\n`; overview += `Generated on: ${new Date().toISOString()}\n\n`; overview += `## Database Overview\n\n`; overview += `Total Collections: ${schemas.length}\n`; overview += `Total Relationships: ${relationships.length}\n\n`; overview += `## Collections\n\n`; schemas.forEach(schema => { overview += `- [${schema.collectionName}](${schema.collectionName}.md) (${schema.documentCount} documents)\n`; }); overview += `\n## Relationships\n\n`; overview += `See [relationships.md](relationships.md) for detailed relationship information.\n\n`; overview += `## Visual Database Diagram\n\n`; overview += `See [database-diagram.md](database-diagram.md) for visual ERD diagrams and database structure.\n\n`; return overview; } generateCollectionMarkdown(schema, relationships, embeddedRelationships) { let doc = `# ${schema.collectionName}\n\n`; doc += `**Document Count:** ${schema.documentCount}\n`; doc += `**Sample Size:** ${schema.sampleSize}\n\n`; doc += `## Schema\n\n`; doc += `| Field | Type(s) | Required | Nullable | Description |\n`; doc += `|-------|---------|----------|----------|-------------|\n`; for (const [fieldName, fieldInfo] of Object.entries(schema.fields)) { const types = fieldInfo.types.join(', '); const required = fieldInfo.required ? '✓' : '✗'; const nullable = fieldInfo.nullable ? '✓' : '✗'; const description = fieldInfo.description || ''; doc += `| ${fieldName} | ${types} | ${required} | ${nullable} | ${description} |\n`; } if (schema.indexes && schema.indexes.length > 0) { doc += `\n## Indexes\n\n`; doc += `| Name | Keys | Unique | Sparse |\n`; doc += `|------|------|--------|--------|\n`; schema.indexes.forEach(index => { const keys = JSON.stringify(index.keys); doc += `| ${index.name} | ${keys} | ${index.unique} | ${index.sparse} |\n`; }); } const outgoingRels = relationships.filter(rel => rel.fromCollection === schema.collectionName); const incomingRels = relationships.filter(rel => rel.toCollection === schema.collectionName); if (outgoingRels.length > 0 || incomingRels.length > 0) { doc += `\n## Relationships\n\n`; if (outgoingRels.length > 0) { doc += `### This Collection References (${outgoingRels.length})\n\n`; doc += `| Field | References | Type | Confidence |\n`; doc += `|-------|------------|------|------------|\n`; outgoingRels.forEach(rel => { doc += `| \`${rel.fromField}\` | [${rel.toCollection}](${rel.toCollection}.md).\`${rel.toField}\` | ${rel.relationshipType} | ${rel.confidence} |\n`; }); doc += `\n`; } if (incomingRels.length > 0) { doc += `### Referenced By (${incomingRels.length})\n\n`; doc += `| From Collection | From Field | Type | Confidence |\n`; doc += `|----------------|------------|------|------------|\n`; incomingRels.forEach(rel => { doc += `| [${rel.fromCollection}](${rel.fromCollection}.md) | \`${rel.fromField}\` | ${rel.relationshipType} | ${rel.confidence} |\n`; }); doc += `\n`; } } return doc; } generateRelationshipsMarkdown(relationships, embeddedRelationships) { let doc = `# Database Relationships\n\n`; if (relationships.length === 0) { doc += `No relationships detected in the database.\n\n`; doc += `This could be because:\n`; doc += `- The database uses application-level relationships\n`; doc += `- Field naming doesn't follow conventional patterns\n`; doc += `- Relationship detection was skipped for performance\n\n`; return doc; } if (relationships.length > 0) { doc += `## Reference Relationships (${relationships.length} found)\n\n`; doc += `| From Collection | From Field | To Collection | To Field | Relationship Type | Confidence | Detection Method |\n`; doc += `|----------------|------------|---------------|----------|-------------------|------------|------------------|\n`; relationships.forEach(rel => { const detectionMethod = rel.detected || 'Pattern matching'; doc += `| [${rel.fromCollection}](${rel.fromCollection}.md) | \`${rel.fromField}\` | [${rel.toCollection}](${rel.toCollection}.md) | \`${rel.toField}\` | ${rel.relationshipType} | ${rel.confidence} | ${detectionMethod} |\n`; }); doc += `\n### Relationship Types Explained\n`; doc += `- **many-to-one**: Multiple documents in the source collection can reference the same document in the target collection\n`; doc += `- **one-to-many**: One document in the source collection references multiple documents in the target collection\n`; doc += `- **one-to-one**: Each document in the source collection references exactly one document in the target collection\n\n`; doc += `### Confidence Levels\n`; doc += `- **high**: ObjectId field with verified reference\n`; doc += `- **medium**: String field following naming conventions\n`; doc += `- **low**: Inferred from field patterns\n\n`; } return doc; } generateComprehensiveMarkdown(schemas, relationships, embeddedRelationships) { let doc = `# Complete MongoDB Database Documentation\n\n`; doc += `Generated on: ${new Date().toISOString()}\n\n`; // Table of Contents doc += `## Table of Contents\n\n`; doc += `- [Database Overview](#database-overview)\n`; doc += `- [Collections](#collections)\n`; schemas.forEach((schema, index) => { if (schema.collectionName && schema.collectionName !== 'undefined' && schema.collectionName !== null) { doc += ` - [${index + 1}. ${schema.collectionName}](#${index + 1}-${schema.collectionName.toLowerCase()})\n`; } }); doc += `- [Database Relationships](#database-relationships)\n\n`; // Database Overview doc += `## Database Overview\n\n`; doc += `**Total Collections:** ${schemas.length}\n`; doc += `**Total Relationships:** ${relationships.length}\n\n`; const totalDocuments = schemas.reduce((sum, schema) => sum + schema.documentCount, 0); doc += `**Total Documents:** ${totalDocuments.toLocaleString()}\n\n`; doc += `### Collections Summary\n\n`; doc += `| Collection | Document Count | Sample Size |\n`; doc += `|------------|----------------|-------------|\n`; schemas.forEach((schema, index) => { if (schema.collectionName && schema.collectionName !== 'undefined' && schema.collectionName !== null) { doc += `| [${index + 1}. ${schema.collectionName}](#${index + 1}-${schema.collectionName.toLowerCase()}) | ${schema.documentCount.toLocaleString()} | ${schema.sampleSize} |\n`; } }); doc += `\n---\n\n`; // Collections doc += `## Collections\n\n`; schemas.forEach((schema, index) => { // Skip schemas with invalid collection names if (!schema.collectionName || schema.collectionName === 'undefined' || schema.collectionName === null) { return; } // Collection header with numbering doc += `## ${index + 1}. ${schema.collectionName}\n\n`; doc += `**Document Count:** ${schema.documentCount.toLocaleString()}\n`; doc += `**Sample Size:** ${schema.sampleSize}\n\n`; // Schema table doc += `#### Schema\n\n`; doc += `| Field | Type(s) | Required | Nullable | Description |\n`; doc += `|-------|---------|----------|----------|-------------|\n`; for (const [fieldName, fieldInfo] of Object.entries(schema.fields)) { const types = fieldInfo.types.join(', '); const required = fieldInfo.required ? '✓' : '✗'; const nullable = fieldInfo.nullable ? '✓' : '✗'; const description = fieldInfo.description || ''; doc += `| ${fieldName} | ${types} | ${required} | ${nullable} | ${description} |\n`; } // Indexes if (schema.indexes && schema.indexes.length > 0) { doc += `\n#### Indexes\n\n`; doc += `| Name | Keys | Unique | Sparse |\n`; doc += `|------|------|--------|--------|\n`; schema.indexes.forEach(index => { const keys = JSON.stringify(index.keys); doc += `| ${index.name} | ${keys} | ${index.unique} | ${index.sparse} |\n`; }); } // Relationships for this collection const outgoingRels = relationships.filter(rel => rel.fromCollection === schema.collectionName); const incomingRels = relationships.filter(rel => rel.toCollection === schema.collectionName); if (outgoingRels.length > 0 || incomingRels.length > 0) { doc += `\n#### Relationships\n\n`; if (outgoingRels.length > 0) { doc += `**This Collection References (${outgoingRels.length}):**\n\n`; doc += `| Field | References | Type | Confidence |\n`; doc += `|-------|------------|------|------------|\n`; outgoingRels.forEach(rel => { doc += `| \`${rel.fromField}\` | ${rel.toCollection}.\`${rel.toField}\` | ${rel.relationshipType} | ${rel.confidence} |\n`; }); doc += `\n`; } if (incomingRels.length > 0) { doc += `**Referenced By (${incomingRels.length}):**\n\n`; doc += `| From Collection | From Field | Type | Confidence |\n`; doc += `|----------------|------------|------|------------|\n`; incomingRels.forEach(rel => { doc += `| ${rel.fromCollection} | \`${rel.fromField}\` | ${rel.relationshipType} | ${rel.confidence} |\n`; }); doc += `\n`; } } // Add separator between collections (except for the last one) if (index < schemas.length - 1) { doc += `---\n\n`; } }); // Database Relationships Summary doc += `\n---\n\n`; doc += `## Database Relationships\n\n`; if (relationships.length === 0) { doc += `No relationships detected in the database.\n\n`; } else { doc += `### All Reference Relationships (${relationships.length} found)\n\n`; doc += `| From Collection | From Field | To Collection | To Field | Relationship Type | Confidence | Detection Method |\n`; doc += `|----------------|------------|---------------|----------|-------------------|------------|------------------|\n`; relationships.forEach(rel => { const detectionMethod = rel.detected || 'Pattern matching'; doc += `| ${rel.fromCollection} | \`${rel.fromField}\` | ${rel.toCollection} | \`${rel.toField}\` | ${rel.relationshipType} | ${rel.confidence} | ${detectionMethod} |\n`; }); doc += `\n### Relationship Types Explained\n`; doc += `- **many-to-one**: Multiple documents in the source collection can reference the same document in the target collection\n`; doc += `- **one-to-many**: One document in the source collection references multiple documents in the target collection\n`; doc += `- **one-to-one**: Each document in the source collection references exactly one document in the target collection\n\n`; doc += `### Confidence Levels\n`; doc += `- **high**: ObjectId field with verified reference\n`; doc += `- **medium**: String field following naming conventions\n`; doc += `- **low**: Inferred from field patterns\n\n`; } return doc; } generateDatabaseDiagram(schemas, relationships) { let doc = `# MongoDB Database Entity Relationship Diagram\n\n`; doc += `This document contains visual diagrams showing the structure and relationships of all collections in the MongoDB database.\n\n`; doc += `Generated on: ${new Date().toISOString()}\n\n`; // Complete Database Schema Diagram doc += `## Complete Database Schema Diagram\n\n`; doc += `\`\`\`mermaid\n`; doc += `erDiagram\n`; // Generate entity definitions schemas.forEach(schema => { if (!schema.collectionName || schema.collectionName === 'undefined' || schema.collectionName === null) { return; } doc += ` ${schema.collectionName} {\n`; // Add key fields (limit to most important ones for readability) const importantFields = []; for (const [fieldName, fieldInfo] of Object.entries(schema.fields)) { if (importantFields.length >= 8) break; // Limit fields for diagram readability let fieldLine = ` `; const types = fieldInfo.types.join('_'); const isKey = fieldName === '_id' ? ' PK' : (fieldName.includes('Id') || fieldName === 'company' || fieldName === 'customer' || fieldName === 'deployment' || fieldName === 'user' || fieldName === 'unit') ? ' FK' : ''; fieldLine += `${types} ${fieldName}${isKey}`; importantFields.push(fieldLine); } doc += importantFields.join('\n') + '\n'; doc += ` }\n\n`; }); // Generate relationships doc += ` %% Relationships\n`; const processedRelationships = new Set(); relationships.forEach(rel => { const relKey = `${rel.toCollection}-${rel.fromCollection}`; if (processedRelationships.has(relKey)) return; processedRelationships.add(relKey); // Convert many-to-one to one-to-many for better diagram readability const cardinality = rel.relationshipType === 'many-to-one' ? '||--o{' : '||--||'; doc += ` ${rel.toCollection} ${cardinality} ${rel.fromCollection} : "${rel.fromField}"\n`; }); doc += `\`\`\`\n\n`; // Simplified relationships diagram doc += `## Simplified Core Relationships Diagram\n\n`; doc += `\`\`\`mermaid\n`; doc += `erDiagram\n`; const coreEntities = ['companies', 'deployments', 'customer', 'users', 'units', 'equipment', 'tasks']; relationships.forEach(rel => { if (coreEntities.includes(rel.fromCollection) && coreEntities.includes(rel.toCollection)) { doc += ` ${rel.toCollection} ||--o{ ${rel.fromCollection} : ${rel.fromField}\n`; } }); doc += `\`\`\`\n\n`; // Collection categories doc += `## Collection Categories\n\n`; doc += `### 🏢 **Core Business Entities**\n`; const coreBusinessSchemas = schemas.filter(s => ['companies', 'customer', 'customers', 'users'].includes(s.collectionName)); coreBusinessSchemas.forEach(schema => { doc += `- **${schema.collectionName}** - (${schema.documentCount.toLocaleString()} records)\n`; }); doc += `\n### 🚀 **Deployment & Operations**\n`; const deploymentSchemas = schemas.filter(s => ['deployments', 'units', 'equipment', 'libraries', 'equipmenttypes'].includes(s.collectionName)); deploymentSchemas.forEach(schema => { doc += `- **${schema.collectionName}** - (${schema.documentCount.toLocaleString()} records)\n`; }); doc += `\n### 📋 **Task & Workflow Management**\n`; const workflowSchemas = schemas.filter(s => ['tasks', 'histories', 'blueprints'].includes(s.collectionName)); workflowSchemas.forEach(schema => { doc += `- **${schema.collectionName}** - (${schema.documentCount.toLocaleString()} records)\n`; }); doc += `\n### 🔔 **Monitoring & Alerts**\n`; const monitoringSchemas = schemas.filter(s => ['notifications', 'telemetries', 'telemetryrecords', 'eventdatas'].includes(s.collectionName)); monitoringSchemas.forEach(schema => { doc += `- **${schema.collectionName}** - (${schema.documentCount.toLocaleString()} records)\n`; }); doc += `\n### ⚙️ **Configuration & System**\n`; const systemSchemas = schemas.filter(s => ['settings', 'addresses', 'tokens', 'helps', 'storagefiles'].includes(s.collectionName)); systemSchemas.forEach(schema => { doc += `- **${schema.collectionName}** - (${schema.documentCount.toLocaleString()} records)\n`; }); // Key relationship patterns doc += `\n## Key Relationship Patterns\n\n`; doc += `### 🌟 **Hub Tables (High Connectivity)**\n`; // Calculate relationship counts for each collection const relationshipCounts = new Map(); relationships.forEach(rel => { relationshipCounts.set(rel.toCollection, (relationshipCounts.get(rel.toCollection) || 0) + 1); relationshipCounts.set(rel.fromCollection, (relationshipCounts.get(rel.fromCollection) || 0) + 1); }); // Sort by relationship count and show top hubs const sortedHubs = Array.from(relationshipCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3); sortedHubs.forEach((hub, index) => { doc += `${index + 1}. **${hub[0]}** - ${hub[1]} relationships\n`; }); // Database statistics doc += `\n## Database Statistics\n`; doc += `- **Total Collections:** ${schemas.length}\n`; doc += `- **Total Relationships:** ${relationships.length}\n`; doc += `- **Total Documents:** ${schemas.reduce((sum, schema) => sum + schema.documentCount, 0).toLocaleString()}\n`; doc += `- **Detection Method:** ObjectId field pattern matching\n`; return doc; } async generateJSONDocs(schemas, relationships) { const documentation = { generatedAt: new Date().toISOString(), schemas, relationships, summary: { totalCollections: schemas.length, totalRelationships: relationships.length } }; await fs.writeFile( path.join(this.outputDir, 'database-documentation.json'), JSON.stringify(documentation, null, 2) ); } async generateHTMLDocs(schemas, relationships, embeddedRelationships) { const html = this.generateHTMLContent(schemas, relationships, embeddedRelationships); await fs.writeFile(path.join(this.outputDir, 'database-documentation.html'), html); } generateHTMLContent(schemas, relationships, embeddedRelationships) { return ` <!DOCTYPE html> <html> <head> <title>MongoDB Database Documentation</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } table { border-collapse: collapse; width: 100%; margin: 20px 0; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } .collection { margin: 30px 0; } .field-type { color: #666; font-style: italic; } .relationship { background-color: #f9f9f9; padding: 10px; margin: 10px 0; } </style> </head> <body> <h1>MongoDB Database Documentation</h1> <p>Generated on: ${new Date().toISOString()}</p> <h2>Overview</h2> <ul> <li>Total Collections: ${schemas.length}</li> <li>Total Relationships: ${relationships.length}</li> </ul> ${schemas.map(schema => this.generateCollectionHTML(schema, relationships, embeddedRelationships)).join('')} <h2>Relationships Summary</h2> <table> <tr> <th>From Collection</th> <th>From Field</th> <th>To Collection</th> <th>To Field</th> <th>Type</th> <th>Confidence</th> </tr> ${relationships.map(rel => ` <tr> <td>${rel.fromCollection}</td> <td>${rel.fromField}</td> <td>${rel.toCollection}</td> <td>${rel.toField}</td> <td>${rel.relationshipType}</td> <td>${rel.confidence}</td> </tr> `).join('')} </table> </body> </html>`; } generateCollectionHTML(schema, relationships) { const outgoingRels = relationships.filter(rel => rel.fromCollection === schema.collectionName); const incomingRels = relationships.filter(rel => rel.toCollection === schema.collectionName); return ` <div class="collection"> <h2>${schema.collectionName}</h2> <p><strong>Document Count:</strong> ${schema.documentCount}</p> <p><strong>Sample Size:</strong> ${schema.sampleSize}</p> <h3>Schema</h3> <table> <tr> <th>Field</th> <th>Type(s)</th> <th>Required</th> <th>Nullable</th> <th>Description</th> </tr> ${Object.entries(schema.fields).map(([fieldName, fieldInfo]) => ` <tr> <td>${fieldName}</td> <td class="field-type">${fieldInfo.types.join(', ')}</td> <td>${fieldInfo.required ? '✓' : '✗'}</td> <td>${fieldInfo.nullable ? '✓' : '✗'}</td> <td>${fieldInfo.description || ''}</td> </tr> `).join('')} </table> ${outgoingRels.length > 0 || incomingRels.length > 0 ? ` <h3>Relationships</h3> ${outgoingRels.map(rel => ` <div class="relationship"> <strong>References:</strong> ${rel.fromField}${rel.toCollection}.${rel.toField} (${rel.relationshipType}) </div> `).join('')} ${incomingRels.map(rel => ` <div class="relationship"> <strong>Referenced by:</strong> ${rel.fromCollection}.${rel.fromField}${rel.toField} (${rel.relationshipType}) </div> `).join('')} ` : ''} </div>`; } } module.exports = DocumentationGenerator;