#!/usr/bin/env node require("dotenv").config(); const mongoose = require("mongoose"); const couchdbService = require("../services/couchdbService"); // MongoDB models const User = require("../models/User"); const Street = require("../models/Street"); const Task = require("../models/Task"); const Post = require("../models/Post"); const Event = require("../models/Event"); const Report = require("../models/Report"); const Reward = require("../models/Reward"); const Badge = require("../models/Badge"); const Comment = require("../models/Comment"); const PointTransaction = require("../models/PointTransaction"); const UserBadge = require("../models/UserBadge"); class MigrationService { constructor() { this.stats = { users: 0, streets: 0, tasks: 0, posts: 0, events: 0, reports: 0, rewards: 0, badges: 0, comments: 0, pointTransactions: 0, userBadges: 0, errors: 0 }; } async initialize() { console.log("šŸš€ Starting MongoDB to CouchDB migration..."); // Connect to MongoDB try { await mongoose.connect(process.env.MONGO_URI); console.log("āœ… Connected to MongoDB"); } catch (error) { console.error("āŒ Failed to connect to MongoDB:", error.message); process.exit(1); } // Initialize CouchDB try { await couchdbService.initialize(); console.log("āœ… Connected to CouchDB"); } catch (error) { console.error("āŒ Failed to connect to CouchDB:", error.message); process.exit(1); } } // Transform functions for each document type transformUser(mongoUser) { return { _id: couchdbService.generateId("user", mongoUser._id.toString()), 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 => couchdbService.generateId("street", id.toString()) ), completedTasks: (mongoUser.completedTasks || []).map(id => couchdbService.generateId("task", id.toString()) ), posts: (mongoUser.posts || []).map(id => couchdbService.generateId("post", id.toString()) ), events: (mongoUser.events || []).map(id => couchdbService.generateId("event", id.toString()) ), 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 // Will be calculated from UserBadge collection }, createdAt: mongoUser.createdAt || new Date().toISOString(), updatedAt: mongoUser.updatedAt || new Date().toISOString() }; } transformStreet(mongoStreet) { return { _id: couchdbService.generateId("street", mongoStreet._id.toString()), type: "street", name: mongoStreet.name, location: mongoStreet.location, adoptedBy: mongoStreet.adoptedBy ? { userId: couchdbService.generateId("user", mongoStreet.adoptedBy.toString()), name: "", // Will be populated in relationship resolution profilePicture: "" } : null, status: mongoStreet.adoptedBy ? "adopted" : "available", stats: { tasksCount: 0, // Will be calculated from Task collection completedTasksCount: 0, // Will be calculated from Task collection reportsCount: 0, // Will be calculated from Report collection openReportsCount: 0 // Will be calculated from Report collection }, createdAt: mongoStreet.createdAt || new Date().toISOString(), updatedAt: mongoStreet.updatedAt || new Date().toISOString() }; } transformTask(mongoTask) { return { _id: couchdbService.generateId("task", mongoTask._id.toString()), type: "task", street: { streetId: couchdbService.generateId("street", mongoTask.street.toString()), name: "", // Will be populated in relationship resolution location: null // Will be populated in relationship resolution }, description: mongoTask.description, completedBy: mongoTask.completedBy ? { userId: couchdbService.generateId("user", mongoTask.completedBy.toString()), name: "", // Will be populated in relationship resolution profilePicture: "" } : null, status: mongoTask.completedBy ? "completed" : "pending", completedAt: mongoTask.completedAt, pointsAwarded: mongoTask.pointsAwarded || 10, createdAt: mongoTask.createdAt || new Date().toISOString(), updatedAt: mongoTask.updatedAt || new Date().toISOString() }; } transformPost(mongoPost) { return { _id: couchdbService.generateId("post", mongoPost._id.toString()), type: "post", user: { userId: couchdbService.generateId("user", mongoPost.user.toString()), name: "", // Will be populated in relationship resolution profilePicture: "" }, content: mongoPost.content, imageUrl: mongoPost.imageUrl, cloudinaryPublicId: mongoPost.cloudinaryPublicId, likes: (mongoPost.likes || []).map(id => couchdbService.generateId("user", id.toString()) ), likesCount: (mongoPost.likes || []).length, commentsCount: 0, // Will be calculated from Comment collection createdAt: mongoPost.createdAt || new Date().toISOString(), updatedAt: mongoPost.updatedAt || new Date().toISOString() }; } transformEvent(mongoEvent) { return { _id: couchdbService.generateId("event", mongoEvent._id.toString()), type: "event", title: mongoEvent.title, description: mongoEvent.description, date: mongoEvent.date, location: mongoEvent.location, participants: (mongoEvent.participants || []).map(userId => ({ userId: couchdbService.generateId("user", userId.toString()), name: "", // Will be populated in relationship resolution profilePicture: "", joinedAt: new Date().toISOString() // Default join time })), participantsCount: (mongoEvent.participants || []).length, status: mongoEvent.status || "upcoming", createdAt: mongoEvent.createdAt || new Date().toISOString(), updatedAt: mongoEvent.updatedAt || new Date().toISOString() }; } transformReport(mongoReport) { return { _id: couchdbService.generateId("report", mongoReport._id.toString()), type: "report", street: { streetId: couchdbService.generateId("street", mongoReport.street.toString()), name: "", // Will be populated in relationship resolution location: null // Will be populated in relationship resolution }, user: { userId: couchdbService.generateId("user", mongoReport.user.toString()), name: "", // Will be populated in relationship resolution profilePicture: "" }, issue: mongoReport.issue, imageUrl: mongoReport.imageUrl, cloudinaryPublicId: mongoReport.cloudinaryPublicId, status: mongoReport.status || "open", createdAt: mongoReport.createdAt || new Date().toISOString(), updatedAt: mongoReport.updatedAt || new Date().toISOString() }; } transformBadge(mongoBadge) { return { _id: couchdbService.generateId("badge", mongoBadge._id.toString()), type: "badge", name: mongoBadge.name, description: mongoBadge.description, icon: mongoBadge.icon, criteria: mongoBadge.criteria, rarity: mongoBadge.rarity || "common", order: mongoBadge.order || 0, isActive: mongoBadge.isActive !== false, createdAt: mongoBadge.createdAt || new Date().toISOString(), updatedAt: mongoBadge.updatedAt || new Date().toISOString() }; } transformComment(mongoComment) { return { _id: couchdbService.generateId("comment", mongoComment._id.toString()), type: "comment", post: { postId: couchdbService.generateId("post", mongoComment.post.toString()), content: "", // Will be populated in relationship resolution userId: "" // Will be populated in relationship resolution }, user: { userId: couchdbService.generateId("user", mongoComment.user.toString()), name: "", // Will be populated in relationship resolution profilePicture: "" }, content: mongoComment.content, createdAt: mongoComment.createdAt || new Date().toISOString(), updatedAt: mongoComment.updatedAt || new Date().toISOString() }; } transformPointTransaction(mongoTransaction) { return { _id: couchdbService.generateId("transaction", mongoTransaction._id.toString()), type: "point_transaction", user: { userId: couchdbService.generateId("user", mongoTransaction.user.toString()), name: "" // Will be populated in relationship resolution }, amount: mongoTransaction.amount, type: mongoTransaction.type, description: mongoTransaction.description, relatedEntity: mongoTransaction.relatedEntity ? { entityType: mongoTransaction.relatedEntity.entityType, entityId: mongoTransaction.relatedEntity.entityId ? couchdbService.generateId( mongoTransaction.relatedEntity.entityType.toLowerCase(), mongoTransaction.relatedEntity.entityId.toString() ) : null, entityName: mongoTransaction.relatedEntity.entityName } : null, balanceAfter: mongoTransaction.balanceAfter, createdAt: mongoTransaction.createdAt || new Date().toISOString() }; } transformUserBadge(mongoUserBadge) { return { _id: couchdbService.generateId("userbadge", mongoUserBadge._id.toString()), type: "user_badge", userId: couchdbService.generateId("user", mongoUserBadge.user.toString()), badgeId: couchdbService.generateId("badge", mongoUserBadge.badge.toString()), progress: mongoUserBadge.progress || 100, earnedAt: mongoUserBadge.earnedAt || new Date().toISOString(), createdAt: mongoUserBadge.createdAt || new Date().toISOString(), updatedAt: mongoUserBadge.updatedAt || new Date().toISOString() }; } // Migration methods async migrateCollection(mongoModel, transformFn, collectionName) { console.log(`šŸ“¦ Migrating ${collectionName}...`); try { const documents = await mongoModel.find({}); console.log(`Found ${documents.length} ${collectionName} documents`); const transformedDocs = documents.map(doc => transformFn(doc)); // Batch insert if (transformedDocs.length > 0) { const result = await couchdbService.bulkDocs({ docs: transformedDocs }); // Count successful migrations const successful = result.filter(r => r.ok).length; this.stats[collectionName] = successful; console.log(`āœ… Successfully migrated ${successful}/${transformedDocs.length} ${collectionName}`); if (successful < transformedDocs.length) { console.log(`āš ļø ${transformedDocs.length - successful} ${collectionName} failed to migrate`); this.stats.errors += transformedDocs.length - successful; } } } catch (error) { console.error(`āŒ Error migrating ${collectionName}:`, error.message); this.stats.errors++; } } async resolveRelationships() { console.log("šŸ”— Resolving relationships and populating embedded data..."); try { // Get all users for lookup const users = await couchdbService.findByType("user"); const userMap = new Map(users.map(u => [u._id, u])); // Get all streets for lookup const streets = await couchdbService.findByType("street"); const streetMap = new Map(streets.map(s => [s._id, s])); // Get all posts for lookup const posts = await couchdbService.findByType("post"); const postMap = new Map(posts.map(p => [p._id, p])); // Update streets with adopter info for (const street of streets) { if (street.adoptedBy && street.adoptedBy.userId) { const user = userMap.get(street.adoptedBy.userId); if (user) { street.adoptedBy.name = user.name; street.adoptedBy.profilePicture = user.profilePicture || ""; } } } // Update tasks with street and user info const tasks = await couchdbService.findByType("task"); for (const task of tasks) { if (task.street && task.street.streetId) { const street = streetMap.get(task.street.streetId); if (street) { task.street.name = street.name; task.street.location = street.location; } } if (task.completedBy && task.completedBy.userId) { const user = userMap.get(task.completedBy.userId); if (user) { task.completedBy.name = user.name; task.completedBy.profilePicture = user.profilePicture || ""; } } } // Update posts with user info for (const post of posts) { if (post.user && post.user.userId) { const user = userMap.get(post.user.userId); if (user) { post.user.name = user.name; post.user.profilePicture = user.profilePicture || ""; } } } // Update comments with post and user info const comments = await couchdbService.findByType("comment"); for (const comment of comments) { if (comment.post && comment.post.postId) { const post = postMap.get(comment.post.postId); if (post) { comment.post.content = post.content; comment.post.userId = post.user.userId; } } if (comment.user && comment.user.userId) { const user = userMap.get(comment.user.userId); if (user) { comment.user.name = user.name; comment.user.profilePicture = user.profilePicture || ""; } } } // Update events with participant info const events = await couchdbService.findByType("event"); for (const event of events) { for (const participant of event.participants) { const user = userMap.get(participant.userId); if (user) { participant.name = user.name; participant.profilePicture = user.profilePicture || ""; } } } // Update reports with street and user info const reports = await couchdbService.findByType("report"); for (const report of reports) { if (report.street && report.street.streetId) { const street = streetMap.get(report.street.streetId); if (street) { report.street.name = street.name; report.street.location = street.location; } } if (report.user && report.user.userId) { const user = userMap.get(report.user.userId); if (user) { report.user.name = user.name; report.user.profilePicture = user.profilePicture || ""; } } } // Update point transactions with user info const transactions = await couchdbService.findByType("point_transaction"); for (const transaction of transactions) { if (transaction.user && transaction.user.userId) { const user = userMap.get(transaction.user.userId); if (user) { transaction.user.name = user.name; } } } // Update user badges and calculate stats const userBadges = await couchdbService.findByType("user_badge"); const badgeMap = new Map(); // Create badge lookup const badges = await couchdbService.findByType("badge"); for (const badge of badges) { badgeMap.set(badge._id, badge); } // Update users with badge info for (const user of users) { const userBadgeDocs = userBadges.filter(ub => ub.userId === user._id); user.earnedBadges = userBadgeDocs.map(ub => { const badge = badgeMap.get(ub.badgeId); return { badgeId: ub.badgeId, name: badge ? badge.name : "Unknown Badge", description: badge ? badge.description : "", icon: badge ? badge.icon : "šŸ†", rarity: badge ? badge.rarity : "common", earnedAt: ub.earnedAt, progress: ub.progress }; }); user.stats.badgesEarned = user.earnedBadges.length; } // Calculate stats for streets for (const street of streets) { const streetTasks = tasks.filter(t => t.street && t.street.streetId === street._id); const streetReports = reports.filter(r => r.street && r.street.streetId === street._id); street.stats.tasksCount = streetTasks.length; street.stats.completedTasksCount = streetTasks.filter(t => t.status === "completed").length; street.stats.reportsCount = streetReports.length; street.stats.openReportsCount = streetReports.filter(r => r.status === "open").length; } // Calculate comment counts for posts for (const post of posts) { const postComments = comments.filter(c => c.post && c.post.postId === post._id); post.commentsCount = postComments.length; } // Batch update all documents const allDocs = [ ...streets, ...tasks, ...posts, ...comments, ...events, ...reports, ...transactions, ...users ]; if (allDocs.length > 0) { const result = await couchdbService.bulkDocs({ docs: allDocs }); const successful = result.filter(r => r.ok).length; console.log(`āœ… Successfully updated ${successful}/${allDocs.length} documents with relationship data`); } } catch (error) { console.error("āŒ Error resolving relationships:", error.message); this.stats.errors++; } } async runMigration() { await this.initialize(); try { // Phase 1: Migrate all collections await this.migrateCollection(User, this.transformUser.bind(this), "users"); await this.migrateCollection(Street, this.transformStreet.bind(this), "streets"); await this.migrateCollection(Task, this.transformTask.bind(this), "tasks"); await this.migrateCollection(Post, this.transformPost.bind(this), "posts"); await this.migrateCollection(Event, this.transformEvent.bind(this), "events"); await this.migrateCollection(Report, this.transformReport.bind(this), "reports"); await this.migrateCollection(Badge, this.transformBadge.bind(this), "badges"); await this.migrateCollection(Comment, this.transformComment.bind(this), "comments"); await this.migrateCollection(PointTransaction, this.transformPointTransaction.bind(this), "pointTransactions"); await this.migrateCollection(UserBadge, this.transformUserBadge.bind(this), "userBadges"); // Phase 2: Resolve relationships await this.resolveRelationships(); // Print final statistics console.log("\nšŸ“Š Migration Summary:"); console.log("===================="); Object.entries(this.stats).forEach(([key, value]) => { if (key !== "errors") { console.log(`${key}: ${value}`); } }); if (this.stats.errors > 0) { console.log(`āŒ Errors: ${this.stats.errors}`); } console.log("===================="); console.log("āœ… Migration completed!"); } catch (error) { console.error("āŒ Migration failed:", error.message); } finally { await mongoose.disconnect(); await couchdbService.shutdown(); } } } // Run migration if this script is executed directly if (require.main === module) { const migration = new MigrationService(); migration.runMigration().catch(console.error); } module.exports = MigrationService;