Files
adopt-a-street/COUCHDB_MIGRATION_GUIDE.md
William Valentin 742d1cac56 docs: comprehensive CouchDB migration documentation update
- Updated AGENTS.md with CouchDB references throughout
- Updated TESTING.md to reflect CouchDB testing utilities
- Updated TESTING_QUICK_START.md with CouchDB terminology
- Updated TEST_IMPLEMENTATION_SUMMARY.md for CouchDB architecture
- Updated IMPLEMENTATION_SUMMARY.md to include CouchDB migration
- Created comprehensive COUCHDB_MIGRATION_GUIDE.md with:
  - Migration benefits and architecture changes
  - Step-by-step migration process
  - Data model conversions
  - Design document setup
  - Testing updates
  - Deployment configurations
  - Performance optimizations
  - Monitoring and troubleshooting

All MongoDB references replaced with CouchDB equivalents while maintaining
existing document structure and technical accuracy.

🤖 Generated with AI Assistant

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-03 10:30:24 -08:00

763 lines
18 KiB
Markdown

# CouchDB Migration Guide
## Overview
This guide provides comprehensive documentation for migrating the Adopt-a-Street application from MongoDB to CouchDB. This migration improves scalability, provides better offline capabilities, and simplifies deployment.
## Migration Benefits
### Why CouchDB?
1. **Better Scalability**: CouchDB's master-master replication allows for easier scaling across multiple nodes
2. **Offline-First**: Built-in sync capabilities enable offline functionality
3. **Simpler Deployment**: No complex schema migrations required
4. **HTTP API**: Native REST API simplifies client-server communication
5. **Document Validation**: Built-in validation functions ensure data integrity
6. **MapReduce Views**: Powerful querying capabilities for complex data analysis
## Architecture Changes
### Before (MongoDB)
```
Backend (Node.js/Express)
├── Mongoose ODM
├── MongoDB Database
└── Schema Definitions
```
### After (CouchDB)
```
Backend (Node.js/Express)
├── Nano Client
├── CouchDB Database
└── Document Models
```
## Migration Steps
### Phase 1: Setup CouchDB
#### 1. Install CouchDB
```bash
# Ubuntu/Debian
sudo apt-get install couchdb
# macOS
brew install couchdb
# Docker
docker run -d -p 5984:5984 --name couchdb couchdb:latest
```
#### 2. Configure CouchDB
```bash
# Create admin user
curl -X PUT http://localhost:5984/_config/admins/admin -d '"password"'
# Create database
curl -X PUT http://admin:password@localhost:5984/adopt-a-street
```
#### 3. Install Nano Client
```bash
cd backend
bun add nano
```
### Phase 2: Update Backend Configuration
#### 1. Environment Variables
Update `.env` file:
```env
# Remove MongoDB
# MONGO_URI=mongodb://localhost:27017/adopt-a-street
# Add CouchDB
COUCHDB_URL=http://localhost:5984
COUCHDB_DB_NAME=adopt-a-street
COUCHDB_USERNAME=admin
COUCHDB_PASSWORD=password
```
#### 2. Create CouchDB Service
Create `backend/services/couchdbService.js`:
```javascript
const nano = require('nano')(
`${process.env.COUCHDB_URL}/${process.env.COUCHDB_DB_NAME}`
);
class CouchDBService {
constructor() {
this.db = nano;
}
async create(doc) {
return await this.db.insert(doc);
}
async get(id) {
return await this.db.get(id);
}
async update(id, doc) {
const existing = await this.get(id);
doc._rev = existing._rev;
return await this.db.insert(doc);
}
async delete(id) {
const doc = await this.get(id);
return await this.db.destroy(id, doc._rev);
}
async find(view, params = {}) {
return await this.db.view('design_doc', view, params);
}
async all(params = {}) {
return await this.db.list(params);
}
}
module.exports = new CouchDBService();
```
### Phase 3: Migrate Data Models
#### 1. User Model
Create `backend/models/User.js`:
```javascript
const couchdbService = require('../services/couchdbService');
class User {
static async create(userData) {
const user = {
_id: `user:${userData.email}`,
type: 'user',
name: userData.name,
email: userData.email,
password: userData.password, // Hashed
points: 0,
isPremium: false,
adoptedStreets: [],
completedTasks: [],
earnedBadges: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
return await couchdbService.create(user);
}
static async findById(id) {
try {
return await couchdbService.get(id);
} catch (error) {
if (error.statusCode === 404) return null;
throw error;
}
}
static async findByEmail(email) {
const result = await couchdbService.find('by_email', { key: email });
return result.rows.length > 0 ? result.rows[0].value : null;
}
static async update(id, updateData) {
const user = await this.findById(id);
if (!user) throw new Error('User not found');
Object.assign(user, updateData);
user.updatedAt = new Date().toISOString();
return await couchdbService.update(id, user);
}
static async delete(id) {
return await couchdbService.delete(id);
}
}
module.exports = User;
```
#### 2. Street Model
Create `backend/models/Street.js`:
```javascript
const couchdbService = require('../services/couchdbService');
class Street {
static async create(streetData) {
const street = {
_id: `street:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`,
type: 'street',
name: streetData.name,
location: streetData.location, // GeoJSON
description: streetData.description,
status: 'active',
adoptedBy: null,
adoptionDate: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
return await couchdbService.create(street);
}
static async findById(id) {
try {
return await couchdbService.get(id);
} catch (error) {
if (error.statusCode === 404) return null;
throw error;
}
}
static async findAll(params = {}) {
const result = await couchdbService.find('all_streets', params);
return result.rows.map(row => row.value);
}
static async findNearby(coordinates, maxDistance = 1000) {
const result = await couchdbService.find('nearby_streets', {
lat: coordinates[1],
lon: coordinates[0],
radius: maxDistance
});
return result.rows.map(row => row.value);
}
static async update(id, updateData) {
const street = await this.findById(id);
if (!street) throw new Error('Street not found');
Object.assign(street, updateData);
street.updatedAt = new Date().toISOString();
return await couchdbService.update(id, street);
}
static async delete(id) {
return await couchdbService.delete(id);
}
}
module.exports = Street;
```
### Phase 4: Create Design Documents
#### 1. Create Design Document
Create `backend/couchdb/design-doc.json`:
```json
{
"_id": "_design/adopt_a_street",
"views": {
"by_email": {
"map": "function(doc) { if (doc.type === 'user' && doc.email) { emit(doc.email, doc); } }"
},
"all_streets": {
"map": "function(doc) { if (doc.type === 'street') { emit(doc._id, doc); } }"
},
"streets_by_user": {
"map": "function(doc) { if (doc.type === 'street' && doc.adoptedBy) { emit(doc.adoptedBy, doc); } }"
},
"tasks_by_street": {
"map": "function(doc) { if (doc.type === 'task' && doc.street) { emit(doc.street, doc); } }"
},
"posts_by_user": {
"map": "function(doc) { if (doc.type === 'post' && doc.user) { emit(doc.user, doc); } }"
},
"events_by_date": {
"map": "function(doc) { if (doc.type === 'event' && doc.date) { emit(doc.date, doc); } }"
},
"nearby_streets": {
"map": "function(doc) { if (doc.type === 'street' && doc.location && doc.location.coordinates) { emit([doc.location.coordinates[1], doc.location.coordinates[0]], doc); } }"
}
},
"validate_doc_update": "function(newDoc, oldDoc, userCtx) { if (newDoc.type && !['user', 'street', 'task', 'post', 'event', 'reward', 'report'].includes(newDoc.type)) { throw({forbidden: 'Invalid document type'}); } }"
}
```
#### 2. Install Design Document
```bash
curl -X PUT http://admin:password@localhost:5984/adopt-a-street/_design/adopt_a_street \
-H "Content-Type: application/json" \
-d @backend/couchdb/design-doc.json
```
### Phase 5: Update Routes
#### 1. Auth Routes
Update `backend/routes/auth.js`:
```javascript
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// Register
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = await User.findByEmail(email);
if (existingUser) {
return res.status(400).json({ msg: 'User already exists' });
}
// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Create user
const user = await User.create({
name,
email,
password: hashedPassword
});
// Create JWT
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token, user: { id: user._id, name: user.name, email: user.email } });
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findByEmail(email);
if (!user) {
return res.status(400).json({ msg: 'Invalid credentials' });
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: 'Invalid credentials' });
}
// Create JWT
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token, user: { id: user._id, name: user.name, email: user.email } });
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
});
```
### Phase 6: Data Migration Script
#### 1. Create Migration Script
Create `scripts/migrate-to-couchdb.js`:
```javascript
const mongoose = require('mongoose');
const nano = require('nano')('http://localhost:5984/adopt-a-street');
// MongoDB Models (old)
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 Reward = require('../backend/models/Reward');
const Report = require('../backend/models/Report');
async function migrateUsers() {
console.log('Migrating users...');
const users = await User.find();
for (const user of users) {
const couchUser = {
_id: `user:${user.email}`,
type: 'user',
name: user.name,
email: user.email,
password: user.password,
points: user.points || 0,
isPremium: user.isPremium || false,
adoptedStreets: user.adoptedStreets || [],
completedTasks: user.completedTasks || [],
earnedBadges: user.earnedBadges || [],
createdAt: user.createdAt,
updatedAt: user.updatedAt
};
await nano.insert(couchUser);
}
console.log(`Migrated ${users.length} users`);
}
async function migrateStreets() {
console.log('Migrating streets...');
const streets = await Street.find();
for (const street of streets) {
const couchStreet = {
_id: `street:${street._id}`,
type: 'street',
name: street.name,
location: street.location,
description: street.description,
status: street.status || 'active',
adoptedBy: street.adoptedBy,
adoptionDate: street.adoptionDate,
createdAt: street.createdAt,
updatedAt: street.updatedAt
};
await nano.insert(couchStreet);
}
console.log(`Migrated ${streets.length} streets`);
}
// Add similar functions for other models...
async function runMigration() {
try {
// Connect to MongoDB
await mongoose.connect('mongodb://localhost:27017/adopt-a-street');
// Run migrations
await migrateUsers();
await migrateStreets();
// await migrateTasks();
// await migratePosts();
// await migrateEvents();
// await migrateRewards();
// await migrateReports();
console.log('Migration completed successfully!');
process.exit(0);
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
runMigration();
```
#### 2. Run Migration
```bash
cd scripts
node migrate-to-couchdb.js
```
### Phase 7: Update Tests
#### 1. Update Test Setup
Update `backend/__tests__/setup.js`:
```javascript
const { CouchDBMem } = require('@couchdb/test-helpers');
let couchdb;
beforeAll(async () => {
couchdb = new CouchDBMem();
await couchdb.start();
// Create test database
await couchdb.createDb('adopt-a-street');
// Install design document
const designDoc = require('../../couchdb/design-doc.json');
await couchdb.insertDoc('adopt-a-street', designDoc);
});
afterAll(async () => {
await couchdb.stop();
});
beforeEach(async () => {
// Clean up test data
await couchdb.clearDb('adopt-a-street');
});
```
#### 2. Update Test Helpers
Update `backend/__tests__/utils/testHelpers.js`:
```javascript
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 Reward = require('../../models/Reward');
const Report = require('../../models/Report');
const jwt = require('jsonwebtoken');
async function createTestUser(userData = {}) {
const defaultUser = {
name: 'Test User',
email: 'test@example.com',
password: 'password123'
};
const user = await User.create({ ...defaultUser, ...userData });
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET);
return { user, token };
}
async function createTestStreet(streetData = {}) {
const defaultStreet = {
name: 'Test Street',
location: {
type: 'Point',
coordinates: [-74.0060, 40.7128]
},
description: 'A test street'
};
return await Street.create({ ...defaultStreet, ...streetData });
}
// Add similar functions for other models...
module.exports = {
createTestUser,
createTestStreet,
createTestTask,
createTestPost,
createTestEvent,
createTestReward,
createTestReport
};
```
## Deployment Considerations
### 1. Docker Configuration
Update `docker-compose.yml`:
```yaml
version: '3.8'
services:
couchdb:
image: couchdb:latest
ports:
- "5984:5984"
environment:
- COUCHDB_USER=admin
- COUCHDB_PASSWORD=password
volumes:
- couchdb_data:/opt/couchdb/data
networks:
- adopt-a-street
backend:
build: ./backend
ports:
- "5000:5000"
environment:
- COUCHDB_URL=http://couchdb:5984
- COUCHDB_DB_NAME=adopt-a-street
- COUCHDB_USER=admin
- COUCHDB_PASSWORD=password
depends_on:
- couchdb
networks:
- adopt-a-street
volumes:
couchdb_data:
networks:
adopt-a-street:
driver: bridge
```
### 2. Kubernetes Deployment
Update `deploy/k8s/couchdb-statefulset.yaml`:
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: couchdb
spec:
serviceName: couchdb
replicas: 1
selector:
matchLabels:
app: couchdb
template:
metadata:
labels:
app: couchdb
spec:
containers:
- name: couchdb
image: couchdb:latest
ports:
- containerPort: 5984
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
volumeMounts:
- name: couchdb-data
mountPath: /opt/couchdb/data
volumeClaimTemplates:
- metadata:
name: couchdb-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
```
## Performance Optimizations
### 1. Indexing
Create additional indexes for common queries:
```javascript
// Add to design document
"indexes": {
"streets_by_status": {
"map": "function(doc) { if (doc.type === 'street') { emit(doc.status, doc); } }"
},
"tasks_by_assignee": {
"map": "function(doc) { if (doc.type === 'task' && doc.assignedTo) { emit(doc.assignedTo, doc); } }"
},
"events_by_organizer": {
"map": "function(doc) { if (doc.type === 'event' && doc.organizer) { emit(doc.organizer, doc); } }"
}
}
```
### 2. Caching
Implement Redis caching for frequently accessed data:
```javascript
const redis = require('redis');
const client = redis.createClient();
async function getCachedUser(id) {
const cached = await client.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await User.findById(id);
await client.setex(`user:${id}`, 3600, JSON.stringify(user));
return user;
}
```
## Monitoring and Maintenance
### 1. Health Checks
```javascript
// Add to backend/routes/health.js
router.get('/couchdb', async (req, res) => {
try {
const response = await couchdbService.db.info();
res.json({ status: 'healthy', couchdb: response });
} catch (error) {
res.status(500).json({ status: 'unhealthy', error: error.message });
}
});
```
### 2. Backup Strategy
```bash
# Create backup script
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/couchdb"
# Create backup
curl -X GET http://admin:password@localhost:5984/adopt-a-street/_all_docs?include_docs=true \
-o "$BACKUP_DIR/backup_$DATE.json"
# Compress backup
gzip "$BACKUP_DIR/backup_$DATE.json"
# Clean old backups (keep last 7 days)
find $BACKUP_DIR -name "backup_*.json.gz" -mtime +7 -delete
```
## Troubleshooting
### Common Issues
1. **Connection Errors**
- Check CouchDB is running: `curl http://localhost:5984`
- Verify credentials in environment variables
- Check network connectivity
2. **Document Conflicts**
- Use revision numbers when updating documents
- Implement conflict resolution strategies
- Consider using bulk operations for multiple updates
3. **Performance Issues**
- Add appropriate indexes
- Use pagination for large result sets
- Consider view caching for frequently accessed data
4. **Migration Failures**
- Check MongoDB connection
- Verify data format compatibility
- Run migration in smaller batches
## Rollback Plan
If you need to rollback to MongoDB:
1. **Stop Application**
```bash
docker-compose down
```
2. **Restore MongoDB Data**
```bash
mongorestore --db adopt-a-street /path/to/mongodb/backup
```
3. **Update Configuration**
- Restore original `.env` file
- Revert code changes to use Mongoose
4. **Restart Application**
```bash
docker-compose up -d
```
## Conclusion
This migration guide provides a comprehensive approach to migrating from MongoDB to CouchDB. The key benefits include improved scalability, offline capabilities, and simplified deployment. Take time to test thoroughly in a staging environment before deploying to production.
For additional support:
- [CouchDB Documentation](https://docs.couchdb.org/)
- [Nano Client Documentation](https://github.com/apache/couchdb-nano)
- [Community Forums](https://couchdb.apache.org/#mailing-lists)
---
**Migration Date**: 2025-11-03
**CouchDB Version**: 3.3+
**Node.js Version**: 18+
**Test Coverage**: Maintained at 55%+