- Add detailed CouchDB design document with denormalization strategy - Create migration script for MongoDB to CouchDB transition - Implement CouchDB service layer with all CRUD operations - Add query examples showing performance improvements - Design supports embedded data for better read performance - Include Mango indexing strategy for optimal query patterns - Provide data consistency and migration strategies This design prioritizes read performance and user experience for the social community nature of the Adopt-a-Street application. 🤖 Generated with AI Assistant Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
568 lines
14 KiB
Markdown
568 lines
14 KiB
Markdown
# CouchDB Document Structure Design
|
|
|
|
## Overview
|
|
|
|
This document outlines the comprehensive CouchDB document structure to replace the existing MongoDB models for the Adopt-a-Street application. The design prioritizes query performance, data consistency, and the denormalization requirements of a document-oriented database.
|
|
|
|
## Design Principles
|
|
|
|
1. **Denormalization over Normalization**: Since CouchDB doesn't support joins, we'll embed frequently accessed data
|
|
2. **Document Type Identification**: Each document includes a `type` field for easy filtering
|
|
3. **String IDs**: Convert MongoDB ObjectIds to strings for consistency
|
|
4. **Timestamp Handling**: Use CouchDB's built-in timestamps plus custom `createdAt`/`updatedAt` fields
|
|
5. **Query-First Design**: Structure documents based on common access patterns
|
|
|
|
## Document Types and Structures
|
|
|
|
### 1. User Documents (`type: "user"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "user_1234567890abcdef",
|
|
"type": "user",
|
|
"name": "John Doe",
|
|
"email": "john@example.com",
|
|
"password": "hashed_password_here",
|
|
"isPremium": false,
|
|
"points": 150,
|
|
"profilePicture": "https://cloudinary.com/image.jpg",
|
|
"cloudinaryPublicId": "abc123",
|
|
"adoptedStreets": ["street_abc123", "street_def456"],
|
|
"completedTasks": ["task_123", "task_456"],
|
|
"posts": ["post_789", "post_012"],
|
|
"events": ["event_345", "event_678"],
|
|
"earnedBadges": [
|
|
{
|
|
"badgeId": "badge_123",
|
|
"name": "Street Hero",
|
|
"description": "Adopted 5 streets",
|
|
"icon": "🏆",
|
|
"rarity": "rare",
|
|
"earnedAt": "2024-01-15T10:30:00Z",
|
|
"progress": 100
|
|
}
|
|
],
|
|
"stats": {
|
|
"streetsAdopted": 2,
|
|
"tasksCompleted": 5,
|
|
"postsCreated": 3,
|
|
"eventsParticipated": 2,
|
|
"badgesEarned": 1
|
|
},
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded badge data to avoid additional lookups in profile views
|
|
- Stats field for quick dashboard queries
|
|
- Keep arrays of IDs for detailed queries when needed
|
|
|
|
### 2. Street Documents (`type: "street"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "street_abc123def456",
|
|
"type": "street",
|
|
"name": "Main Street",
|
|
"location": {
|
|
"type": "Point",
|
|
"coordinates": [-74.0060, 40.7128]
|
|
},
|
|
"adoptedBy": {
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe",
|
|
"profilePicture": "https://cloudinary.com/image.jpg"
|
|
},
|
|
"status": "adopted",
|
|
"stats": {
|
|
"tasksCount": 5,
|
|
"completedTasksCount": 3,
|
|
"reportsCount": 2,
|
|
"openReportsCount": 1
|
|
},
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded adopter info for map display without additional queries
|
|
- Stats field for quick overview on street details
|
|
- Keep GeoJSON format for geospatial queries
|
|
|
|
### 3. Task Documents (`type: "task"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "task_1234567890abcdef",
|
|
"type": "task",
|
|
"street": {
|
|
"streetId": "street_abc123def456",
|
|
"name": "Main Street",
|
|
"location": {
|
|
"type": "Point",
|
|
"coordinates": [-74.0060, 40.7128]
|
|
}
|
|
},
|
|
"description": "Clean up litter on sidewalk",
|
|
"completedBy": {
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe",
|
|
"profilePicture": "https://cloudinary.com/image.jpg"
|
|
},
|
|
"status": "completed",
|
|
"completedAt": "2024-01-15T10:30:00Z",
|
|
"pointsAwarded": 10,
|
|
"createdAt": "2024-01-10T00:00:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded street info for task list display
|
|
- Embedded completer info for activity feeds
|
|
- Added `completedAt` and `pointsAwarded` for gamification tracking
|
|
|
|
### 4. Post Documents (`type: "post"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "post_1234567890abcdef",
|
|
"type": "post",
|
|
"user": {
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe",
|
|
"profilePicture": "https://cloudinary.com/image.jpg"
|
|
},
|
|
"content": "Great day cleaning up Main Street!",
|
|
"imageUrl": "https://cloudinary.com/post_image.jpg",
|
|
"cloudinaryPublicId": "post_abc123",
|
|
"likes": ["user_456", "user_789"],
|
|
"likesCount": 2,
|
|
"commentsCount": 5,
|
|
"createdAt": "2024-01-15T10:30:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded user info for social feed display
|
|
- Denormalized `likesCount` for quick sorting
|
|
- Keep `likes` as array of user IDs for like/unlike operations
|
|
|
|
### 5. Comment Documents (`type: "comment"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "comment_1234567890abcdef",
|
|
"type": "comment",
|
|
"post": {
|
|
"postId": "post_1234567890abcdef",
|
|
"content": "Great day cleaning up Main Street!",
|
|
"userId": "user_1234567890abcdef"
|
|
},
|
|
"user": {
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe",
|
|
"profilePicture": "https://cloudinary.com/image.jpg"
|
|
},
|
|
"content": "Awesome work! 🎉",
|
|
"createdAt": "2024-01-15T11:00:00Z",
|
|
"updatedAt": "2024-01-15T11:00:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded both post and user info for comment display
|
|
- Post reference for updating comment counts
|
|
|
|
### 6. Event Documents (`type: "event"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "event_1234567890abcdef",
|
|
"type": "event",
|
|
"title": "Community Cleanup Day",
|
|
"description": "Join us for a neighborhood cleanup event",
|
|
"date": "2024-02-01T09:00:00Z",
|
|
"location": "Central Park",
|
|
"participants": [
|
|
{
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe",
|
|
"profilePicture": "https://cloudinary.com/image.jpg",
|
|
"joinedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
],
|
|
"participantsCount": 1,
|
|
"status": "upcoming",
|
|
"createdAt": "2024-01-10T00:00:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded participant objects with join timestamps
|
|
- Denormalized `participantsCount` for quick display
|
|
- Rich participant data for event management
|
|
|
|
### 7. Report Documents (`type: "report"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "report_1234567890abcdef",
|
|
"type": "report",
|
|
"street": {
|
|
"streetId": "street_abc123def456",
|
|
"name": "Main Street",
|
|
"location": {
|
|
"type": "Point",
|
|
"coordinates": [-74.0060, 40.7128]
|
|
}
|
|
},
|
|
"user": {
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe",
|
|
"profilePicture": "https://cloudinary.com/image.jpg"
|
|
},
|
|
"issue": "Broken streetlight needs repair",
|
|
"imageUrl": "https://cloudinary.com/report_image.jpg",
|
|
"cloudinaryPublicId": "report_abc123",
|
|
"status": "open",
|
|
"createdAt": "2024-01-15T10:30:00Z",
|
|
"updatedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded street and user info for report management
|
|
- Keep status for filtering and workflow management
|
|
|
|
### 8. Badge Documents (`type: "badge"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "badge_1234567890abcdef",
|
|
"type": "badge",
|
|
"name": "Street Hero",
|
|
"description": "Adopted 5 streets",
|
|
"icon": "🏆",
|
|
"criteria": {
|
|
"type": "street_adoptions",
|
|
"threshold": 5
|
|
},
|
|
"rarity": "rare",
|
|
"order": 10,
|
|
"isActive": true,
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Static badge definitions with criteria for gamification engine
|
|
- Added `isActive` flag for badge management
|
|
- `order` field for display sorting
|
|
|
|
### 9. Point Transaction Documents (`type: "point_transaction"`)
|
|
|
|
```json
|
|
{
|
|
"_id": "transaction_1234567890abcdef",
|
|
"type": "point_transaction",
|
|
"user": {
|
|
"userId": "user_1234567890abcdef",
|
|
"name": "John Doe"
|
|
},
|
|
"amount": 10,
|
|
"type": "task_completion",
|
|
"description": "Completed task: Clean up litter on sidewalk",
|
|
"relatedEntity": {
|
|
"entityType": "Task",
|
|
"entityId": "task_1234567890abcdef",
|
|
"entityName": "Clean up litter on sidewalk"
|
|
},
|
|
"balanceAfter": 150,
|
|
"createdAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Design Decisions:**
|
|
- Embedded user info for transaction history
|
|
- Rich related entity data for audit trail
|
|
- No updates needed - transactions are immutable
|
|
|
|
## Mango Index Strategy
|
|
|
|
### Primary Indexes
|
|
|
|
```javascript
|
|
// User authentication
|
|
{
|
|
"index": {
|
|
"fields": ["type", "email"]
|
|
},
|
|
"name": "user-by-email",
|
|
"type": "json"
|
|
}
|
|
|
|
// Geospatial queries for streets
|
|
{
|
|
"index": {
|
|
"fields": ["type", "location"]
|
|
},
|
|
"name": "streets-by-location",
|
|
"type": "json"
|
|
}
|
|
|
|
// User's related data
|
|
{
|
|
"index": {
|
|
"fields": ["type", "user.userId"]
|
|
},
|
|
"name": "by-user",
|
|
"type": "json"
|
|
}
|
|
|
|
// Leaderboards
|
|
{
|
|
"index": {
|
|
"fields": ["type", "points"]
|
|
},
|
|
"name": "users-by-points",
|
|
"type": "json"
|
|
}
|
|
|
|
// Social feed
|
|
{
|
|
"index": {
|
|
"fields": ["type", "createdAt"]
|
|
},
|
|
"name": "posts-by-date",
|
|
"type": "json"
|
|
}
|
|
|
|
// Street status
|
|
{
|
|
"index": {
|
|
"fields": ["type", "status"]
|
|
},
|
|
"name": "streets-by-status",
|
|
"type": "json"
|
|
}
|
|
|
|
// Event queries
|
|
{
|
|
"index": {
|
|
"fields": ["type", "date", "status"]
|
|
},
|
|
"name": "events-by-date-status",
|
|
"type": "json"
|
|
}
|
|
```
|
|
|
|
## Common Query Patterns
|
|
|
|
### 1. User Authentication
|
|
```javascript
|
|
// Find user by email
|
|
{
|
|
"selector": {
|
|
"type": "user",
|
|
"email": "john@example.com"
|
|
},
|
|
"limit": 1
|
|
}
|
|
```
|
|
|
|
### 2. Geospatial Street Search
|
|
```javascript
|
|
// Find streets within bounding box
|
|
{
|
|
"selector": {
|
|
"type": "street",
|
|
"status": "available",
|
|
"location": {
|
|
"$geoWithin": {
|
|
"$box": [[-74.1, 40.6], [-73.9, 40.8]]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. User's Activity Feed
|
|
```javascript
|
|
// Get user's posts, tasks, and events
|
|
{
|
|
"selector": {
|
|
"$or": [
|
|
{
|
|
"type": "post",
|
|
"user.userId": "user_123"
|
|
},
|
|
{
|
|
"type": "task",
|
|
"completedBy.userId": "user_123"
|
|
},
|
|
{
|
|
"type": "event",
|
|
"participants": {
|
|
"$elemMatch": {
|
|
"userId": "user_123"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"sort": [{"createdAt": "desc"}]
|
|
}
|
|
```
|
|
|
|
### 4. Leaderboard
|
|
```javascript
|
|
// Top users by points
|
|
{
|
|
"selector": {
|
|
"type": "user",
|
|
"points": {"$gt": 0}
|
|
},
|
|
"sort": [{"points": "desc"}],
|
|
"limit": 10
|
|
}
|
|
```
|
|
|
|
### 5. Social Feed with Comments
|
|
```javascript
|
|
// Get posts with user info and comment counts
|
|
{
|
|
"selector": {
|
|
"type": "post"
|
|
},
|
|
"sort": [{"createdAt": "desc"}],
|
|
"limit": 20
|
|
}
|
|
|
|
// Then fetch comments for each post
|
|
{
|
|
"selector": {
|
|
"type": "comment",
|
|
"post.postId": {"$in": ["post_123", "post_456"]}
|
|
},
|
|
"sort": [{"createdAt": "asc"}]
|
|
}
|
|
```
|
|
|
|
## Data Consistency Strategy
|
|
|
|
### Update Patterns
|
|
|
|
1. **User Points Update**:
|
|
- Update user document points
|
|
- Create point transaction document
|
|
- Check and award badges if thresholds met
|
|
|
|
2. **Post Creation**:
|
|
- Create post document
|
|
- Update user document (add to posts array, increment stats)
|
|
- Update user's post creation count for badge criteria
|
|
|
|
3. **Task Completion**:
|
|
- Update task document (status, completedBy, completedAt)
|
|
- Update user document (add to completedTasks, increment stats, add points)
|
|
- Update street document (increment completedTasksCount)
|
|
- Create point transaction
|
|
- Check for badge awards
|
|
|
|
4. **Street Adoption**:
|
|
- Update street document (adoptedBy, status)
|
|
- Update user document (add to adoptedStreets, increment stats)
|
|
- Create point transaction
|
|
- Check for badge awards
|
|
|
|
### Cascade Deletion Handling
|
|
|
|
Since CouchDB doesn't have transactions, use a "soft delete" approach:
|
|
|
|
```javascript
|
|
// Mark document as deleted
|
|
{
|
|
"_id": "post_123",
|
|
"type": "post",
|
|
"isDeleted": true,
|
|
"deletedAt": "2024-01-15T10:30:00Z"
|
|
}
|
|
|
|
// Background cleanup job can periodically remove truly deleted documents
|
|
```
|
|
|
|
### Counter Management
|
|
|
|
For counters like `commentsCount`, `likesCount`, etc.:
|
|
|
|
1. **Optimistic Updates**: Update counter immediately, rollback if needed
|
|
2. **Periodic Reconciliation**: Background job to recalculate counts from actual data
|
|
3. **Event-Driven Updates**: Use CouchDB changes feed to trigger counter updates
|
|
|
|
## Migration Strategy
|
|
|
|
### Phase 1: Data Export and Transformation
|
|
|
|
```javascript
|
|
// MongoDB to CouchDB transformation script
|
|
const transformUser = (mongoUser) => ({
|
|
_id: `user_${mongoUser._id}`,
|
|
type: "user",
|
|
name: mongoUser.name,
|
|
email: mongoUser.email,
|
|
password: mongoUser.password,
|
|
isPremium: mongoUser.isPremium,
|
|
points: mongoUser.points,
|
|
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}`),
|
|
stats: {
|
|
streetsAdopted: mongoUser.adoptedStreets.length,
|
|
tasksCompleted: mongoUser.completedTasks.length,
|
|
postsCreated: mongoUser.posts.length,
|
|
eventsParticipated: mongoUser.events.length,
|
|
badgesEarned: 0 // Will be populated from UserBadge collection
|
|
},
|
|
createdAt: mongoUser.createdAt,
|
|
updatedAt: mongoUser.updatedAt
|
|
});
|
|
```
|
|
|
|
### Phase 2: Relationship Resolution
|
|
|
|
1. **First Pass**: Migrate all documents with ID references
|
|
2. **Second Pass**: Resolve relationships and embed data
|
|
3. **Third Pass**: Calculate and populate stats and counters
|
|
|
|
### Phase 3: Validation and Testing
|
|
|
|
1. **Data Integrity**: Verify all relationships are maintained
|
|
2. **Query Performance**: Test common query patterns
|
|
3. **Functionality**: Ensure all application features work
|
|
|
|
## Benefits of This Design
|
|
|
|
1. **Query Performance**: Most common queries require single document lookups
|
|
2. **Reduced Network Calls**: Embedded data eliminates multiple round trips
|
|
3. **Offline Capability**: Rich documents support better offline functionality
|
|
4. **Scalability**: Denormalized design scales well with read-heavy workloads
|
|
5. **Flexibility**: Document structure can evolve without schema migrations
|
|
|
|
## Trade-offs
|
|
|
|
1. **Data Duplication**: User data appears in multiple documents
|
|
2. **Update Complexity**: Changes to user data require updating multiple documents
|
|
3. **Storage Overhead**: Larger documents due to embedded data
|
|
4. **Consistency Challenges**: Eventual consistency for related data updates
|
|
|
|
This design prioritizes read performance and user experience over write efficiency, which aligns well with the social community nature of the Adopt-a-Street application. |