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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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" });
|
||||
}),
|
||||
|
||||
@@ -72,7 +72,10 @@ mongoose
|
||||
// CouchDB (primary database)
|
||||
couchdbService.initialize()
|
||||
.then(() => console.log("CouchDB initialized"))
|
||||
.catch((err) => console.log("CouchDB initialization error:", err));
|
||||
.catch((err) => {
|
||||
console.log("CouchDB initialization error:", err);
|
||||
process.exit(1); // Exit if CouchDB fails to initialize since it's the primary database
|
||||
});
|
||||
|
||||
// Socket.IO Authentication Middleware
|
||||
io.use(socketAuth);
|
||||
@@ -123,13 +126,27 @@ app.use("/api/auth/login", authLimiter);
|
||||
app.use("/api", apiLimiter);
|
||||
|
||||
// Health check endpoint (for Kubernetes liveness/readiness probes)
|
||||
app.get("/api/health", (req, res) => {
|
||||
res.status(200).json({
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
mongodb: mongoose.connection.readyState === 1 ? "connected" : "disconnected",
|
||||
});
|
||||
app.get("/api/health", async (req, res) => {
|
||||
try {
|
||||
const couchdbStatus = await couchdbService.checkConnection();
|
||||
|
||||
res.status(200).json({
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
mongodb: mongoose.connection.readyState === 1 ? "connected" : "disconnected",
|
||||
couchdb: couchdbStatus ? "connected" : "disconnected",
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: "unhealthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
mongodb: mongoose.connection.readyState === 1 ? "connected" : "disconnected",
|
||||
couchdb: "disconnected",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Routes
|
||||
@@ -156,3 +173,42 @@ app.use(errorHandler);
|
||||
server.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("SIGTERM received, shutting down gracefully");
|
||||
|
||||
try {
|
||||
// Close MongoDB connection
|
||||
await mongoose.connection.close();
|
||||
console.log("MongoDB connection closed");
|
||||
|
||||
// Close server
|
||||
server.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during shutdown:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("SIGINT received, shutting down gracefully");
|
||||
|
||||
try {
|
||||
// Close MongoDB connection
|
||||
await mongoose.connection.close();
|
||||
console.log("MongoDB connection closed");
|
||||
|
||||
// Close server
|
||||
server.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during shutdown:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -412,6 +412,22 @@ class CouchDBService {
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check connection health
|
||||
*/
|
||||
async checkConnection() {
|
||||
try {
|
||||
if (!this.connection) {
|
||||
return false;
|
||||
}
|
||||
await this.connection.info();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("CouchDB connection check failed:", error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic CRUD operations
|
||||
async createDocument(doc) {
|
||||
if (!this.isConnected) await this.initialize();
|
||||
@@ -498,6 +514,62 @@ class CouchDBService {
|
||||
return await this.find(query);
|
||||
}
|
||||
|
||||
async findDocuments(selector = {}, options = {}) {
|
||||
const query = {
|
||||
selector,
|
||||
...options
|
||||
};
|
||||
return await this.find(query);
|
||||
}
|
||||
|
||||
async countDocuments(selector = {}) {
|
||||
const query = {
|
||||
selector,
|
||||
limit: 0, // We don't need documents, just count
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.db.find(query);
|
||||
return response.total_rows || 0;
|
||||
} catch (error) {
|
||||
console.error("Error counting documents:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findWithPagination(selector = {}, options = {}) {
|
||||
const { page = 1, limit = 10, sort = {} } = options;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const query = {
|
||||
selector,
|
||||
limit,
|
||||
skip,
|
||||
sort: Object.entries(sort).map(([field, order]) => ({
|
||||
[field]: order === -1 ? "desc" : "asc"
|
||||
}))
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.db.find(query);
|
||||
const totalCount = response.total_rows || 0;
|
||||
const totalPages = Math.ceil(totalCount / limit);
|
||||
|
||||
return {
|
||||
docs: response.docs,
|
||||
totalDocs: totalCount,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error finding documents with pagination:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// View query helper
|
||||
async view(designDoc, viewName, params = {}) {
|
||||
if (!this.isConnected) await this.initialize();
|
||||
|
||||
Reference in New Issue
Block a user