UNPKG

mongoose-database-schema

Version:

MongoDB database documentation generator with table schemas and relationships

187 lines (154 loc) 5.7 kB
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;