feat(backend): implement comments, image uploads, and data consistency
Implement additional backend features and improve data models: Comments System: - Create Comment model with user and post relationships - Add comments routes: GET /api/posts/:postId/comments (paginated), POST (create), DELETE (own comments) - Update Post model with commentsCount field - Emit Socket.IO events for newComment and commentDeleted - Pagination support for comment lists - Authorization checks (users can only delete own comments) - 500 character limit on comments Image Upload System: - Implement Cloudinary configuration (config/cloudinary.js) - Add uploadImage() and deleteImage() helper functions - Image optimization: max 1000x1000, auto quality, auto format (WebP) - Integrate image upload in users routes (profile pictures) - Integrate image upload in posts routes (post images with add/update endpoints) - File validation: 5MB limit, JPG/PNG/GIF/WebP only - Automatic image deletion when removing posts/reports Data Consistency Improvements: - Add cascade deletes in Street model (remove from user, delete associated tasks) - Add cascade deletes in Task model (remove from user completedTasks) - Add cascade deletes in Post model (remove from user posts) - Update user relationships on save (adoptedStreets, completedTasks, posts, events) - Add proper indexes for performance (2dsphere for location, compound indexes) - Add virtual relationships and toJSON configurations Model Updates: - Street: Add cascade hooks, location 2dsphere index - Task: Add cascade hooks, compound indexes for queries - Post: Add imageUrl, cloudinaryPublicId, commentsCount fields - Event: Add participants tracking - Report: Add image upload support - User: Add earnedBadges virtual, profilePicture, cloudinaryPublicId 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
32
backend/models/Comment.js
Normal file
32
backend/models/Comment.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const CommentSchema = new mongoose.Schema(
|
||||
{
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
post: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Post",
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Compound index for efficient querying of comments by post
|
||||
CommentSchema.index({ post: 1, createdAt: -1 });
|
||||
|
||||
module.exports = mongoose.model("Comment", CommentSchema);
|
||||
@@ -13,6 +13,7 @@ const EventSchema = new mongoose.Schema(
|
||||
date: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
@@ -24,10 +25,19 @@ const EventSchema = new mongoose.Schema(
|
||||
ref: "User",
|
||||
},
|
||||
],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ["upcoming", "ongoing", "completed", "cancelled"],
|
||||
default: "upcoming",
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Index for querying upcoming events
|
||||
EventSchema.index({ date: 1, status: 1 });
|
||||
|
||||
module.exports = mongoose.model("Event", EventSchema);
|
||||
|
||||
@@ -6,6 +6,7 @@ const PostSchema = new mongoose.Schema(
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
@@ -14,16 +15,48 @@ const PostSchema = new mongoose.Schema(
|
||||
imageUrl: {
|
||||
type: String,
|
||||
},
|
||||
cloudinaryPublicId: {
|
||||
type: String,
|
||||
},
|
||||
likes: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
},
|
||||
],
|
||||
commentsCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Index for querying posts by creation date
|
||||
PostSchema.index({ createdAt: -1 });
|
||||
|
||||
// Update user relationship when post is created
|
||||
PostSchema.post("save", async function (doc) {
|
||||
const User = mongoose.model("User");
|
||||
|
||||
// Add post to user's posts if not already there
|
||||
await User.updateOne(
|
||||
{ _id: doc.user },
|
||||
{ $addToSet: { posts: doc._id } }
|
||||
);
|
||||
});
|
||||
|
||||
// Cascade cleanup when a post is deleted
|
||||
PostSchema.pre("deleteOne", { document: true, query: false }, async function () {
|
||||
const User = mongoose.model("User");
|
||||
|
||||
// Remove post from user's posts
|
||||
await User.updateOne(
|
||||
{ _id: this.user },
|
||||
{ $pull: { posts: this._id } }
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Post", PostSchema);
|
||||
|
||||
@@ -16,6 +16,12 @@ const ReportSchema = new mongoose.Schema(
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageUrl: {
|
||||
type: String,
|
||||
},
|
||||
cloudinaryPublicId: {
|
||||
type: String,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ["open", "resolved"],
|
||||
@@ -27,4 +33,9 @@ const ReportSchema = new mongoose.Schema(
|
||||
},
|
||||
);
|
||||
|
||||
// Indexes for performance
|
||||
ReportSchema.index({ street: 1, status: 1 });
|
||||
ReportSchema.index({ user: 1 });
|
||||
ReportSchema.index({ createdAt: -1 });
|
||||
|
||||
module.exports = mongoose.model("Report", ReportSchema);
|
||||
|
||||
@@ -33,5 +33,37 @@ const StreetSchema = new mongoose.Schema(
|
||||
);
|
||||
|
||||
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 } }
|
||||
);
|
||||
}
|
||||
|
||||
// Delete all tasks associated with this street
|
||||
await Task.deleteMany({ street: this._id });
|
||||
});
|
||||
|
||||
// Update user relationship when street is adopted
|
||||
StreetSchema.post("save", async function (doc) {
|
||||
if (doc.adoptedBy && doc.status === "adopted") {
|
||||
const User = mongoose.model("User");
|
||||
|
||||
// Add street to user's adoptedStreets if not already there
|
||||
await User.updateOne(
|
||||
{ _id: doc.adoptedBy },
|
||||
{ $addToSet: { adoptedStreets: doc._id } }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Street", StreetSchema);
|
||||
|
||||
@@ -6,6 +6,7 @@ const TaskSchema = new mongoose.Schema(
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Street",
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
@@ -14,11 +15,13 @@ const TaskSchema = new mongoose.Schema(
|
||||
completedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
index: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ["pending", "completed"],
|
||||
default: "pending",
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,4 +29,34 @@ const TaskSchema = new mongoose.Schema(
|
||||
},
|
||||
);
|
||||
|
||||
// Compound indexes for common queries
|
||||
TaskSchema.index({ street: 1, status: 1 });
|
||||
TaskSchema.index({ completedBy: 1, status: 1 });
|
||||
|
||||
// Update user relationship when task is completed
|
||||
TaskSchema.post("save", async function (doc) {
|
||||
if (doc.completedBy && doc.status === "completed") {
|
||||
const User = mongoose.model("User");
|
||||
|
||||
// Add task to user's completedTasks if not already there
|
||||
await User.updateOne(
|
||||
{ _id: doc.completedBy },
|
||||
{ $addToSet: { completedTasks: doc._id } }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cascade cleanup when a task is deleted
|
||||
TaskSchema.pre("deleteOne", { document: true, query: false }, async function () {
|
||||
const User = mongoose.model("User");
|
||||
|
||||
// Remove task from user's completedTasks
|
||||
if (this.completedBy) {
|
||||
await User.updateOne(
|
||||
{ _id: this.completedBy },
|
||||
{ $pull: { completedTasks: this._id } }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Task", TaskSchema);
|
||||
|
||||
Reference in New Issue
Block a user