230 lines
7.0 KiB
JavaScript
230 lines
7.0 KiB
JavaScript
const userDB = require("../db/user.db")
|
||
const commentDB = require("../db/comment.db")
|
||
const {
|
||
addCommentToRedis,
|
||
removeCommentFromRedis,
|
||
getCommentsForDeal,
|
||
} = require("./redis/commentCache.service")
|
||
const { getOrCacheDeal, getDealIdByCommentId } = require("./redis/dealCache.service")
|
||
const { generateCommentId } = require("./redis/commentId.service")
|
||
const {
|
||
queueCommentCreate,
|
||
queueCommentDelete,
|
||
queueNotificationCreate,
|
||
} = require("./redis/dbSync.service")
|
||
const { publishNotification } = require("./redis/notificationPubsub.service")
|
||
const { trackUserCategoryInterest, USER_INTEREST_ACTIONS } = require("./userInterest.service")
|
||
const { sanitizeOptionalPlainText } = require("../utils/inputSanitizer")
|
||
|
||
function parseParentId(value) {
|
||
if (value === undefined || value === null || value === "" || value === "null") return null
|
||
const pid = Number(value)
|
||
if (!Number.isInteger(pid) || pid <= 0) throw new Error("Gecersiz parentId.")
|
||
return pid
|
||
}
|
||
|
||
function normalizeSort(value) {
|
||
const normalized = String(value || "new").trim().toLowerCase()
|
||
if (["top", "best", "liked"].includes(normalized)) return "TOP"
|
||
return "NEW"
|
||
}
|
||
|
||
async function ensureDealCached(dealId) {
|
||
return getOrCacheDeal(dealId, { ttlSeconds: 15 * 60 })
|
||
}
|
||
|
||
async function getCommentsByDealId(dealId, { parentId, page, limit, sort, viewer } = {}) {
|
||
const id = Number(dealId)
|
||
const deal = await ensureDealCached(id)
|
||
if (!deal) throw new Error("Deal bulunamadi.")
|
||
|
||
return getCommentsForDeal({
|
||
dealId: id,
|
||
deal,
|
||
parentId: parseParentId(parentId),
|
||
page,
|
||
limit,
|
||
sort: normalizeSort(sort),
|
||
viewerId: viewer?.userId ?? null,
|
||
})
|
||
}
|
||
|
||
async function createComment({ dealId, userId, text, parentId = null }) {
|
||
const normalizedText = sanitizeOptionalPlainText(text, { maxLength: 2000 })
|
||
if (!normalizedText) {
|
||
throw new Error("Yorum bos olamaz.")
|
||
}
|
||
|
||
const deal = await ensureDealCached(dealId)
|
||
if (!deal) throw new Error("Deal bulunamadi.")
|
||
if (deal.status !== "ACTIVE" && deal.status !== "EXPIRED") {
|
||
throw new Error("Bu deal icin yorum acilamaz.")
|
||
}
|
||
|
||
let parent = null
|
||
if (parentId != null) {
|
||
const pid = parseParentId(parentId)
|
||
const comments = Array.isArray(deal.comments) ? deal.comments : []
|
||
const cachedParent = comments.find((c) => Number(c.id) === Number(pid))
|
||
if (!cachedParent || cachedParent.deletedAt) throw new Error("Yanıtlanan yorum bulunamadi.")
|
||
if (Number(cachedParent.dealId) !== Number(dealId)) {
|
||
throw new Error("Yanıtlanan yorum bu deal'a ait degil.")
|
||
}
|
||
parent = {
|
||
id: cachedParent.id,
|
||
dealId: cachedParent.dealId,
|
||
userId: Number(cachedParent.userId) || null,
|
||
}
|
||
}
|
||
|
||
const user = await userDB.findUser(
|
||
{ id: userId },
|
||
{ select: { id: true, username: true, avatarUrl: true } }
|
||
)
|
||
if (!user) throw new Error("Kullanici bulunamadi.")
|
||
|
||
const createdAt = new Date()
|
||
const commentId = await generateCommentId()
|
||
const comment = {
|
||
id: commentId,
|
||
text: normalizedText,
|
||
userId,
|
||
dealId,
|
||
parentId: parent ? parent.id : null,
|
||
createdAt,
|
||
likeCount: 0,
|
||
repliesCount: 0,
|
||
user,
|
||
}
|
||
|
||
await addCommentToRedis({
|
||
...comment,
|
||
repliesCount: 0,
|
||
})
|
||
|
||
queueCommentCreate({
|
||
commentId,
|
||
dealId,
|
||
userId,
|
||
text: normalizedText,
|
||
parentId: parent ? parent.id : null,
|
||
createdAt: createdAt.toISOString(),
|
||
}).catch((err) => console.error("DB sync comment create failed:", err?.message || err))
|
||
|
||
trackUserCategoryInterest({
|
||
userId,
|
||
categoryId: deal.categoryId,
|
||
action: USER_INTEREST_ACTIONS.COMMENT_CREATE,
|
||
}).catch((err) => console.error("User interest track failed:", err?.message || err))
|
||
|
||
const parentUserId = Number(parent?.userId)
|
||
if (
|
||
parent &&
|
||
Number.isInteger(parentUserId) &&
|
||
parentUserId > 0 &&
|
||
parentUserId !== Number(userId)
|
||
) {
|
||
const notificationPayload = {
|
||
userId: parentUserId,
|
||
message: "Yorumuna cevap geldi.",
|
||
type: "COMMENT_REPLY",
|
||
extras: {
|
||
dealId: Number(dealId),
|
||
commentId: Number(commentId),
|
||
parentCommentId: Number(parent.id),
|
||
},
|
||
createdAt: createdAt.toISOString(),
|
||
}
|
||
queueNotificationCreate(notificationPayload).catch((err) =>
|
||
console.error("DB sync comment reply notification failed:", err?.message || err)
|
||
)
|
||
publishNotification(notificationPayload).catch((err) =>
|
||
console.error("Comment reply notification publish failed:", err?.message || err)
|
||
)
|
||
}
|
||
|
||
return comment
|
||
}
|
||
|
||
async function deleteComment(commentId, userId) {
|
||
const cid = Number(commentId)
|
||
if (!Number.isInteger(cid) || cid <= 0) throw new Error("Gecersiz commentId.")
|
||
|
||
let dealId = await getDealIdByCommentId(cid)
|
||
let dbFallback = null
|
||
if (!dealId) {
|
||
dbFallback = await commentDB.findComment(
|
||
{ id: cid },
|
||
{ select: { id: true, dealId: true, userId: true, parentId: true, deletedAt: true } }
|
||
)
|
||
if (!dbFallback || dbFallback.deletedAt) throw new Error("Yorum bulunamadi.")
|
||
dealId = dbFallback.dealId
|
||
}
|
||
|
||
const deal = await ensureDealCached(dealId)
|
||
if (!deal) throw new Error("Yorum bulunamadi.")
|
||
|
||
const comments = Array.isArray(deal.comments) ? deal.comments : []
|
||
const comment = comments.find((c) => Number(c.id) === cid)
|
||
const effective = comment || dbFallback
|
||
if (!effective || effective.deletedAt) throw new Error("Yorum bulunamadi.")
|
||
if (Number(effective.userId) !== Number(userId)) throw new Error("Bu yorumu silme yetkin yok.")
|
||
|
||
queueCommentDelete({
|
||
commentId: cid,
|
||
dealId: effective.dealId,
|
||
createdAt: new Date().toISOString(),
|
||
}).catch((err) => console.error("DB sync comment delete failed:", err?.message || err))
|
||
|
||
await removeCommentFromRedis({
|
||
commentId: cid,
|
||
dealId: effective.dealId,
|
||
})
|
||
|
||
return { message: "Yorum silindi." }
|
||
}
|
||
|
||
async function deleteCommentAsMod(commentId) {
|
||
const cid = Number(commentId)
|
||
if (!Number.isInteger(cid) || cid <= 0) throw new Error("Gecersiz commentId.")
|
||
|
||
let dealId = await getDealIdByCommentId(cid)
|
||
let dbFallback = null
|
||
if (!dealId) {
|
||
dbFallback = await commentDB.findComment(
|
||
{ id: cid },
|
||
{ select: { id: true, dealId: true, userId: true, parentId: true, deletedAt: true } }
|
||
)
|
||
if (!dbFallback || dbFallback.deletedAt) throw new Error("Yorum bulunamadi.")
|
||
dealId = dbFallback.dealId
|
||
}
|
||
|
||
const deal = await ensureDealCached(dealId)
|
||
if (!deal) throw new Error("Yorum bulunamadi.")
|
||
|
||
const comments = Array.isArray(deal.comments) ? deal.comments : []
|
||
const comment = comments.find((c) => Number(c.id) === cid)
|
||
const effective = comment || dbFallback
|
||
if (!effective || effective.deletedAt) throw new Error("Yorum bulunamadi.")
|
||
|
||
queueCommentDelete({
|
||
commentId: cid,
|
||
dealId: effective.dealId,
|
||
createdAt: new Date().toISOString(),
|
||
}).catch((err) => console.error("DB sync comment delete failed:", err?.message || err))
|
||
|
||
await removeCommentFromRedis({
|
||
commentId: cid,
|
||
dealId: effective.dealId,
|
||
})
|
||
|
||
return { message: "Yorum silindi." }
|
||
}
|
||
|
||
module.exports = {
|
||
getCommentsByDealId,
|
||
createComment,
|
||
deleteComment,
|
||
deleteCommentAsMod,
|
||
}
|