165 lines
4.8 KiB
JavaScript
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,
|
|
}
|