mongoose-database-schema
Version:
MongoDB database documentation generator with table schemas and relationships
544 lines (452 loc) • 23.7 kB
JavaScript
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;