mongoose-database-schema
Version:
MongoDB database documentation generator with table schemas and relationships
187 lines (154 loc) • 5.7 kB
JavaScript
class RelationshipDetector {
constructor(db) {
this.db = db;
this.relationships = [];
}
async detectRelationships(schemas) {
this.relationships = [];
console.log(' Analyzing collection relationships...');
// First, detect obvious ObjectId references
await this.detectObjectIdReferences(schemas);
// Then detect string-based references
await this.detectStringReferences(schemas);
return this.relationships;
}
async detectObjectIdReferences(schemas) {
console.log(' Detecting ObjectId references...');
for (const schema of schemas) {
for (const [fieldName, field] of Object.entries(schema.fields)) {
if (field.types.includes('objectId') && fieldName !== '_id') {
// Check if this ObjectId field references another collection
const possibleTargetCollection = this.guessTargetCollection(fieldName, schemas);
if (possibleTargetCollection) {
// Quick verification with one sample
const isValid = await this.quickVerifyObjectIdReference(
schema.collectionName,
fieldName,
possibleTargetCollection
);
if (isValid) {
this.relationships.push({
type: 'reference',
fromCollection: schema.collectionName,
fromField: fieldName,
toCollection: possibleTargetCollection,
toField: '_id',
relationshipType: 'many-to-one',
confidence: 'high',
detected: 'ObjectId field pattern'
});
}
}
}
}
}
}
async detectStringReferences(schemas) {
console.log(' Detecting string-based references...');
for (const schema of schemas) {
for (const [fieldName, field] of Object.entries(schema.fields)) {
if (field.types.includes('string') && fieldName.endsWith('_id')) {
// Extract potential collection name from field name
const baseName = fieldName.replace('_id', '');
const possibleTargetCollection = this.findCollectionByName(baseName, schemas);
if (possibleTargetCollection) {
this.relationships.push({
type: 'reference',
fromCollection: schema.collectionName,
fromField: fieldName,
toCollection: possibleTargetCollection,
toField: '_id',
relationshipType: 'many-to-one',
confidence: 'medium',
detected: 'String field naming pattern'
});
}
}
}
}
}
guessTargetCollection(fieldName, schemas) {
// Common patterns: company, customer, user, etc.
const collectionNames = schemas.map(s => s.collectionName);
// Direct match
if (collectionNames.includes(fieldName)) {
return fieldName;
}
// Pluralize and check
const pluralized = this.pluralize(fieldName);
if (collectionNames.includes(pluralized)) {
return pluralized;
}
// Singularize and check
const singularized = this.singularize(fieldName);
if (collectionNames.includes(singularized)) {
return singularized;
}
return null;
}
findCollectionByName(baseName, schemas) {
const collectionNames = schemas.map(s => s.collectionName);
// Try exact match
if (collectionNames.includes(baseName)) {
return baseName;
}
// Try pluralized
const pluralized = this.pluralize(baseName);
if (collectionNames.includes(pluralized)) {
return pluralized;
}
// Try singularized
const singularized = this.singularize(baseName);
if (collectionNames.includes(singularized)) {
return singularized;
}
return null;
}
pluralize(word) {
if (word.endsWith('y')) return word.slice(0, -1) + 'ies';
if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z')) return word + 'es';
return word + 's';
}
async quickVerifyObjectIdReference(fromCollection, fromField, toCollection) {
try {
const fromColl = this.db.collection(fromCollection);
const toColl = this.db.collection(toCollection);
// Just check if the field exists and the target collection exists
const hasField = await fromColl.findOne(
{ [fromField]: { $exists: true, $type: "objectId" } },
{ projection: { [fromField]: 1 }, timeout: 3000 }
);
if (!hasField) return false;
// Check if target collection has documents
const hasTargetDocs = await toColl.findOne({}, { projection: { _id: 1 }, timeout: 3000 });
return !!hasTargetDocs;
} catch (error) {
return false;
}
}
singularize(word) {
if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
if (word.endsWith('es')) return word.slice(0, -2);
if (word.endsWith('s')) return word.slice(0, -1);
return word;
}
async detectEmbeddedRelationships(schemas) {
const embeddedRelationships = [];
for (const schema of schemas) {
for (const fieldName in schema.fields) {
if (fieldName.includes('.') && !fieldName.includes('[]')) {
const parentField = fieldName.split('.')[0];
embeddedRelationships.push({
type: 'embedded',
collection: schema.collectionName,
parentField,
embeddedField: fieldName,
relationshipType: 'one-to-one'
});
}
}
}
return embeddedRelationships;
}
}
module.exports = RelationshipDetector;