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, }