feat: complete MongoDB to CouchDB migration

- Migrate Report model to CouchDB with embedded street/user data
- Migrate UserBadge model to CouchDB with badge population
- Update all remaining routes (reports, users, badges, payments) to use CouchDB
- Add CouchDB health check and graceful shutdown to server.js
- Add missing methods to couchdbService (checkConnection, findWithPagination, etc.)
- Update Kubernetes deployment manifests for CouchDB support
- Add comprehensive CouchDB setup documentation

All core functionality now uses CouchDB as primary database while maintaining
MongoDB for backward compatibility during transition period.

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
William Valentin
2025-11-01 13:29:48 -07:00
parent 9ac21fca72
commit df94c17e1f
14 changed files with 684 additions and 155 deletions

View File

@@ -1,41 +1,109 @@
const mongoose = require("mongoose");
const couchdbService = require("../services/couchdbService");
const ReportSchema = new mongoose.Schema(
{
street: {
type: mongoose.Schema.Types.ObjectId,
ref: "Street",
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
issue: {
type: String,
required: true,
},
imageUrl: {
type: String,
},
cloudinaryPublicId: {
type: String,
},
status: {
type: String,
enum: ["open", "resolved"],
default: "open",
},
},
{
timestamps: true,
},
);
class Report {
static async create(reportData) {
const doc = {
type: "report",
...reportData,
status: reportData.status || "open",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// Indexes for performance
ReportSchema.index({ street: 1, status: 1 });
ReportSchema.index({ user: 1 });
ReportSchema.index({ createdAt: -1 });
return await couchdbService.createDocument(doc);
}
module.exports = mongoose.model("Report", ReportSchema);
static async findById(id) {
const doc = await couchdbService.getDocument(id);
if (doc && doc.type === "report") {
return doc;
}
return null;
}
static async find(filter = {}) {
const selector = {
type: "report",
...filter,
};
return await couchdbService.findDocuments(selector);
}
static async findWithPagination(options = {}) {
const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
const selector = { type: "report" };
return await couchdbService.findWithPagination(selector, {
page,
limit,
sort,
});
}
static async update(id, updateData) {
const doc = await couchdbService.getDocument(id);
if (!doc || doc.type !== "report") {
throw new Error("Report not found");
}
const updatedDoc = {
...doc,
...updateData,
updatedAt: new Date().toISOString(),
};
return await couchdbService.updateDocument(id, updatedDoc);
}
static async delete(id) {
const doc = await couchdbService.getDocument(id);
if (!doc || doc.type !== "report") {
throw new Error("Report not found");
}
return await couchdbService.deleteDocument(id, doc._rev);
}
static async countDocuments(filter = {}) {
const selector = {
type: "report",
...filter,
};
return await couchdbService.countDocuments(selector);
}
static async findByStreet(streetId) {
const selector = {
type: "report",
"street._id": streetId,
};
return await couchdbService.findDocuments(selector);
}
static async findByUser(userId) {
const selector = {
type: "report",
"user._id": userId,
};
return await couchdbService.findDocuments(selector);
}
static async findByStatus(status) {
const selector = {
type: "report",
status,
};
return await couchdbService.findDocuments(selector);
}
static async update(id, updateData) {
return await couchdbService.update(id, updateData);
}
}
module.exports = Report;

View File

@@ -1,37 +1,118 @@
const mongoose = require("mongoose");
const couchdbService = require("../services/couchdbService");
const UserBadgeSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
index: true,
},
badge: {
type: mongoose.Schema.Types.ObjectId,
ref: "Badge",
required: true,
index: true,
},
earnedAt: {
type: Date,
default: Date.now,
},
progress: {
type: Number,
default: 0,
},
},
{
timestamps: true,
},
);
class UserBadge {
static async create(userBadgeData) {
const doc = {
type: "user_badge",
...userBadgeData,
earnedAt: userBadgeData.earnedAt || new Date().toISOString(),
progress: userBadgeData.progress || 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// Compound unique index to prevent duplicate badge awards
UserBadgeSchema.index({ user: 1, badge: 1 }, { unique: true });
return await couchdbService.createDocument(doc);
}
// Index for user badge queries
UserBadgeSchema.index({ user: 1, earnedAt: -1 });
static async findById(id) {
const doc = await couchdbService.getDocument(id);
if (doc && doc.type === "user_badge") {
return doc;
}
return null;
}
module.exports = mongoose.model("UserBadge", UserBadgeSchema);
static async find(filter = {}) {
const selector = {
type: "user_badge",
...filter,
};
return await couchdbService.findDocuments(selector);
}
static async findByUser(userId) {
const selector = {
type: "user_badge",
userId: userId,
};
const userBadges = await couchdbService.findDocuments(selector);
// Populate badge data for each user badge
const populatedBadges = await Promise.all(
userBadges.map(async (userBadge) => {
if (userBadge.badgeId) {
const badge = await couchdbService.getDocument(userBadge.badgeId);
return {
...userBadge,
badge: badge,
};
}
return userBadge;
})
);
return populatedBadges;
}
static async findByBadge(badgeId) {
const selector = {
type: "user_badge",
badgeId: badgeId,
};
return await couchdbService.findDocuments(selector);
}
static async update(id, updateData) {
const doc = await couchdbService.getDocument(id);
if (!doc || doc.type !== "user_badge") {
throw new Error("UserBadge not found");
}
const updatedDoc = {
...doc,
...updateData,
updatedAt: new Date().toISOString(),
};
return await couchdbService.updateDocument(id, updatedDoc);
}
static async delete(id) {
const doc = await couchdbService.getDocument(id);
if (!doc || doc.type !== "user_badge") {
throw new Error("UserBadge not found");
}
return await couchdbService.deleteDocument(id, doc._rev);
}
static async findByUserAndBadge(userId, badgeId) {
const selector = {
type: "user_badge",
userId: userId,
badgeId: badgeId,
};
const results = await couchdbService.findDocuments(selector);
return results[0] || null;
}
static async updateProgress(userId, badgeId, progress) {
const userBadge = await this.findByUserAndBadge(userId, badgeId);
if (userBadge) {
return await this.update(userBadge._id, { progress });
} else {
return await this.create({
userId,
badgeId,
progress,
});
}
}
}
module.exports = UserBadge;