HotTRDealsBackend/services/category.service.js
2026-02-09 21:47:55 +00:00

165 lines
4.8 KiB
JavaScript

const categoryDb = require("../db/category.db")
const dealService = require("./deal.service")
const { listCategoriesFromRedis, setCategoriesInRedis, setCategoryInRedis } = require("./redis/categoryCache.service")
function normalizeCategory(category = {}) {
const id = Number(category.id)
if (!Number.isInteger(id) || id < 0) return null
const parentIdRaw = category.parentId
const parentId =
parentIdRaw === null || parentIdRaw === undefined ? null : Number(parentIdRaw)
return {
id,
name: category.name,
slug: String(category.slug || "").trim().toLowerCase(),
parentId: Number.isInteger(parentId) ? parentId : null,
isActive: category.isActive !== undefined ? Boolean(category.isActive) : true,
description: category.description ?? "",
}
}
function buildCategoryMaps(categories = []) {
const byId = new Map()
const bySlug = new Map()
categories.forEach((item) => {
const category = normalizeCategory(item)
if (!category) return
byId.set(category.id, category)
if (category.slug) bySlug.set(category.slug, category)
})
return { byId, bySlug }
}
function getCategoryBreadcrumbFromMap(categoryId, byId, { includeUndefined = false } = {}) {
const currentId = Number(categoryId)
if (!Number.isInteger(currentId)) return []
const path = []
const visited = new Set()
let nextId = currentId
while (true) {
if (visited.has(nextId)) break
visited.add(nextId)
const category = byId.get(nextId)
if (!category) break
if (includeUndefined || category.id !== 0) {
path.push({ id: category.id, name: category.name, slug: category.slug })
}
if (category.parentId === null || category.parentId === undefined) break
nextId = Number(category.parentId)
}
return path.reverse()
}
function getCategoryDescendantIdsFromMap(categoryId, categories = []) {
const rootId = Number(categoryId)
if (!Number.isInteger(rootId) || rootId <= 0) return []
const childrenByParent = new Map()
categories.forEach((item) => {
const category = normalizeCategory(item)
if (!category || category.parentId === null) return
const parentId = Number(category.parentId)
if (!Number.isInteger(parentId)) return
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, [])
childrenByParent.get(parentId).push(category.id)
})
const seen = new Set([rootId])
const queue = [rootId]
while (queue.length) {
const current = queue.shift()
const children = childrenByParent.get(current) || []
children.forEach((childId) => {
if (seen.has(childId)) return
seen.add(childId)
queue.push(childId)
})
}
return Array.from(seen)
}
async function listCategoriesCached() {
let categories = await listCategoriesFromRedis()
if (categories.length) return categories
categories = await categoryDb.listCategories({
select: { id: true, name: true, slug: true, parentId: true, isActive: true, description: true },
orderBy: { id: "asc" },
})
if (categories.length) {
await setCategoriesInRedis(categories)
}
return categories
}
async function findCategoryBySlug(slug) {
const normalizedSlug = String(slug || "").trim()
if (!normalizedSlug) {
throw new Error("INVALID_SLUG")
}
const categories = await listCategoriesCached()
if (categories.length) {
const { byId, bySlug } = buildCategoryMaps(categories)
const cachedCategory = bySlug.get(normalizedSlug.toLowerCase())
if (cachedCategory) {
const breadcrumb = getCategoryBreadcrumbFromMap(cachedCategory.id, byId)
return { category: cachedCategory, breadcrumb }
}
}
const category = await categoryDb.findCategoryBySlug(normalizedSlug)
if (!category) {
throw new Error("CATEGORY_NOT_FOUND")
}
const normalizedCategory = normalizeCategory(category) || category
await setCategoryInRedis(normalizedCategory)
const breadcrumb = await categoryDb.getCategoryBreadcrumb(category.id)
return { category: normalizedCategory, breadcrumb }
}
async function getDealsByCategoryId(categoryId, { page = 1, limit = 10, filters = {}, viewer = null, scope = "USER" } = {}) {
const normalizedId = Number(categoryId)
if (!Number.isInteger(normalizedId) || normalizedId <= 0) {
throw new Error("INVALID_CATEGORY_ID")
}
let categoryIds = []
const categories = await listCategoriesCached()
if (categories.length) {
categoryIds = getCategoryDescendantIdsFromMap(normalizedId, categories)
}
if (!categoryIds.length) {
categoryIds = await categoryDb.getCategoryDescendantIds(normalizedId)
}
return dealService.getDeals({
preset: "NEW",
q: filters?.q,
page,
limit,
viewer,
scope,
baseWhere: { categoryId: { in: categoryIds } },
filters,
useRedisSearch: true,
})
}
module.exports = {
findCategoryBySlug,
getDealsByCategoryId,
}