feat: Migrate Street and Task models from MongoDB to CouchDB

- Replace Street model with CouchDB-based implementation
- Replace Task model with CouchDB-based implementation
- Update routes to use new model interfaces
- Handle geospatial queries with CouchDB design documents
- Maintain adoption functionality and middleware
- Use denormalized document structure with embedded data
- Update test files to work with new models
- Ensure API compatibility while using CouchDB underneath

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
William Valentin
2025-11-01 13:12:34 -07:00
parent 2961107136
commit 7c7bc954ef
14 changed files with 1943 additions and 928 deletions

View File

@@ -1,69 +1,282 @@
const mongoose = require("mongoose");
const couchdbService = require("../services/couchdbService");
const StreetSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
location: {
type: {
type: String,
enum: ["Point"],
required: true,
},
coordinates: {
type: [Number],
required: true,
},
},
adoptedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
status: {
type: String,
enum: ["available", "adopted"],
default: "available",
},
},
{
timestamps: true,
},
);
StreetSchema.index({ location: "2dsphere" });
StreetSchema.index({ adoptedBy: 1 });
StreetSchema.index({ status: 1 });
// Cascade cleanup when a street is deleted
StreetSchema.pre("deleteOne", { document: true, query: false }, async function () {
const User = mongoose.model("User");
const Task = mongoose.model("Task");
// Remove street from user's adoptedStreets
if (this.adoptedBy) {
await User.updateOne(
{ _id: this.adoptedBy },
{ $pull: { adoptedStreets: this._id } }
);
class Street {
constructor(data) {
this._id = data._id || null;
this._rev = data._rev || null;
this.type = "street";
this.name = data.name;
this.location = data.location;
this.adoptedBy = data.adoptedBy || null;
this.status = data.status || "available";
this.createdAt = data.createdAt || new Date().toISOString();
this.updatedAt = data.updatedAt || new Date().toISOString();
this.stats = data.stats || {
completedTasksCount: 0,
reportsCount: 0,
openReportsCount: 0
};
}
// Delete all tasks associated with this street
await Task.deleteMany({ street: this._id });
});
// Static methods for MongoDB-like interface
static async find(filter = {}) {
try {
await couchdbService.initialize();
// Convert MongoDB filter to CouchDB selector
const selector = { type: "street", ...filter };
// Handle special cases
if (filter._id) {
selector._id = filter._id;
}
if (filter.status) {
selector.status = filter.status;
}
if (filter.adoptedBy) {
selector["adoptedBy.userId"] = filter.adoptedBy;
}
// Update user relationship when street is adopted
StreetSchema.post("save", async function (doc) {
if (doc.adoptedBy && doc.status === "adopted") {
const User = mongoose.model("User");
const query = {
selector,
sort: filter.sort || [{ name: "asc" }]
};
// Add street to user's adoptedStreets if not already there
await User.updateOne(
{ _id: doc.adoptedBy },
{ $addToSet: { adoptedStreets: doc._id } }
);
// Add pagination if specified
if (filter.skip) query.skip = filter.skip;
if (filter.limit) query.limit = filter.limit;
const docs = await couchdbService.find(query);
// Convert to Street instances
return docs.map(doc => new Street(doc));
} catch (error) {
console.error("Error finding streets:", error.message);
throw error;
}
}
});
module.exports = mongoose.model("Street", StreetSchema);
static async findById(id) {
try {
await couchdbService.initialize();
const doc = await couchdbService.getDocument(id);
if (!doc || doc.type !== "street") {
return null;
}
return new Street(doc);
} catch (error) {
if (error.statusCode === 404) {
return null;
}
console.error("Error finding street by ID:", error.message);
throw error;
}
}
static async findOne(filter = {}) {
try {
const streets = await Street.find(filter);
return streets.length > 0 ? streets[0] : null;
} catch (error) {
console.error("Error finding one street:", error.message);
throw error;
}
}
static async countDocuments(filter = {}) {
try {
await couchdbService.initialize();
const selector = { type: "street", ...filter };
// Use Mango query with count
const query = {
selector,
fields: ["_id"]
};
const docs = await couchdbService.find(query);
return docs.length;
} catch (error) {
console.error("Error counting streets:", error.message);
throw error;
}
}
static async create(data) {
try {
await couchdbService.initialize();
const street = new Street(data);
const doc = await couchdbService.createDocument(street.toJSON());
return new Street(doc);
} catch (error) {
console.error("Error creating street:", error.message);
throw error;
}
}
static async deleteMany(filter = {}) {
try {
await couchdbService.initialize();
const streets = await Street.find(filter);
const deletePromises = streets.map(street => street.delete());
await Promise.all(deletePromises);
return { deletedCount: streets.length };
} catch (error) {
console.error("Error deleting many streets:", error.message);
throw error;
}
}
// Instance methods
async save() {
try {
await couchdbService.initialize();
this.updatedAt = new Date().toISOString();
if (this._id && this._rev) {
// Update existing document
const doc = await couchdbService.updateDocument(this.toJSON());
this._rev = doc._rev;
} else {
// Create new document
const doc = await couchdbService.createDocument(this.toJSON());
this._id = doc._id;
this._rev = doc._rev;
}
return this;
} catch (error) {
console.error("Error saving street:", error.message);
throw error;
}
}
async delete() {
try {
await couchdbService.initialize();
if (!this._id || !this._rev) {
throw new Error("Street must have _id and _rev to delete");
}
// Handle cascade operations
await this._handleCascadeDelete();
await couchdbService.deleteDocument(this._id, this._rev);
return this;
} catch (error) {
console.error("Error deleting street:", error.message);
throw error;
}
}
async _handleCascadeDelete() {
try {
// Remove street from user's adoptedStreets
if (this.adoptedBy && this.adoptedBy.userId) {
const User = require("./User");
const user = await User.findById(this.adoptedBy.userId);
if (user) {
user.adoptedStreets = user.adoptedStreets.filter(id => id !== this._id);
await user.save();
}
}
// Delete all tasks associated with this street
const Task = require("./Task");
await Task.deleteMany({ "street.streetId": this._id });
} catch (error) {
console.error("Error handling cascade delete:", error.message);
throw error;
}
}
// Populate method for compatibility
async populate(path) {
if (path === "adoptedBy" && this.adoptedBy && this.adoptedBy.userId) {
const User = require("./User");
const user = await User.findById(this.adoptedBy.userId);
if (user) {
this.adoptedBy = {
userId: user._id,
name: user.name,
profilePicture: user.profilePicture
};
}
}
return this;
}
// Geospatial query helper
static async findNearby(coordinates, maxDistance = 1000) {
try {
await couchdbService.initialize();
// For CouchDB, we'll use a bounding box approach
// Calculate bounding box around the point
const [lng, lat] = coordinates;
const earthRadius = 6371000; // Earth's radius in meters
const latDelta = (maxDistance / earthRadius) * (180 / Math.PI);
const lngDelta = (maxDistance / earthRadius) * (180 / Math.PI) / Math.cos(lat * Math.PI / 180);
const bounds = [
[lng - lngDelta, lat - latDelta], // Southwest corner
[lng + lngDelta, lat + latDelta] // Northeast corner
];
const streets = await couchdbService.findStreetsByLocation(bounds);
return streets.map(doc => new Street(doc));
} catch (error) {
console.error("Error finding nearby streets:", error.message);
throw error;
}
}
// Convert to plain object
toJSON() {
return {
_id: this._id,
_rev: this._rev,
type: this.type,
name: this.name,
location: this.location,
adoptedBy: this.adoptedBy,
status: this.status,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
stats: this.stats
};
}
// Convert to MongoDB-like format for API responses
toObject() {
const obj = this.toJSON();
// Remove CouchDB-specific fields for API compatibility
delete obj._rev;
delete obj.type;
// Add _id field for compatibility
if (obj._id) {
obj.id = obj._id;
}
return obj;
}
}
// Export both the class and static methods for compatibility
module.exports = Street;