- Updated backend/scripts/seedBadges.js to use only CouchDB - Removed MongoDB dependencies and conditional logic - Created new scripts/seedBadges.js for root directory usage - Updated scripts/migrate-production.js to make MongoDB migration optional - Fixed module path resolution for all scripts to work from any directory - Cleaned up scripts/migrate-to-couchdb.js imports - All scripts now work with CouchDB service without MongoDB dependencies 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
651 lines
20 KiB
JavaScript
651 lines
20 KiB
JavaScript
// 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; |