325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
const categoryDb = require("../db/category.db")
|
|
const sellerDb = require("../db/seller.db")
|
|
const { slugify } = require("../utils/slugify")
|
|
const {
|
|
listCategoriesFromRedis,
|
|
setCategoryInRedis,
|
|
setCategoriesInRedis,
|
|
getCategoryById,
|
|
} = require("./redis/categoryCache.service")
|
|
const {
|
|
listSellersFromRedis,
|
|
setSellerInRedis,
|
|
setSellersInRedis,
|
|
getSellerById,
|
|
setSellerDomainInRedis,
|
|
} = require("./redis/sellerCache.service")
|
|
const {
|
|
queueCategoryUpsert,
|
|
queueSellerUpsert,
|
|
queueSellerDomainUpsert,
|
|
} = require("./redis/dbSync.service")
|
|
const { ensureCategoryIdCounter, generateCategoryId } = require("./redis/categoryId.service")
|
|
const { ensureSellerIdCounter, generateSellerId } = require("./redis/sellerId.service")
|
|
const { sanitizeOptionalPlainText } = require("../utils/inputSanitizer")
|
|
const { normalizeMediaPath } = require("../utils/mediaPath")
|
|
|
|
function httpError(statusCode, message) {
|
|
const err = new Error(message)
|
|
err.statusCode = statusCode
|
|
return err
|
|
}
|
|
|
|
function normalizeCategoryPayload(input = {}, fallback = {}) {
|
|
const name =
|
|
input.name !== undefined
|
|
? sanitizeOptionalPlainText(input.name, { maxLength: 120 }) || ""
|
|
: sanitizeOptionalPlainText(fallback.name, { maxLength: 120 }) || ""
|
|
const rawSlug =
|
|
input.slug !== undefined
|
|
? sanitizeOptionalPlainText(input.slug, { maxLength: 160 }) || ""
|
|
: sanitizeOptionalPlainText(fallback.slug, { maxLength: 160 }) || ""
|
|
const slug = rawSlug ? slugify(rawSlug) : name ? slugify(name) : fallback.slug
|
|
const description =
|
|
input.description !== undefined
|
|
? sanitizeOptionalPlainText(input.description, { maxLength: 300 }) || ""
|
|
: sanitizeOptionalPlainText(fallback.description, { maxLength: 300 }) || ""
|
|
const parentId =
|
|
input.parentId !== undefined && input.parentId !== null
|
|
? Number(input.parentId)
|
|
: input.parentId === null
|
|
? null
|
|
: fallback.parentId ?? null
|
|
const isActive =
|
|
input.isActive !== undefined ? Boolean(input.isActive) : Boolean(fallback.isActive ?? true)
|
|
|
|
return { name, slug, description, parentId, isActive }
|
|
}
|
|
|
|
async function ensureCategoryParent(parentId) {
|
|
if (parentId === null || parentId === undefined) return null
|
|
const pid = Number(parentId)
|
|
if (!Number.isInteger(pid) || pid < 0) throw httpError(400, "INVALID_PARENT_ID")
|
|
if (pid === 0) return 0
|
|
const cached = await getCategoryById(pid)
|
|
if (cached) return pid
|
|
const fromDb = await categoryDb.findCategoryById(pid, { select: { id: true, name: true, slug: true, parentId: true, isActive: true, description: true } })
|
|
if (!fromDb) throw httpError(404, "CATEGORY_PARENT_NOT_FOUND")
|
|
await setCategoryInRedis(fromDb)
|
|
return pid
|
|
}
|
|
|
|
async function listCategoriesCached() {
|
|
let categories = await listCategoriesFromRedis()
|
|
if (!categories.length) {
|
|
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
|
|
.map((cat) => ({
|
|
id: cat.id,
|
|
name: cat.name,
|
|
slug: cat.slug,
|
|
parentId: cat.parentId ?? null,
|
|
isActive: cat.isActive !== undefined ? Boolean(cat.isActive) : true,
|
|
description: cat.description ?? "",
|
|
}))
|
|
.sort((a, b) => a.id - b.id)
|
|
}
|
|
|
|
async function createCategory(input = {}) {
|
|
const payload = normalizeCategoryPayload(input)
|
|
if (!payload.name) throw httpError(400, "CATEGORY_NAME_REQUIRED")
|
|
if (!payload.slug) throw httpError(400, "CATEGORY_SLUG_REQUIRED")
|
|
|
|
const categories = await listCategoriesCached()
|
|
const duplicate = categories.find((c) => c.slug === payload.slug)
|
|
if (duplicate) throw httpError(400, "CATEGORY_SLUG_EXISTS")
|
|
|
|
await ensureCategoryIdCounter()
|
|
const id = await generateCategoryId()
|
|
const parentId = await ensureCategoryParent(payload.parentId ?? null)
|
|
|
|
const category = {
|
|
id,
|
|
name: payload.name,
|
|
slug: payload.slug,
|
|
parentId: parentId ?? null,
|
|
isActive: payload.isActive,
|
|
description: payload.description ?? "",
|
|
}
|
|
|
|
await setCategoryInRedis(category)
|
|
queueCategoryUpsert({
|
|
categoryId: id,
|
|
data: {
|
|
name: category.name,
|
|
slug: category.slug,
|
|
parentId: category.parentId,
|
|
isActive: category.isActive,
|
|
description: category.description ?? "",
|
|
},
|
|
updatedAt: new Date().toISOString(),
|
|
}).catch((err) => console.error("DB sync category create failed:", err?.message || err))
|
|
|
|
return category
|
|
}
|
|
|
|
async function updateCategory(categoryId, input = {}) {
|
|
const id = Number(categoryId)
|
|
if (!Number.isInteger(id) || id < 0) throw httpError(400, "INVALID_CATEGORY_ID")
|
|
|
|
const cached = await getCategoryById(id)
|
|
const existing =
|
|
cached ||
|
|
(await categoryDb.findCategoryById(id, {
|
|
select: { id: true, name: true, slug: true, parentId: true, isActive: true, description: true },
|
|
}))
|
|
if (!existing) throw httpError(404, "CATEGORY_NOT_FOUND")
|
|
|
|
const payload = normalizeCategoryPayload(input, existing)
|
|
if (!payload.name) throw httpError(400, "CATEGORY_NAME_REQUIRED")
|
|
if (!payload.slug) throw httpError(400, "CATEGORY_SLUG_REQUIRED")
|
|
if (payload.parentId !== null && Number(payload.parentId) === id) {
|
|
throw httpError(400, "INVALID_PARENT_ID")
|
|
}
|
|
|
|
const categories = await listCategoriesCached()
|
|
const duplicate = categories.find((c) => c.slug === payload.slug && Number(c.id) !== id)
|
|
if (duplicate) throw httpError(400, "CATEGORY_SLUG_EXISTS")
|
|
|
|
const parentId = await ensureCategoryParent(payload.parentId ?? null)
|
|
const category = {
|
|
id,
|
|
name: payload.name,
|
|
slug: payload.slug,
|
|
parentId: parentId ?? null,
|
|
isActive: payload.isActive,
|
|
description: payload.description ?? "",
|
|
}
|
|
|
|
await setCategoryInRedis(category)
|
|
queueCategoryUpsert({
|
|
categoryId: id,
|
|
data: {
|
|
name: category.name,
|
|
slug: category.slug,
|
|
parentId: category.parentId,
|
|
isActive: category.isActive,
|
|
description: category.description ?? "",
|
|
},
|
|
updatedAt: new Date().toISOString(),
|
|
}).catch((err) => console.error("DB sync category update failed:", err?.message || err))
|
|
|
|
return category
|
|
}
|
|
|
|
function normalizeSellerPayload(input = {}, fallback = {}) {
|
|
const name =
|
|
input.name !== undefined
|
|
? sanitizeOptionalPlainText(input.name, { maxLength: 120 }) || ""
|
|
: sanitizeOptionalPlainText(fallback.name, { maxLength: 120 }) || ""
|
|
const url =
|
|
input.url !== undefined
|
|
? sanitizeOptionalPlainText(input.url, { maxLength: 500 }) || ""
|
|
: sanitizeOptionalPlainText(fallback.url, { maxLength: 500 }) || ""
|
|
const sellerLogo =
|
|
input.sellerLogo !== undefined
|
|
? normalizeMediaPath(sanitizeOptionalPlainText(input.sellerLogo, { maxLength: 500 }) || "") || ""
|
|
: normalizeMediaPath(sanitizeOptionalPlainText(fallback.sellerLogo, { maxLength: 500 }) || "") || ""
|
|
const isActive =
|
|
input.isActive !== undefined ? Boolean(input.isActive) : Boolean(fallback.isActive ?? true)
|
|
return { name, url: url ?? "", sellerLogo: sellerLogo ?? "", isActive }
|
|
}
|
|
|
|
async function listSellersCached() {
|
|
let sellers = await listSellersFromRedis()
|
|
if (!sellers.length) {
|
|
sellers = await sellerDb.findSellers({}, {
|
|
select: { id: true, name: true, url: true, sellerLogo: true, isActive: true },
|
|
orderBy: { name: "asc" },
|
|
})
|
|
if (sellers.length) await setSellersInRedis(sellers)
|
|
}
|
|
return sellers.map((seller) => ({
|
|
id: seller.id,
|
|
name: seller.name,
|
|
url: seller.url ?? "",
|
|
sellerLogo: normalizeMediaPath(seller.sellerLogo) ?? "",
|
|
isActive: seller.isActive !== undefined ? Boolean(seller.isActive) : true,
|
|
}))
|
|
}
|
|
|
|
async function createSeller(input = {}, { createdById } = {}) {
|
|
const payload = normalizeSellerPayload(input)
|
|
if (!payload.name) throw httpError(400, "SELLER_NAME_REQUIRED")
|
|
const creatorId = Number(createdById)
|
|
if (!Number.isInteger(creatorId) || creatorId <= 0) throw httpError(400, "CREATED_BY_REQUIRED")
|
|
|
|
const sellers = await listSellersCached()
|
|
const duplicate = sellers.find((s) => s.name.toLowerCase() === payload.name.toLowerCase())
|
|
if (duplicate) throw httpError(400, "SELLER_NAME_EXISTS")
|
|
|
|
await ensureSellerIdCounter()
|
|
const id = await generateSellerId()
|
|
const seller = {
|
|
id,
|
|
name: payload.name,
|
|
url: payload.url ?? "",
|
|
sellerLogo: payload.sellerLogo ?? "",
|
|
isActive: payload.isActive,
|
|
}
|
|
|
|
await setSellerInRedis(seller)
|
|
queueSellerUpsert({
|
|
sellerId: id,
|
|
data: {
|
|
name: seller.name,
|
|
url: seller.url ?? "",
|
|
sellerLogo: seller.sellerLogo ?? "",
|
|
isActive: seller.isActive,
|
|
createdById: creatorId,
|
|
},
|
|
updatedAt: new Date().toISOString(),
|
|
}).catch((err) => console.error("DB sync seller create failed:", err?.message || err))
|
|
|
|
if (input.domain) {
|
|
const domain = (sanitizeOptionalPlainText(input.domain, { maxLength: 255 }) || "").toLowerCase()
|
|
if (domain) {
|
|
await setSellerDomainInRedis(domain, id)
|
|
queueSellerDomainUpsert({ sellerId: id, domain, createdById: creatorId }).catch((err) =>
|
|
console.error("DB sync seller domain failed:", err?.message || err)
|
|
)
|
|
}
|
|
}
|
|
|
|
return seller
|
|
}
|
|
|
|
async function updateSeller(sellerId, input = {}, { createdById } = {}) {
|
|
const id = Number(sellerId)
|
|
if (!Number.isInteger(id) || id <= 0) throw httpError(400, "INVALID_SELLER_ID")
|
|
const creatorId = Number(createdById)
|
|
if (!Number.isInteger(creatorId) || creatorId <= 0) throw httpError(400, "CREATED_BY_REQUIRED")
|
|
|
|
const cached = await getSellerById(id)
|
|
const existing =
|
|
cached ||
|
|
(await sellerDb.findSeller({ id }, { select: { id: true, name: true, url: true, sellerLogo: true, isActive: true } }))
|
|
if (!existing) throw httpError(404, "SELLER_NOT_FOUND")
|
|
|
|
const payload = normalizeSellerPayload(input, existing)
|
|
if (!payload.name) throw httpError(400, "SELLER_NAME_REQUIRED")
|
|
|
|
const sellers = await listSellersCached()
|
|
const duplicate = sellers.find(
|
|
(s) => s.name.toLowerCase() === payload.name.toLowerCase() && Number(s.id) !== id
|
|
)
|
|
if (duplicate) throw httpError(400, "SELLER_NAME_EXISTS")
|
|
|
|
const seller = {
|
|
id,
|
|
name: payload.name,
|
|
url: payload.url ?? "",
|
|
sellerLogo: payload.sellerLogo ?? "",
|
|
isActive: payload.isActive,
|
|
}
|
|
|
|
await setSellerInRedis(seller)
|
|
queueSellerUpsert({
|
|
sellerId: id,
|
|
data: {
|
|
name: seller.name,
|
|
url: seller.url ?? "",
|
|
sellerLogo: seller.sellerLogo ?? "",
|
|
isActive: seller.isActive,
|
|
},
|
|
updatedAt: new Date().toISOString(),
|
|
}).catch((err) => console.error("DB sync seller update failed:", err?.message || err))
|
|
|
|
if (input.domain) {
|
|
const domain = (sanitizeOptionalPlainText(input.domain, { maxLength: 255 }) || "").toLowerCase()
|
|
if (domain) {
|
|
await setSellerDomainInRedis(domain, id)
|
|
queueSellerDomainUpsert({ sellerId: id, domain, createdById: creatorId }).catch((err) =>
|
|
console.error("DB sync seller domain failed:", err?.message || err)
|
|
)
|
|
}
|
|
}
|
|
|
|
return seller
|
|
}
|
|
|
|
module.exports = {
|
|
listCategoriesCached,
|
|
createCategory,
|
|
updateCategory,
|
|
listSellersCached,
|
|
createSeller,
|
|
updateSeller,
|
|
}
|