freshyo/packages/db_helper_sqlite/src/admin-apis/product.ts
2026-04-27 21:21:11 +05:30

852 lines
24 KiB
TypeScript

import { db } from '../db/db_index'
import {
productInfo,
units,
specialDeals,
productSlots,
productTags,
productReviews,
productGroupInfo,
productGroupMembership,
productTagInfo,
users,
storeInfo,
} from '../db/schema'
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import type {
AdminProduct,
AdminProductGroupInfo,
AdminProductTagInfo,
AdminProductTagWithProducts,
AdminProductReview,
AdminProductWithDetails,
AdminProductWithRelations,
AdminSpecialDeal,
AdminUnit,
AdminUpdateSlotProductsResult,
Store,
} from '@packages/shared'
type ProductRow = InferSelectModel<typeof productInfo>
type UnitRow = InferSelectModel<typeof units>
type StoreRow = InferSelectModel<typeof storeInfo>
type SpecialDealRow = InferSelectModel<typeof specialDeals>
type ProductTagInfoRow = InferSelectModel<typeof productTagInfo>
type ProductTagRow = InferSelectModel<typeof productTags>
type ProductGroupRow = InferSelectModel<typeof productGroupInfo>
type ProductGroupMembershipRow = InferSelectModel<typeof productGroupMembership>
type ProductReviewRow = InferSelectModel<typeof productReviews>
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
const mapUnit = (unit: UnitRow): AdminUnit => ({
id: unit.id,
shortNotation: unit.shortNotation,
fullName: unit.fullName,
})
const mapStore = (store: StoreRow): Store => ({
id: store.id,
name: store.name,
description: store.description,
imageUrl: store.imageUrl,
owner: store.owner,
createdAt: store.createdAt,
// updatedAt: store.createdAt,
})
const mapProduct = (product: ProductRow): AdminProduct => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
longDescription: product.longDescription ?? null,
unitId: product.unitId,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
images: getStringArray(product.images),
imageKeys: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
isSuspended: product.isSuspended,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice ? String(product.flashPrice) : null,
createdAt: product.createdAt,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.storeId,
})
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
id: deal.id,
productId: deal.productId,
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
validTill: deal.validTill,
})
const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription ?? null,
imageUrl: tag.imageUrl ?? null,
isDashboardTag: tag.isDashboardTag,
relatedStores: tag.relatedStores,
createdAt: tag.createdAt,
})
export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
type ProductWithRelationsRow = ProductRow & { unit: UnitRow; store: StoreRow | null }
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
unit: true,
store: true,
},
}) as ProductWithRelationsRow[]
return products.map((product) => ({
...mapProduct(product),
unit: mapUnit(product.unit),
store: product.store ? mapStore(product.store) : null,
}))
}
export async function getProductById(id: number): Promise<AdminProductWithDetails | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
},
})
if (!product) {
return null
}
const deals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, id),
orderBy: specialDeals.quantity,
})
const productTagsData = await db.query.productTags.findMany({
where: eq(productTags.productId, id),
with: {
tag: true,
},
}) as Array<ProductTagRow & { tag: ProductTagInfoRow }>
return {
...mapProduct(product),
unit: mapUnit(product.unit),
deals: deals.map(mapSpecialDeal),
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
}
}
export async function deleteProduct(id: number): Promise<AdminProduct | null> {
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
.returning()
if (!deletedProduct) {
return null
}
return mapProduct(deletedProduct)
}
type ProductInfoInsert = InferInsertModel<typeof productInfo>
type ProductInfoUpdate = Partial<ProductInfoInsert>
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
const productQuantityRaw = (input as any).productQuantity
const productQuantity = typeof productQuantityRaw === 'string'
? Number(productQuantityRaw)
: productQuantityRaw
const safeProductQuantity = typeof productQuantity === 'number' && Number.isFinite(productQuantity)
? productQuantity
: 1
const [product] = await db.insert(productInfo).values({
...input,
productQuantity: safeProductQuantity,
}).returning()
return mapProduct(product)
}
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
const productQuantityRaw = (updates as any).productQuantity
const productQuantity = typeof productQuantityRaw === 'string'
? Number(productQuantityRaw)
: productQuantityRaw
const safeUpdates = typeof productQuantityRaw === 'undefined'
? updates
: {
...updates,
productQuantity: typeof productQuantity === 'number' && Number.isFinite(productQuantity)
? productQuantity
: 1,
}
const [product] = await db.update(productInfo)
.set(safeUpdates)
.where(eq(productInfo.id, id))
.returning()
if (!product) {
return null
}
return mapProduct(product)
}
export async function toggleProductOutOfStock(id: number): Promise<AdminProduct | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
})
if (!product) {
return null
}
const [updatedProduct] = await db
.update(productInfo)
.set({
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productInfo.id, id))
.returning()
if (!updatedProduct) {
return null
}
return mapProduct(updatedProduct)
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
}) as Array<{ productId: number }>
const currentProductIds = currentAssociations.map((assoc: { productId: number }) => assoc.productId)
const newProductIds = productIds.map((id: string) => parseInt(id))
const productsToAdd = newProductIds.filter((id: number) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id: number) => !newProductIds.includes(id))
if (productsToRemove.length > 0) {
await db.delete(productSlots).where(
and(
eq(productSlots.slotId, parseInt(slotId)),
inArray(productSlots.productId, productsToRemove)
)
)
}
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map((productId) => ({
productId,
slotId: parseInt(slotId),
}))
await db.insert(productSlots).values(newAssociations)
}
return {
message: 'Slot products updated successfully',
added: productsToAdd.length,
removed: productsToRemove.length,
}
}
export async function getSlotProductIds(slotId: string): Promise<number[]> {
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
})
return associations.map((assoc: { productId: number }) => assoc.productId)
}
export async function getAllUnits(): Promise<AdminUnit[]> {
const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation,
})
return allUnits.map(mapUnit)
}
export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]> {
const tags = await db.query.productTagInfo.findMany({
with: {
products: {
with: {
product: true,
},
},
},
}) as Array<ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }>
return tags.map((tag: ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }) => ({
...mapTagInfo(tag),
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})),
}))
}
export async function getAllProductTagInfos(): Promise<AdminProductTagInfo[]> {
const tags = await db.query.productTagInfo.findMany({
orderBy: productTagInfo.tagName,
})
return tags.map(mapTagInfo)
}
export async function getProductTagInfoById(tagId: number): Promise<AdminProductTagInfo | null> {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, tagId),
})
if (!tag) {
return null
}
return mapTagInfo(tag)
}
export interface CreateProductTagInput {
tagName: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
}
export async function createProductTag(input: CreateProductTagInput): Promise<AdminProductTagWithProducts> {
const [tag] = await db.insert(productTagInfo).values({
tagName: input.tagName,
tagDescription: input.tagDescription || null,
imageUrl: input.imageUrl || null,
isDashboardTag: input.isDashboardTag || false,
relatedStores: input.relatedStores || [],
}).returning()
return {
...mapTagInfo(tag),
products: [],
}
}
export async function getProductTagById(tagId: number): Promise<AdminProductTagWithProducts | null> {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, tagId),
with: {
products: {
with: {
product: true,
},
},
},
})
if (!tag) {
return null
}
return {
...mapTagInfo(tag),
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})),
}
}
export interface UpdateProductTagInput {
tagName?: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
}
export async function updateProductTag(tagId: number, input: UpdateProductTagInput): Promise<AdminProductTagWithProducts> {
const [tag] = await db.update(productTagInfo).set({
...(input.tagName !== undefined && { tagName: input.tagName }),
...(input.tagDescription !== undefined && { tagDescription: input.tagDescription }),
...(input.imageUrl !== undefined && { imageUrl: input.imageUrl }),
...(input.isDashboardTag !== undefined && { isDashboardTag: input.isDashboardTag }),
...(input.relatedStores !== undefined && { relatedStores: input.relatedStores }),
}).where(eq(productTagInfo.id, tagId)).returning()
const fullTag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, tagId),
with: {
products: {
with: {
product: true,
},
},
},
})
return {
...mapTagInfo(tag),
products: fullTag?.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
productId: assignment.productId,
tagId: assignment.tagId,
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})) || [],
}
}
export async function deleteProductTag(tagId: number): Promise<void> {
await db.delete(productTagInfo).where(eq(productTagInfo.id, tagId))
}
export async function checkProductTagExistsByName(tagName: string): Promise<boolean> {
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.tagName, tagName),
})
return !!tag
}
export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
if (slotIds.length === 0) {
return {}
}
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
}) as Array<{ slotId: number; productId: number }>
const result: Record<number, number[]> = {}
for (const assoc of associations) {
if (!result[assoc.slotId]) {
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
}
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = []
}
})
return result
}
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
adminResponse: productReviews.adminResponse,
adminResponseImages: productReviews.adminResponseImages,
userName: users.name,
})
.from(productReviews)
.innerJoin(users, eq(productReviews.userId, users.id))
.where(eq(productReviews.productId, productId))
.orderBy(desc(productReviews.reviewTime))
.limit(limit)
.offset(offset)
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
const totalCount = Number(totalCountResult[0].count)
const mappedReviews: AdminProductReview[] = reviews.map((review: any) => ({
id: review.id,
reviewBody: review.reviewBody,
ratings: review.ratings,
imageUrls: review.imageUrls,
reviewTime: review.reviewTime,
adminResponse: review.adminResponse ?? null,
adminResponseImages: review.adminResponseImages,
userName: review.userName ?? null,
}))
return {
reviews: mappedReviews,
totalCount,
}
}
export async function respondToReview(
reviewId: number,
adminResponse: string | undefined,
adminResponseImages: string[]
): Promise<AdminProductReview | null> {
const [updatedReview] = await db
.update(productReviews)
.set({
adminResponse,
adminResponseImages,
})
.where(eq(productReviews.id, reviewId))
.returning()
if (!updatedReview) {
return null
}
return {
id: updatedReview.id,
reviewBody: updatedReview.reviewBody,
ratings: updatedReview.ratings,
imageUrls: updatedReview.imageUrls,
reviewTime: updatedReview.reviewTime,
adminResponse: updatedReview.adminResponse ?? null,
adminResponseImages: updatedReview.adminResponseImages,
userName: null,
}
}
export async function getAllProductGroups() {
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
with: {
product: true,
},
},
},
orderBy: desc(productGroupInfo.createdAt),
})
return groups.map((group: any) => ({
id: group.id,
groupName: group.groupName,
description: group.description ?? null,
createdAt: group.createdAt,
products: group.memberships.map((membership: any) => mapProduct(membership.product)),
productCount: group.memberships.length,
memberships: group.memberships
}))
}
export async function createProductGroup(
groupName: string,
description: string | undefined,
productIds: number[]
): Promise<AdminProductGroupInfo> {
const [newGroup] = await db
.insert(productGroupInfo)
.values({
groupName,
description,
})
.returning()
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: newGroup.id,
}))
await db.insert(productGroupMembership).values(memberships)
}
return {
id: newGroup.id,
groupName: newGroup.groupName,
description: newGroup.description ?? null,
createdAt: newGroup.createdAt,
}
}
export async function updateProductGroup(
id: number,
groupName: string | undefined,
description: string | undefined,
productIds: number[] | undefined
): Promise<AdminProductGroupInfo | null> {
const updateData: Partial<{
groupName: string
description: string | null
}> = {}
if (groupName !== undefined) updateData.groupName = groupName
if (description !== undefined) updateData.description = description
const [updatedGroup] = await db
.update(productGroupInfo)
.set(updateData)
.where(eq(productGroupInfo.id, id))
.returning()
if (!updatedGroup) {
return null
}
if (productIds !== undefined) {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: id,
}))
await db.insert(productGroupMembership).values(memberships)
}
}
return {
id: updatedGroup.id,
groupName: updatedGroup.groupName,
description: updatedGroup.description ?? null,
createdAt: updatedGroup.createdAt,
}
}
export async function deleteProductGroup(id: number): Promise<AdminProductGroupInfo | null> {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
const [deletedGroup] = await db
.delete(productGroupInfo)
.where(eq(productGroupInfo.id, id))
.returning()
if (!deletedGroup) {
return null
}
return {
id: deletedGroup.id,
groupName: deletedGroup.groupName,
description: deletedGroup.description ?? null,
createdAt: deletedGroup.createdAt,
}
}
export async function addProductToGroup(groupId: number, productId: number): Promise<void> {
await db.insert(productGroupMembership).values({ groupId, productId })
}
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
await db.delete(productGroupMembership)
.where(and(
eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId)
))
}
export async function updateProductPrices(updates: Array<{
productId: number
price?: number
marketPrice?: number | null
flashPrice?: number | null
isFlashAvailable?: boolean
}>) {
if (updates.length === 0) {
return { updatedCount: 0, invalidIds: [] }
}
const productIds = updates.map((update) => update.productId)
const existingProducts = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true },
}) as Array<{ id: number }>
const existingIds = new Set(existingProducts.map((product: { id: number }) => product.id))
const invalidIds = productIds.filter((id) => !existingIds.has(id))
if (invalidIds.length > 0) {
return { updatedCount: 0, invalidIds }
}
const updatePromises = updates.map((update) => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: Partial<Pick<ProductInfoInsert, 'price' | 'marketPrice' | 'flashPrice' | 'isFlashAvailable'>> = {}
if (price !== undefined) updateData.price = price.toString()
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
return db
.update(productInfo)
.set(updateData)
.where(eq(productInfo.id, productId))
})
await Promise.all(updatePromises)
return { updatedCount: updates.length, invalidIds: [] }
}
// ==========================================================================
// Product Helpers for Admin Controller
// ==========================================================================
export async function checkProductExistsByName(name: string): Promise<boolean> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.name, name),
columns: { id: true },
})
return !!product
}
export async function checkUnitExists(unitId: number): Promise<boolean> {
const unit = await db.query.units.findFirst({
where: eq(units.id, unitId),
columns: { id: true },
})
return !!unit
}
export async function getProductImagesById(productId: number): Promise<string[] | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
columns: { images: true },
})
if (!product) {
return null
}
return getStringArray(product.images) || []
}
export interface CreateSpecialDealInput {
quantity: number
price: number
validTill: string | Date
}
export async function createSpecialDealsForProduct(
productId: number,
deals: CreateSpecialDealInput[]
): Promise<AdminSpecialDeal[]> {
if (deals.length === 0) {
return []
}
const dealInserts = deals.map((deal) => ({
productId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: new Date(deal.validTill),
}))
const createdDeals = await db
.insert(specialDeals)
.values(dealInserts)
.returning()
return createdDeals.map(mapSpecialDeal)
}
export async function updateProductDeals(
productId: number,
deals: CreateSpecialDealInput[]
): Promise<void> {
if (deals.length === 0) {
await db.delete(specialDeals).where(eq(specialDeals.productId, productId))
return
}
const existingDeals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, productId),
})
const existingDealsMap = new Map<string, SpecialDealRow>(
existingDeals.map((deal: SpecialDealRow) => [`${deal.quantity}-${deal.price}`, deal])
)
const newDealsMap = new Map<string, CreateSpecialDealInput>(
deals.map((deal) => [`${deal.quantity}-${deal.price}`, deal])
)
const dealsToAdd = deals.filter((deal) => {
const key = `${deal.quantity}-${deal.price}`
return !existingDealsMap.has(key)
})
const dealsToRemove = existingDeals.filter((deal: SpecialDealRow) => {
const key = `${deal.quantity}-${deal.price}`
return !newDealsMap.has(key)
})
const dealsToUpdate = deals.filter((deal: CreateSpecialDealInput) => {
const key = `${deal.quantity}-${deal.price}`
const existing = existingDealsMap.get(key)
const nextValidTill = deal.validTill instanceof Date
? deal.validTill.toISOString().split('T')[0]
: String(deal.validTill)
return existing && existing.validTill.toISOString().split('T')[0] !== nextValidTill
})
if (dealsToRemove.length > 0) {
await db.delete(specialDeals).where(
inArray(specialDeals.id, dealsToRemove.map((deal: SpecialDealRow) => deal.id))
)
}
if (dealsToAdd.length > 0) {
const dealInserts = dealsToAdd.map((deal) => ({
productId,
quantity: deal.quantity.toString(),
price: deal.price.toString(),
validTill: new Date(deal.validTill),
}))
await db.insert(specialDeals).values(dealInserts)
}
for (const deal of dealsToUpdate) {
const key = `${deal.quantity}-${deal.price}`
const existingDeal = existingDealsMap.get(key)
if (existingDeal) {
await db.update(specialDeals)
.set({ validTill: new Date(deal.validTill) })
.where(eq(specialDeals.id, existingDeal.id))
}
}
}
export async function replaceProductTags(productId: number, tagIds: number[]): Promise<void> {
await db.delete(productTags).where(eq(productTags.productId, productId))
if (tagIds.length === 0) {
return
}
const tagAssociations = tagIds.map((tagId) => ({
productId,
tagId,
}))
await db.insert(productTags).values(tagAssociations)
}