@feardread/fear 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/controllers/review.js +161 -164
- package/libs/emailer/smtp.js +2 -1
- package/models/review.js +5 -4
- package/package.json +1 -1
- package/routes/review.js +9 -5
package/controllers/review.js
CHANGED
|
@@ -2,45 +2,32 @@ const { tryCatch } = require("../libs/handler/error");
|
|
|
2
2
|
const methods = require("./crud");
|
|
3
3
|
const Review = require("../models/review");
|
|
4
4
|
const Product = require("../models/product");
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* Add or update a product review
|
|
7
8
|
* @param {Object} req - Express request object
|
|
8
9
|
* @param {Object} res - Express response object
|
|
9
10
|
*/
|
|
10
|
-
exports.
|
|
11
|
+
exports.add = async (req, res) => {
|
|
11
12
|
const { username, email, rating, comment } = req.body;
|
|
12
|
-
const productId = req.params.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const productId = req.params.id || req.body.productId;
|
|
14
|
+
|
|
15
15
|
// Validate required fields
|
|
16
16
|
if (!rating || rating < 1 || rating > 5) {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
return Promise.resolve(
|
|
18
|
+
res.status(400).json({
|
|
19
|
+
success: false,
|
|
20
|
+
message: "Rating must be between 1 and 5"
|
|
21
|
+
}));
|
|
21
22
|
}
|
|
22
|
-
|
|
23
23
|
if (!comment || comment.trim().length === 0) {
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
return Promise.resolve(
|
|
25
|
+
res.status(400).json({
|
|
26
|
+
success: false,
|
|
27
|
+
message: "Comment is required"
|
|
28
|
+
}));
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
// Find product and check if it exists
|
|
31
|
-
const product = await Product.findById(productId);
|
|
32
|
-
if (!product) {
|
|
33
|
-
return res.status(404).json({
|
|
34
|
-
success: false,
|
|
35
|
-
message: "Product not found"
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Check if user already reviewed this product
|
|
40
|
-
const existingReviewIndex = product.reviews.findIndex(
|
|
41
|
-
(review) => review.user.toString() === userId.toString()
|
|
42
|
-
);
|
|
43
|
-
|
|
30
|
+
|
|
44
31
|
const reviewData = {
|
|
45
32
|
productId: productId,
|
|
46
33
|
username,
|
|
@@ -50,57 +37,50 @@ exports.review = tryCatch(async (req, res) => {
|
|
|
50
37
|
createdAt: new Date()
|
|
51
38
|
};
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
product
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return res.status(200).json({
|
|
91
|
-
success: true,
|
|
92
|
-
message: existingReviewIndex !== -1 ? "Review updated successfully" : "Review added successfully",
|
|
93
|
-
result: updatedProduct,
|
|
94
|
-
review: reviewData
|
|
95
|
-
});
|
|
96
|
-
});
|
|
40
|
+
// Find product and check if it exists
|
|
41
|
+
return Product.findById(productId)
|
|
42
|
+
.then(product => {
|
|
43
|
+
if (!product) {
|
|
44
|
+
res.status(400).json({
|
|
45
|
+
success: false,
|
|
46
|
+
message: "Product not found"
|
|
47
|
+
});
|
|
48
|
+
return Promise.reject(new Error("Product not found"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
product.reviews.push(reviewData);
|
|
52
|
+
|
|
53
|
+
// Recalculate average rating
|
|
54
|
+
const totalRating = product.reviews.reduce((sum, review) => sum + review.rating, 0);
|
|
55
|
+
product.rating = Number((totalRating / product.reviews.length).toFixed(1));
|
|
56
|
+
product.totalReviews = product.reviews.length;
|
|
57
|
+
|
|
58
|
+
return product.save();
|
|
59
|
+
})
|
|
60
|
+
.then(() => {
|
|
61
|
+
|
|
62
|
+
return Review.create(reviewData);
|
|
63
|
+
})
|
|
64
|
+
.then(review => {
|
|
65
|
+
return res.status(200).json({
|
|
66
|
+
success: true,
|
|
67
|
+
message: "Review added successfully",
|
|
68
|
+
result: review,
|
|
69
|
+
});
|
|
70
|
+
//return Promise.resolve(review);
|
|
71
|
+
})
|
|
72
|
+
.catch(error => {
|
|
73
|
+
console.error('Error in review process:', error);
|
|
74
|
+
return Promise.reject(error);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
97
77
|
|
|
98
78
|
/**
|
|
99
79
|
* Add or update product rating (alternative rating system)
|
|
100
80
|
* @param {Object} req - Express request object
|
|
101
81
|
* @param {Object} res - Express response object
|
|
102
82
|
*/
|
|
103
|
-
exports.rating = tryCatch(
|
|
83
|
+
exports.rating = tryCatch((req, res) => {
|
|
104
84
|
const { _id: userId } = req.user;
|
|
105
85
|
const { star, prodId, comment = "" } = req.body;
|
|
106
86
|
|
|
@@ -120,64 +100,79 @@ exports.rating = tryCatch(async (req, res) => {
|
|
|
120
100
|
}
|
|
121
101
|
|
|
122
102
|
// Find product
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const ratingData = {
|
|
142
|
-
star: Number(star),
|
|
143
|
-
comment: comment.trim(),
|
|
144
|
-
postedby: userId,
|
|
145
|
-
createdAt: new Date()
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if (existingRatingIndex !== -1) {
|
|
149
|
-
// Update existing rating
|
|
150
|
-
product.ratings[existingRatingIndex] = {
|
|
151
|
-
...product.ratings[existingRatingIndex],
|
|
152
|
-
...ratingData,
|
|
153
|
-
updatedAt: new Date()
|
|
154
|
-
};
|
|
155
|
-
} else {
|
|
156
|
-
// Add new rating
|
|
157
|
-
product.ratings.push(ratingData);
|
|
158
|
-
}
|
|
103
|
+
Product.findById(prodId)
|
|
104
|
+
.then(product => {
|
|
105
|
+
if (!product) {
|
|
106
|
+
return res.status(404).json({
|
|
107
|
+
success: false,
|
|
108
|
+
message: "Product not found"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Initialize ratings array if it doesn't exist
|
|
113
|
+
if (!product.ratings) {
|
|
114
|
+
product.ratings = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if user already rated this product
|
|
118
|
+
const existingRatingIndex = product.ratings.findIndex(
|
|
119
|
+
(rating) => rating.postedby.toString() === userId.toString()
|
|
120
|
+
);
|
|
159
121
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
122
|
+
const ratingData = {
|
|
123
|
+
star: Number(star),
|
|
124
|
+
comment: comment.trim(),
|
|
125
|
+
postedby: userId,
|
|
126
|
+
createdAt: new Date()
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (existingRatingIndex !== -1) {
|
|
130
|
+
// Update existing rating
|
|
131
|
+
product.ratings[existingRatingIndex] = {
|
|
132
|
+
...product.ratings[existingRatingIndex],
|
|
133
|
+
...ratingData,
|
|
134
|
+
updatedAt: new Date()
|
|
135
|
+
};
|
|
136
|
+
} else {
|
|
137
|
+
// Add new rating
|
|
138
|
+
product.ratings.push(ratingData);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Calculate average rating
|
|
142
|
+
const totalRatingSum = product.ratings.reduce((sum, rating) => sum + rating.star, 0);
|
|
143
|
+
const averageRating = totalRatingSum / product.ratings.length;
|
|
144
|
+
|
|
145
|
+
// Update product with new rating
|
|
146
|
+
product.totalrating = Number(averageRating.toFixed(1));
|
|
147
|
+
product.totalRatings = product.ratings.length;
|
|
148
|
+
|
|
149
|
+
// Save updated product and return the promise
|
|
150
|
+
return product.save().then(updatedProduct => ({
|
|
151
|
+
updatedProduct,
|
|
152
|
+
existingRatingIndex,
|
|
153
|
+
ratingData
|
|
154
|
+
}));
|
|
155
|
+
})
|
|
156
|
+
.then(({ updatedProduct, existingRatingIndex, ratingData }) => {
|
|
157
|
+
return res.status(200).json({
|
|
158
|
+
success: true,
|
|
159
|
+
message: existingRatingIndex !== -1 ? "Rating updated successfully" : "Rating added successfully",
|
|
160
|
+
result: updatedProduct,
|
|
161
|
+
rating: {
|
|
162
|
+
averageRating: updatedProduct.totalrating,
|
|
163
|
+
totalRatings: updatedProduct.totalRatings,
|
|
164
|
+
userRating: ratingData
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
})
|
|
168
|
+
.catch(error => {
|
|
169
|
+
console.error('Error in rating process:', error);
|
|
170
|
+
return res.status(500).json({
|
|
171
|
+
success: false,
|
|
172
|
+
message: "Failed to process rating",
|
|
173
|
+
error: error.message
|
|
174
|
+
});
|
|
175
|
+
});
|
|
181
176
|
});
|
|
182
177
|
|
|
183
178
|
/**
|
|
@@ -185,19 +180,10 @@ exports.rating = tryCatch(async (req, res) => {
|
|
|
185
180
|
* @param {Object} req - Express request object
|
|
186
181
|
* @param {Object} res - Express response object
|
|
187
182
|
*/
|
|
188
|
-
exports.getProductReviews = tryCatch(
|
|
189
|
-
const productId = req.params.id;
|
|
183
|
+
exports.getProductReviews = tryCatch((req, res) => {
|
|
184
|
+
const productId = req.params.id || req.body.productId;
|
|
190
185
|
const { page = 1, limit = 10, sortBy = 'newest' } = req.query;
|
|
191
186
|
|
|
192
|
-
// Validate product exists
|
|
193
|
-
const product = await Product.findById(productId);
|
|
194
|
-
if (!product) {
|
|
195
|
-
return res.status(404).json({
|
|
196
|
-
success: false,
|
|
197
|
-
message: "Product not found"
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
187
|
let sortCriteria;
|
|
202
188
|
switch (sortBy) {
|
|
203
189
|
case 'oldest':
|
|
@@ -215,28 +201,39 @@ exports.getProductReviews = tryCatch(async (req, res) => {
|
|
|
215
201
|
break;
|
|
216
202
|
}
|
|
217
203
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
204
|
+
// Validate product exists
|
|
205
|
+
return Product.findById(productId)
|
|
206
|
+
.then(product => {
|
|
207
|
+
if (!product) {
|
|
208
|
+
return res.status(404).json({
|
|
209
|
+
success: false,
|
|
210
|
+
message: "Product not found"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const totalReviews = product.reviews.length;
|
|
215
|
+
const totalPages = Math.ceil(totalReviews / Number(limit));
|
|
216
|
+
|
|
217
|
+
return res.status(200).json({
|
|
218
|
+
success: true,
|
|
219
|
+
message: "Product reviews retrieved successfully",
|
|
220
|
+
result: product.reviews,
|
|
221
|
+
pagination: {
|
|
222
|
+
currentPage: Number(page),
|
|
223
|
+
totalPages,
|
|
224
|
+
totalReviews,
|
|
225
|
+
limit: Number(limit)
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
})
|
|
229
|
+
.catch(error => {
|
|
230
|
+
console.error('Error fetching reviews:', error);
|
|
231
|
+
return res.status(500).json({
|
|
232
|
+
success: false,
|
|
233
|
+
message: "Failed to fetch reviews",
|
|
234
|
+
error: error.message
|
|
235
|
+
});
|
|
236
|
+
});
|
|
240
237
|
});
|
|
241
238
|
|
|
242
239
|
// Extend with CRUD methods
|
|
@@ -245,4 +242,4 @@ for (const prop in crud) {
|
|
|
245
242
|
if (crud.hasOwnProperty(prop)) {
|
|
246
243
|
module.exports[prop] = crud[prop];
|
|
247
244
|
}
|
|
248
|
-
}
|
|
245
|
+
}
|
package/libs/emailer/smtp.js
CHANGED
|
@@ -21,7 +21,8 @@ module.exports = function (fear) {
|
|
|
21
21
|
const { apiKey, domain, region } = _this.mailConfig.mailgun;
|
|
22
22
|
|
|
23
23
|
if (!apiKey || !domain) {
|
|
24
|
-
|
|
24
|
+
logger.error('Mailgun requires apiKey and domain in configuration.')
|
|
25
|
+
return;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const mailgun = new Mailgun(_this.mailConfig.mailgun);
|
package/models/review.js
CHANGED
|
@@ -3,12 +3,13 @@ const mongoose = require("mongoose");
|
|
|
3
3
|
const reviewSchema = new mongoose.Schema({
|
|
4
4
|
productId: { type: mongoose.Schema.Types.ObjectId, ref:"Product", required:true },
|
|
5
5
|
username: { type: String, required: true },
|
|
6
|
-
email: { type: String, required: false,
|
|
6
|
+
email: { type: String, required: false, lowercase: true, trim: true,
|
|
7
7
|
match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
|
|
8
8
|
},
|
|
9
|
-
rating: { type:Number, required:true, min:1, max:5 },
|
|
10
|
-
comment: { type:String, required:true },
|
|
11
|
-
createdAt: { type:Date, default:Date.now }
|
|
9
|
+
rating: { type: Number, required:true, min:1, max:5 },
|
|
10
|
+
comment: { type: String, required:true },
|
|
11
|
+
createdAt: { type: Date, default:Date.now },
|
|
12
|
+
verified: { type: Boolean, default: false, index: true }
|
|
12
13
|
},
|
|
13
14
|
{ timestamps: true, versionKey:false}
|
|
14
15
|
);
|
package/package.json
CHANGED
package/routes/review.js
CHANGED
|
@@ -2,10 +2,14 @@ const Review = require("../controllers/review");
|
|
|
2
2
|
|
|
3
3
|
module.exports = (fear) => {
|
|
4
4
|
const router = fear.createRouter();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
router.
|
|
5
|
+
const handler = fear.getHandler();
|
|
6
|
+
|
|
7
|
+
router.get("/all", handler.async(Review.all));
|
|
8
|
+
router.post("/new", handler.async(Review.add));
|
|
9
|
+
router.route("/:id")
|
|
10
|
+
.put(Review.update)
|
|
11
|
+
.delete(Review.delete)
|
|
12
|
+
.get(Review.getProductReviews);
|
|
13
|
+
|
|
10
14
|
return router;
|
|
11
15
|
};
|