// Setup module path to include backend node_modules const path = require('path'); const backendPath = path.join(__dirname, '..', 'backend'); process.env.NODE_PATH = path.join(backendPath, 'node_modules') + ':' + (process.env.NODE_PATH || ''); require('module').Module._initPaths(); const mongoose = require('mongoose'); const Nano = require('nano'); // MongoDB models (only needed for migration) const User = require('../backend/models/User'); const Street = require('../backend/models/Street'); const Task = require('../backend/models/Task'); const Post = require('../backend/models/Post'); const Event = require('../backend/models/Event'); const Report = require('../backend/models/Report'); const Badge = require('../backend/models/Badge'); const Comment = require('../backend/models/Comment'); const PointTransaction = require('../backend/models/PointTransaction'); const UserBadge = require('../backend/models/UserBadge'); // Configuration const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/adopt-a-street'; const COUCHDB_URL = process.env.COUCHDB_URL || 'http://localhost:5984'; const COUCHDB_DB = process.env.COUCHDB_DB || 'adopt-a-street'; class MongoToCouchMigrator { constructor() { this.nano = Nano(COUCHDB_URL); this.db = null; this.migrationStats = { users: 0, streets: 0, tasks: 0, posts: 0, events: 0, reports: 0, badges: 0, comments: 0, pointTransactions: 0, userBadges: 0, errors: [] }; } async initialize() { console.log('šŸš€ Initializing migration...'); // Connect to MongoDB await mongoose.connect(MONGO_URI); console.log('āœ… Connected to MongoDB'); // Initialize CouchDB try { this.db = this.nano.db.use(COUCHDB_DB); console.log('āœ… Connected to existing CouchDB database'); } catch (error) { if (error.statusCode === 404) { await this.nano.db.create(COUCHDB_DB); this.db = this.nano.db.use(COUCHDB_DB); console.log('āœ… Created new CouchDB database'); } else { throw error; } } // Create indexes await this.createIndexes(); console.log('āœ… Created CouchDB indexes'); } async createIndexes() { const indexes = [ { index: { fields: ['type', 'email'] }, name: 'user-by-email', type: 'json' }, { index: { fields: ['type', 'location'] }, name: 'streets-by-location', type: 'json' }, { index: { fields: ['type', 'user.userId'] }, name: 'by-user', type: 'json' }, { index: { fields: ['type', 'points'] }, name: 'users-by-points', type: 'json' }, { index: { fields: ['type', 'createdAt'] }, name: 'posts-by-date', type: 'json' }, { index: { fields: ['type', 'status'] }, name: 'streets-by-status', type: 'json' }, { index: { fields: ['type', 'date', 'status'] }, name: 'events-by-date-status', type: 'json' }, { index: { fields: ['type', 'post.postId'] }, name: 'comments-by-post', type: 'json' }, { index: { fields: ['type', 'user.userId', 'createdAt'] }, name: 'transactions-by-user-date', type: 'json' } ]; for (const index of indexes) { try { await this.db.createIndex(index); } catch (error) { console.warn(`āš ļø Index creation failed: ${index.name}`, error.message); } } } transformUser(mongoUser) { return { _id: `user_${mongoUser._id}`, type: 'user', name: mongoUser.name, email: mongoUser.email, password: mongoUser.password, isPremium: mongoUser.isPremium || false, points: mongoUser.points || 0, profilePicture: mongoUser.profilePicture, cloudinaryPublicId: mongoUser.cloudinaryPublicId, adoptedStreets: mongoUser.adoptedStreets.map(id => `street_${id}`), completedTasks: mongoUser.completedTasks.map(id => `task_${id}`), posts: mongoUser.posts.map(id => `post_${id}`), events: mongoUser.events.map(id => `event_${id}`), earnedBadges: [], // Will be populated from UserBadge collection stats: { streetsAdopted: mongoUser.adoptedStreets.length, tasksCompleted: mongoUser.completedTasks.length, postsCreated: mongoUser.posts.length, eventsParticipated: mongoUser.events.length, badgesEarned: 0 }, createdAt: mongoUser.createdAt, updatedAt: mongoUser.updatedAt }; } transformStreet(mongoStreet) { return { _id: `street_${mongoStreet._id}`, type: 'street', name: mongoStreet.name, location: mongoStreet.location, adoptedBy: mongoStreet.adoptedBy ? { userId: `user_${mongoStreet.adoptedBy}`, name: '', // Will be populated from user data profilePicture: '' } : null, status: mongoStreet.status, stats: { tasksCount: 0, // Will be calculated completedTasksCount: 0, // Will be calculated reportsCount: 0, // Will be calculated openReportsCount: 0 // Will be calculated }, createdAt: mongoStreet.createdAt, updatedAt: mongoStreet.updatedAt }; } transformTask(mongoTask) { return { _id: `task_${mongoTask._id}`, type: 'task', street: { streetId: `street_${mongoTask.street}`, name: '', // Will be populated from street data location: null // Will be populated from street data }, description: mongoTask.description, completedBy: mongoTask.completedBy ? { userId: `user_${mongoTask.completedBy}`, name: '', // Will be populated from user data profilePicture: '' } : null, status: mongoTask.status, completedAt: mongoTask.updatedAt, // Approximation pointsAwarded: 10, // Default, could be made configurable createdAt: mongoTask.createdAt, updatedAt: mongoTask.updatedAt }; } transformPost(mongoPost) { return { _id: `post_${mongoPost._id}`, type: 'post', user: { userId: `user_${mongoPost.user}`, name: '', // Will be populated from user data profilePicture: '' }, content: mongoPost.content, imageUrl: mongoPost.imageUrl, cloudinaryPublicId: mongoPost.cloudinaryPublicId, likes: mongoPost.likes.map(id => `user_${id}`), likesCount: mongoPost.likes.length, commentsCount: mongoPost.commentsCount || 0, createdAt: mongoPost.createdAt, updatedAt: mongoPost.updatedAt }; } transformEvent(mongoEvent) { return { _id: `event_${mongoEvent._id}`, type: 'event', title: mongoEvent.title, description: mongoEvent.description, date: mongoEvent.date, location: mongoEvent.location, participants: mongoEvent.participants.map(userId => ({ userId: `user_${userId}`, name: '', // Will be populated from user data profilePicture: '', joinedAt: mongoEvent.createdAt // Approximation })), participantsCount: mongoEvent.participants.length, status: mongoEvent.status, createdAt: mongoEvent.createdAt, updatedAt: mongoEvent.updatedAt }; } transformReport(mongoReport) { return { _id: `report_${mongoReport._id}`, type: 'report', street: { streetId: `street_${mongoReport.street}`, name: '', // Will be populated from street data location: null // Will be populated from street data }, user: { userId: `user_${mongoReport.user}`, name: '', // Will be populated from user data profilePicture: '' }, issue: mongoReport.issue, imageUrl: mongoReport.imageUrl, cloudinaryPublicId: mongoReport.cloudinaryPublicId, status: mongoReport.status, createdAt: mongoReport.createdAt, updatedAt: mongoReport.updatedAt }; } transformBadge(mongoBadge) { return { _id: `badge_${mongoBadge._id}`, type: 'badge', name: mongoBadge.name, description: mongoBadge.description, icon: mongoBadge.icon, criteria: mongoBadge.criteria, rarity: mongoBadge.rarity, order: mongoBadge.order || 0, isActive: true, createdAt: mongoBadge.createdAt, updatedAt: mongoBadge.updatedAt }; } transformComment(mongoComment) { return { _id: `comment_${mongoComment._id}`, type: 'comment', post: { postId: `post_${mongoComment.post}`, content: '', // Will be populated from post data userId: '' // Will be populated from post data }, user: { userId: `user_${mongoComment.user}`, name: '', // Will be populated from user data profilePicture: '' }, content: mongoComment.content, createdAt: mongoComment.createdAt, updatedAt: mongoComment.updatedAt }; } transformPointTransaction(mongoTransaction) { return { _id: `transaction_${mongoTransaction._id}`, type: 'point_transaction', user: { userId: `user_${mongoTransaction.user}`, name: '' // Will be populated from user data }, amount: mongoTransaction.amount, type: mongoTransaction.type, description: mongoTransaction.description, relatedEntity: mongoTransaction.relatedEntity ? { entityType: mongoTransaction.relatedEntity.entityType, entityId: mongoTransaction.relatedEntity.entityId ? `${mongoTransaction.relatedEntity.entityType.toLowerCase()}_${mongoTransaction.relatedEntity.entityId}` : null, entityName: '' // Will be populated if possible } : null, balanceAfter: mongoTransaction.balanceAfter, createdAt: mongoTransaction.createdAt }; } transformUserBadge(mongoUserBadge) { return { _id: `userbadge_${mongoUserBadge._id}`, type: 'user_badge', userId: `user_${mongoUserBadge.user}`, badgeId: `badge_${mongoUserBadge.badge}`, earnedAt: mongoUserBadge.earnedAt, progress: mongoUserBadge.progress, createdAt: mongoUserBadge.createdAt, updatedAt: mongoUserBadge.updatedAt }; } async migrateCollection(mongoModel, transformer, collectionName) { console.log(`šŸ“¦ Migrating ${collectionName}...`); try { const documents = await mongoModel.find().lean(); const transformedDocs = documents.map(transformer.bind(this)); // Batch insert to CouchDB const batchSize = 100; for (let i = 0; i < transformedDocs.length; i += batchSize) { const batch = transformedDocs.slice(i, i + batchSize); for (const doc of batch) { try { await this.db.insert(doc); this.migrationStats[collectionName]++; } catch (error) { if (error.statusCode === 409) { // Document already exists, update it const existing = await this.db.get(doc._id); doc._rev = existing._rev; await this.db.insert(doc); this.migrationStats[collectionName]++; } else { console.error(`āŒ Error inserting ${collectionName} ${doc._id}:`, error.message); this.migrationStats.errors.push(`${collectionName} ${doc._id}: ${error.message}`); } } } } console.log(`āœ… Migrated ${this.migrationStats[collectionName]} ${collectionName}`); } catch (error) { console.error(`āŒ Error migrating ${collectionName}:`, error.message); this.migrationStats.errors.push(`Collection ${collectionName}: ${error.message}`); } } async populateRelationships() { console.log('šŸ”— Populating relationships...'); // Get all users for lookup const users = await this.db.find({ selector: { type: 'user' }, fields: ['_id', 'name', 'profilePicture'] }); const userMap = {}; users.docs.forEach(user => { userMap[user._id] = { name: user.name, profilePicture: user.profilePicture || '' }; }); // Get all streets for lookup const streets = await this.db.find({ selector: { type: 'street' }, fields: ['_id', 'name', 'location'] }); const streetMap = {}; streets.docs.forEach(street => { streetMap[street._id] = { name: street.name, location: street.location }; }); // Update streets with adopter info for (const street of streets.docs) { if (street.adoptedBy) { const adopterInfo = userMap[street.adoptedBy.userId]; if (adopterInfo) { street.adoptedBy.name = adopterInfo.name; street.adoptedBy.profilePicture = adopterInfo.profilePicture; await this.db.insert(street); } } } // Update tasks with street and user info const tasks = await this.db.find({ selector: { type: 'task' }, fields: ['_id', 'street', 'completedBy'] }); for (const task of tasks.docs) { let updated = false; if (task.street && streetMap[task.street.streetId]) { task.street.name = streetMap[task.street.streetId].name; task.street.location = streetMap[task.street.streetId].location; updated = true; } if (task.completedBy && userMap[task.completedBy.userId]) { task.completedBy.name = userMap[task.completedBy.userId].name; task.completedBy.profilePicture = userMap[task.completedBy.userId].profilePicture; updated = true; } if (updated) { await this.db.insert(task); } } // Update posts with user info const posts = await this.db.find({ selector: { type: 'post' }, fields: ['_id', 'user'] }); for (const post of posts.docs) { if (userMap[post.user.userId]) { post.user.name = userMap[post.user.userId].name; post.user.profilePicture = userMap[post.user.userId].profilePicture; await this.db.insert(post); } } // Update events with participant info const events = await this.db.find({ selector: { type: 'event' }, fields: ['_id', 'participants'] }); for (const event of events.docs) { let updated = false; for (const participant of event.participants) { if (userMap[participant.userId]) { participant.name = userMap[participant.userId].name; participant.profilePicture = userMap[participant.userId].profilePicture; updated = true; } } if (updated) { await this.db.insert(event); } } // Update reports with street and user info const reports = await this.db.find({ selector: { type: 'report' }, fields: ['_id', 'street', 'user'] }); for (const report of reports.docs) { let updated = false; if (report.street && streetMap[report.street.streetId]) { report.street.name = streetMap[report.street.streetId].name; report.street.location = streetMap[report.street.streetId].location; updated = true; } if (report.user && userMap[report.user.userId]) { report.user.name = userMap[report.user.userId].name; report.user.profilePicture = userMap[report.user.userId].profilePicture; updated = true; } if (updated) { await this.db.insert(report); } } console.log('āœ… Relationships populated'); } async calculateStats() { console.log('šŸ“Š Calculating statistics...'); // Calculate street stats const streets = await this.db.find({ selector: { type: 'street' }, fields: ['_id'] }); for (const street of streets.docs) { const tasks = await this.db.find({ selector: { type: 'task', 'street.streetId': street._id }, fields: ['status'] }); const reports = await this.db.find({ selector: { type: 'report', 'street.streetId': street._id }, fields: ['status'] }); const streetDoc = await this.db.get(street._id); streetDoc.stats = { tasksCount: tasks.docs.length, completedTasksCount: tasks.docs.filter(t => t.status === 'completed').length, reportsCount: reports.docs.length, openReportsCount: reports.docs.filter(r => r.status === 'open').length }; await this.db.insert(streetDoc); } // Populate user badges const userBadges = await this.db.find({ selector: { type: 'user_badge' }, fields: ['userId', 'badgeId', 'earnedAt', 'progress'] }); const badges = await this.db.find({ selector: { type: 'badge' }, fields: ['_id', 'name', 'description', 'icon', 'rarity'] }); const badgeMap = {}; badges.docs.forEach(badge => { badgeMap[badge._id] = { badgeId: badge._id, name: badge.name, description: badge.description, icon: badge.icon, rarity: badge.rarity }; }); const userBadgeMap = {}; userBadges.docs.forEach(userBadge => { if (!userBadgeMap[userBadge.userId]) { userBadgeMap[userBadge.userId] = []; } const badgeInfo = badgeMap[userBadge.badgeId]; if (badgeInfo) { userBadgeMap[userBadge.userId].push({ ...badgeInfo, earnedAt: userBadge.earnedAt, progress: userBadge.progress }); } }); // Update users with badge info for (const userId in userBadgeMap) { const userDoc = await this.db.get(userId); userDoc.earnedBadges = userBadgeMap[userId]; userDoc.stats.badgesEarned = userBadgeMap[userId].length; await this.db.insert(userDoc); } console.log('āœ… Statistics calculated'); } async runMigration() { try { await this.initialize(); // Phase 1: Migrate basic documents await this.migrateCollection(User, this.transformUser, 'users'); await this.migrateCollection(Street, this.transformStreet, 'streets'); await this.migrateCollection(Task, this.transformTask, 'tasks'); await this.migrateCollection(Post, this.transformPost, 'posts'); await this.migrateCollection(Event, this.transformEvent, 'events'); await this.migrateCollection(Report, this.transformReport, 'reports'); await this.migrateCollection(Badge, this.transformBadge, 'badges'); await this.migrateCollection(Comment, this.transformComment, 'comments'); await this.migrateCollection(PointTransaction, this.transformPointTransaction, 'pointTransactions'); await this.migrateCollection(UserBadge, this.transformUserBadge, 'userBadges'); // Phase 2: Populate relationships await this.populateRelationships(); // Phase 3: Calculate statistics await this.calculateStats(); console.log('\nšŸŽ‰ Migration completed!'); console.log('\nšŸ“ˆ Migration Statistics:'); console.log(`Users: ${this.migrationStats.users}`); console.log(`Streets: ${this.migrationStats.streets}`); console.log(`Tasks: ${this.migrationStats.tasks}`); console.log(`Posts: ${this.migrationStats.posts}`); console.log(`Events: ${this.migrationStats.events}`); console.log(`Reports: ${this.migrationStats.reports}`); console.log(`Badges: ${this.migrationStats.badges}`); console.log(`Comments: ${this.migrationStats.comments}`); console.log(`Point Transactions: ${this.migrationStats.pointTransactions}`); console.log(`User Badges: ${this.migrationStats.userBadges}`); if (this.migrationStats.errors.length > 0) { console.log('\nāš ļø Errors encountered:'); this.migrationStats.errors.forEach(error => console.log(` - ${error}`)); } } catch (error) { console.error('āŒ Migration failed:', error); throw error; } finally { await mongoose.disconnect(); console.log('šŸ”Œ Disconnected from MongoDB'); } } } // Run migration if called directly if (require.main === module) { const migrator = new MongoToCouchMigrator(); migrator.runMigration().catch(console.error); } module.exports = MongoToCouchMigrator;