Files
adopt-a-street/scripts/migrate-to-couchdb.js
William Valentin 4b710aae62 refactor: update all scripts to use CouchDB instead of MongoDB
- 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>
2025-11-02 23:29:22 -08:00

651 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;