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

@@ -14,7 +14,12 @@ const router = express.Router();
router.get(
"/",
asyncHandler(async (req, res) => {
const badges = await Badge.find().sort({ order: 1, rarity: 1 });
const badges = await Badge.find({ type: "badge" });
// Sort by order and rarity in JavaScript since CouchDB doesn't support complex sorting
badges.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order;
return a.rarity.localeCompare(b.rarity);
});
res.json(badges);
})
);
@@ -33,7 +38,7 @@ router.get(
);
/**
* GET /api/users/:userId/badges
* GET /api/badges/users/:userId
* Get badges earned by a specific user
*/
router.get(
@@ -41,9 +46,10 @@ router.get(
asyncHandler(async (req, res) => {
const { userId } = req.params;
const userBadges = await UserBadge.find({ user: userId })
.populate("badge")
.sort({ earnedAt: -1 });
const userBadges = await UserBadge.findByUser(userId);
// Sort by earnedAt in JavaScript
userBadges.sort((a, b) => new Date(b.earnedAt) - new Date(a.earnedAt));
res.json(
userBadges.map((ub) => ({

View File

@@ -14,8 +14,7 @@ router.post("/subscribe", auth, async (req, res) => {
return res.status(404).json({ msg: "User not found" });
}
user.isPremium = true;
await user.save();
await User.update(req.user.id, { isPremium: true });
res.json({ msg: "Subscription successful" });
} catch (err) {

View File

@@ -1,5 +1,7 @@
const express = require("express");
const Report = require("../models/Report");
const User = require("../models/User");
const Street = require("../models/Street");
const auth = require("../middleware/auth");
const { asyncHandler } = require("../middleware/errorHandler");
const {
@@ -15,23 +17,23 @@ const router = express.Router();
router.get(
"/",
asyncHandler(async (req, res) => {
const { paginate, buildPaginatedResponse } = require("../middleware/pagination");
// Parse pagination params
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const skip = (page - 1) * limit;
const reports = await Report.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate("street", ["name"])
.populate("user", ["name", "profilePicture"]);
const result = await Report.findWithPagination({
page,
limit,
sort: { createdAt: -1 },
});
const totalCount = await Report.countDocuments();
res.json(buildPaginatedResponse(reports, totalCount, page, limit));
res.json({
reports: result.docs,
totalCount: result.totalDocs,
currentPage: result.page,
totalPages: result.totalPages,
hasNext: result.hasNextPage,
hasPrev: result.hasPrevPage,
});
}),
);
@@ -43,11 +45,29 @@ router.post(
handleUploadError,
createReportValidation,
asyncHandler(async (req, res) => {
const { street, issue } = req.body;
const { street: streetId, issue } = req.body;
// Get street and user data for embedding
const street = await Street.findById(streetId);
if (!street) {
return res.status(404).json({ msg: "Street not found" });
}
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
const reportData = {
street,
user: req.user.id,
street: {
_id: street._id,
name: street.name,
},
user: {
_id: user._id,
name: user.name,
profilePicture: user.profilePicture,
},
issue,
};
@@ -61,15 +81,7 @@ router.post(
reportData.cloudinaryPublicId = result.publicId;
}
const newReport = new Report(reportData);
const report = await newReport.save();
// Populate user and street data
await report.populate([
{ path: "user", select: "name profilePicture" },
{ path: "street", select: "name" },
]);
const report = await Report.create(reportData);
res.json(report);
}),
);
@@ -88,7 +100,7 @@ router.post(
}
// Verify user owns the report
if (report.user.toString() !== req.user.id) {
if (report.user._id !== req.user.id) {
return res.status(403).json({ msg: "Not authorized" });
}
@@ -107,11 +119,12 @@ router.post(
"adopt-a-street/reports",
);
report.imageUrl = result.url;
report.cloudinaryPublicId = result.publicId;
await report.save();
const updatedReport = await Report.update(req.params.id, {
imageUrl: result.url,
cloudinaryPublicId: result.publicId,
});
res.json(report);
res.json(updatedReport);
}),
);
@@ -126,11 +139,11 @@ router.put(
return res.status(404).json({ msg: "Report not found" });
}
report.status = "resolved";
const updatedReport = await Report.update(req.params.id, {
status: "resolved",
});
await report.save();
res.json(report);
res.json(updatedReport);
}),
);

View File

@@ -1,5 +1,6 @@
const express = require("express");
const User = require("../models/User");
const Street = require("../models/Street");
const auth = require("../middleware/auth");
const { asyncHandler } = require("../middleware/errorHandler");
const { userIdValidation } = require("../middleware/validators/userValidator");
@@ -14,11 +15,34 @@ router.get(
auth,
userIdValidation,
asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id).populate("adoptedStreets");
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
res.json(user);
// Get adopted streets data
let adoptedStreets = [];
if (user.adoptedStreets && user.adoptedStreets.length > 0) {
adoptedStreets = await Promise.all(
user.adoptedStreets.map(async (streetId) => {
const street = await Street.findById(streetId);
return street ? {
_id: street._id,
name: street.name,
location: street.location,
status: street.status,
} : null;
})
);
adoptedStreets = adoptedStreets.filter(Boolean);
}
const userWithStreets = {
...user,
adoptedStreets,
};
res.json(userWithStreets);
}),
);
@@ -50,13 +74,14 @@ router.post(
);
// Update user with new profile picture
user.profilePicture = result.url;
user.cloudinaryPublicId = result.publicId;
await user.save();
const updatedUser = await User.update(req.user.id, {
profilePicture: result.url,
cloudinaryPublicId: result.publicId,
});
res.json({
msg: "Profile picture updated successfully",
profilePicture: user.profilePicture,
profilePicture: updatedUser.profilePicture,
});
}),
);
@@ -79,9 +104,10 @@ router.delete(
await deleteImage(user.cloudinaryPublicId);
// Remove from user
user.profilePicture = undefined;
user.cloudinaryPublicId = undefined;
await user.save();
await User.update(req.user.id, {
profilePicture: undefined,
cloudinaryPublicId: undefined,
});
res.json({ msg: "Profile picture deleted successfully" });
}),