194 lines
5.9 KiB
JavaScript
194 lines
5.9 KiB
JavaScript
const dealDB = require("../db/deal.db")
|
|
const dealSaveDB = require("../db/dealSave.db")
|
|
const { getDealsByIdsFromRedis } = require("./redis/hotDealList.service")
|
|
const {
|
|
ensureUserCache,
|
|
getUserSavedIdsFromRedis,
|
|
addUserSavedDeal,
|
|
removeUserSavedDeal,
|
|
setUserSavedDeals,
|
|
} = require("./redis/userCache.service")
|
|
const { mapDealToRedisJson } = require("./redis/dealIndexing.service")
|
|
const { getOrCacheDeal, updateDealSavesInRedis, setDealInRedis } = require("./redis/dealCache.service")
|
|
const { queueDealSaveUpdate } = require("./redis/dbSync.service")
|
|
|
|
const PAGE_LIMIT = 20
|
|
const ALLOWED_STATUSES = new Set(["ACTIVE", "EXPIRED"])
|
|
|
|
function assertPositiveInt(value, name) {
|
|
const num = Number(value)
|
|
if (!Number.isInteger(num) || num <= 0) throw new Error(`Gecersiz ${name}.`)
|
|
return num
|
|
}
|
|
|
|
function normalizePage(value) {
|
|
const num = Number(value)
|
|
if (!Number.isInteger(num) || num < 1) return 1
|
|
return num
|
|
}
|
|
|
|
const DEAL_CACHE_INCLUDE = {
|
|
user: { select: { id: true, username: true, avatarUrl: true } },
|
|
images: { orderBy: { order: "asc" }, select: { id: true, imageUrl: true, order: true } },
|
|
dealTags: { include: { tag: { select: { id: true, slug: true, name: true } } } },
|
|
votes: { select: { userId: true, voteType: true } },
|
|
savedBy: { select: { userId: true, createdAt: true } },
|
|
comments: {
|
|
orderBy: { createdAt: "desc" },
|
|
include: {
|
|
user: { select: { id: true, username: true, avatarUrl: true } },
|
|
likes: { select: { userId: true } },
|
|
},
|
|
},
|
|
aiReview: {
|
|
select: {
|
|
bestCategoryId: true,
|
|
needsReview: true,
|
|
hasIssue: true,
|
|
issueType: true,
|
|
issueReason: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
async function saveDealForUser({ userId, dealId }) {
|
|
const uid = assertPositiveInt(userId, "userId")
|
|
const did = assertPositiveInt(dealId, "dealId")
|
|
|
|
const deal = await getOrCacheDeal(did, { ttlSeconds: 15 * 60 })
|
|
.catch(() => null)
|
|
if (!deal) {
|
|
const err = new Error("Deal bulunamadi.")
|
|
err.statusCode = 404
|
|
throw err
|
|
}
|
|
if (!ALLOWED_STATUSES.has(String(deal.status))) {
|
|
const err = new Error("Bu deal kaydedilemez.")
|
|
err.statusCode = 400
|
|
throw err
|
|
}
|
|
|
|
await updateDealSavesInRedis({
|
|
dealId: did,
|
|
userId: uid,
|
|
action: "SAVE",
|
|
createdAt: new Date().toISOString(),
|
|
minSeconds: 15 * 60,
|
|
})
|
|
await addUserSavedDeal(uid, did, { ttlSeconds: 60 * 60 })
|
|
queueDealSaveUpdate({
|
|
dealId: did,
|
|
userId: uid,
|
|
action: "SAVE",
|
|
createdAt: new Date().toISOString(),
|
|
}).catch((err) => console.error("DB sync dealSave queue failed:", err?.message || err))
|
|
return { saved: true }
|
|
}
|
|
|
|
async function removeSavedDealForUser({ userId, dealId }) {
|
|
const uid = assertPositiveInt(userId, "userId")
|
|
const did = assertPositiveInt(dealId, "dealId")
|
|
|
|
await updateDealSavesInRedis({
|
|
dealId: did,
|
|
userId: uid,
|
|
action: "UNSAVE",
|
|
createdAt: new Date().toISOString(),
|
|
minSeconds: 15 * 60,
|
|
})
|
|
await removeUserSavedDeal(uid, did, { ttlSeconds: 60 * 60 })
|
|
queueDealSaveUpdate({
|
|
dealId: did,
|
|
userId: uid,
|
|
action: "UNSAVE",
|
|
createdAt: new Date().toISOString(),
|
|
}).catch((err) => console.error("DB sync dealSave queue failed:", err?.message || err))
|
|
return { removed: true }
|
|
}
|
|
|
|
async function listSavedDeals({ userId, page = 1 }) {
|
|
const uid = assertPositiveInt(userId, "userId")
|
|
const safePage = normalizePage(page)
|
|
const skip = (safePage - 1) * PAGE_LIMIT
|
|
|
|
await ensureUserCache(uid, { ttlSeconds: 60 * 60 })
|
|
const redisCache = await getUserSavedIdsFromRedis(uid)
|
|
const redisJsonIds = redisCache?.jsonIds || []
|
|
const savedSet = redisCache?.savedSet || new Set()
|
|
const unsavedSet = redisCache?.unsavedSet || new Set()
|
|
|
|
const where = {
|
|
userId: uid,
|
|
deal: { status: { in: Array.from(ALLOWED_STATUSES) } },
|
|
}
|
|
|
|
const [total, saves] = await Promise.all([
|
|
dealSaveDB.countDealSavesByUser(uid, { where }),
|
|
dealSaveDB.findDealSavesByUser(
|
|
uid,
|
|
{
|
|
skip,
|
|
take: PAGE_LIMIT,
|
|
orderBy: { createdAt: "desc" },
|
|
where,
|
|
}
|
|
),
|
|
])
|
|
|
|
const dbDealIds = saves.map((s) => Number(s.dealId)).filter((id) => Number.isInteger(id) && id > 0)
|
|
const baseDb = dbDealIds.filter((id) => !unsavedSet.has(id))
|
|
const extraSaved = Array.from(savedSet).filter((id) => !unsavedSet.has(id))
|
|
|
|
let mergedIds = []
|
|
if (redisJsonIds.length) {
|
|
const filteredJson = redisJsonIds.filter((id) => !unsavedSet.has(id))
|
|
const jsonSet = new Set(filteredJson)
|
|
const prependSaved = extraSaved.filter((id) => !jsonSet.has(id))
|
|
mergedIds = [...prependSaved, ...filteredJson]
|
|
baseDb.forEach((id) => {
|
|
if (!jsonSet.has(id)) mergedIds.push(id)
|
|
})
|
|
} else {
|
|
const baseSet = new Set(baseDb)
|
|
const prependSaved = extraSaved.filter((id) => !baseSet.has(id))
|
|
mergedIds = [...prependSaved, ...baseDb]
|
|
}
|
|
|
|
await setUserSavedDeals(uid, mergedIds, { ttlSeconds: 60 * 60 })
|
|
const pageIds = mergedIds.slice(skip, skip + PAGE_LIMIT)
|
|
|
|
const cachedDeals = await getDealsByIdsFromRedis(pageIds, uid)
|
|
const cachedMap = new Map(cachedDeals.map((d) => [Number(d.id), d]))
|
|
const missingIds = pageIds.filter((id) => !cachedMap.has(id))
|
|
|
|
if (missingIds.length) {
|
|
const missingDeals = await dealDB.findDeals(
|
|
{ id: { in: missingIds }, status: { in: Array.from(ALLOWED_STATUSES) } },
|
|
{ include: DEAL_CACHE_INCLUDE }
|
|
)
|
|
await Promise.all(
|
|
missingDeals.map((deal) => {
|
|
const payload = mapDealToRedisJson(deal)
|
|
return setDealInRedis(deal.id, payload, { ttlSeconds: 15 * 60 })
|
|
})
|
|
)
|
|
const hydrated = await getDealsByIdsFromRedis(missingIds, uid)
|
|
hydrated.forEach((d) => cachedMap.set(Number(d.id), d))
|
|
}
|
|
|
|
const results = pageIds.map((id) => cachedMap.get(id)).filter(Boolean)
|
|
|
|
return {
|
|
page: safePage,
|
|
total: mergedIds.length,
|
|
totalPages: mergedIds.length ? Math.ceil(mergedIds.length / PAGE_LIMIT) : 0,
|
|
results,
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
saveDealForUser,
|
|
removeSavedDealForUser,
|
|
listSavedDeals,
|
|
}
|