refactor: remove MongoDB legacy code and references

- Archive migration script to scripts/archive/migrate-to-couchdb.js
- Update error handler middleware for CouchDB-appropriate errors
- Fix MongoDB references in test utilities and comments
- Replace MongoDB ObjectId references with CouchDB ID patterns
- Preserve existing functionality while removing legacy dependencies

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
William Valentin
2025-11-03 09:29:10 -08:00
parent 05c0075245
commit 97f794fca5
5 changed files with 15 additions and 17 deletions

View File

@@ -0,0 +1,651 @@
// 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;