#!/usr/bin/env node /** * Script to update all MongoDB models to include tenantId field * This script adds tenant isolation to all existing models */ import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Models that should NOT have tenantId (system-wide models) const EXCLUDED_MODELS = [ 'Tenant.js', 'Language.js', // System languages are shared ]; // Models that need special handling const SPECIAL_MODELS = { 'User.js': 'Already updated with tenantId', 'Role.js': 'System roles are shared across tenants', }; async function updateModel(filePath) { const fileName = path.basename(filePath); // Skip excluded models if (EXCLUDED_MODELS.includes(fileName)) { console.log(`⏩ Skipping ${fileName} (excluded)`); return; } // Skip special models if (SPECIAL_MODELS[fileName]) { console.log(`⏩ Skipping ${fileName} (${SPECIAL_MODELS[fileName]})`); return; } try { let content = await fs.readFile(filePath, 'utf8'); // Check if model already has tenantId if (content.includes('tenantId:')) { console.log(`✅ ${fileName} already has tenantId`); return; } // Find the schema definition const schemaPattern = /const\s+\w+Schema\s*=\s*new\s+(?:mongoose\.)?Schema\s*\(\s*\{/; const match = content.match(schemaPattern); if (!match) { console.log(`⚠️ ${fileName} - Could not find schema definition`); return; } // Insert tenantId field after schema opening const insertPosition = match.index + match[0].length; const tenantIdField = ` // Multi-tenant support tenantId: { type: mongoose.Schema.Types.ObjectId, ref: 'Tenant', required: true, index: true },`; content = content.slice(0, insertPosition) + tenantIdField + content.slice(insertPosition); // Update indexes to include tenantId // Find existing index definitions const indexPattern = /(\w+Schema\.index\s*\(\s*\{[^}]+\}\s*(?:,\s*\{[^}]+\})?\s*\);?)/g; let indexMatches = [...content.matchAll(indexPattern)]; if (indexMatches.length > 0) { // Add compound indexes with tenantId let additionalIndexes = '\n\n// Multi-tenant indexes'; indexMatches.forEach(match => { const indexDef = match[1]; // Extract the index fields const fieldsMatch = indexDef.match(/\{([^}]+)\}/); if (fieldsMatch) { const fields = fieldsMatch[1].trim(); // Skip if already includes tenantId if (!fields.includes('tenantId')) { // Create compound index with tenantId const newIndex = indexDef.replace(/\{([^}]+)\}/, '{ tenantId: 1, $1 }'); additionalIndexes += '\n' + newIndex; } } }); // Find where to insert the new indexes (after existing indexes) const lastIndexMatch = indexMatches[indexMatches.length - 1]; const insertPos = lastIndexMatch.index + lastIndexMatch[0].length; content = content.slice(0, insertPos) + additionalIndexes + content.slice(insertPos); } else { // No existing indexes, add basic tenantId index after schema definition const schemaEndPattern = /}\s*(?:,\s*\{[^}]+\})?\s*\);/; const schemaEndMatch = content.match(schemaEndPattern); if (schemaEndMatch) { const insertPos = schemaEndMatch.index + schemaEndMatch[0].length; const basicIndex = '\n\n// Multi-tenant index\n' + fileName.replace('.js', '') + 'Schema.index({ tenantId: 1 });'; content = content.slice(0, insertPos) + basicIndex + content.slice(insertPos); } } // Update unique indexes to be unique within tenant content = content.replace( /index\s*\(\s*\{([^}]+)\}\s*,\s*\{\s*unique:\s*true\s*\}\s*\)/g, (match, fields) => { if (!fields.includes('tenantId')) { return `index({ tenantId: 1, ${fields} }, { unique: true })`; } return match; } ); // Save the updated file await fs.writeFile(filePath, content, 'utf8'); console.log(`✨ Updated ${fileName} with tenantId support`); } catch (error) { console.error(`❌ Error updating ${filePath}:`, error.message); } } async function findModels(dir) { const models = []; async function walk(currentDir) { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { // Skip node_modules and other non-service directories if (!entry.name.includes('node_modules') && !entry.name.startsWith('.') && entry.name !== 'scripts' && entry.name !== 'frontend') { await walk(fullPath); } } else if (entry.isFile() && entry.name.endsWith('.js')) { // Check if it's in a models directory if (currentDir.includes('/models')) { models.push(fullPath); } } } } await walk(dir); return models; } async function main() { try { console.log('🔍 Finding all model files...'); const projectRoot = path.join(__dirname, '..'); const models = await findModels(path.join(projectRoot, 'services')); console.log(`\n📋 Found ${models.length} model files\n`); for (const model of models) { await updateModel(model); } console.log('\n✅ Model update complete!'); console.log('\n⚠️ Important next steps:'); console.log('1. Review the changes to ensure they are correct'); console.log('2. Update all queries to include tenantId filtering'); console.log('3. Update all create operations to include tenantId'); console.log('4. Test thoroughly to ensure tenant isolation works'); } catch (error) { console.error('❌ Script failed:', error); process.exit(1); } } // Run the script main();