diff --git a/apps/backend/.wrangler/state/v3/do/freshyo-backend-CacheCreator/dfa6f17a813eaf9ad999935788399d71cfee3694a0a726e6d79c51fb1e78afcd.sqlite b/apps/backend/.wrangler/state/v3/do/freshyo-backend-CacheCreator/dfa6f17a813eaf9ad999935788399d71cfee3694a0a726e6d79c51fb1e78afcd.sqlite index 27fd414..508aa59 100644 Binary files a/apps/backend/.wrangler/state/v3/do/freshyo-backend-CacheCreator/dfa6f17a813eaf9ad999935788399d71cfee3694a0a726e6d79c51fb1e78afcd.sqlite and b/apps/backend/.wrangler/state/v3/do/freshyo-backend-CacheCreator/dfa6f17a813eaf9ad999935788399d71cfee3694a0a726e6d79c51fb1e78afcd.sqlite differ diff --git a/apps/backend/src/stores/slot-store.ts b/apps/backend/src/stores/slot-store.ts index 1e90c09..02c6bb5 100644 --- a/apps/backend/src/stores/slot-store.ts +++ b/apps/backend/src/stores/slot-store.ts @@ -42,19 +42,19 @@ async function transformSlotToStoreSlot(slot: SlotWithProductsData): Promise ({ - id: productSlot.product.id, - name: productSlot.product.name, - productQuantity: productSlot.product.productQuantity, - shortDescription: productSlot.product.shortDescription, - price: productSlot.product.price.toString(), - marketPrice: productSlot.product.marketPrice?.toString() || null, - unit: productSlot.product.unit?.shortNotation || null, + products: slot.products.map((product) => ({ + id: product.id, + name: product.name, + productQuantity: product.productQuantity, + shortDescription: product.shortDescription, + price: product.price.toString(), + marketPrice: product.marketPrice?.toString() || null, + unit: product.unit?.shortNotation || null, images: scaffoldAssetUrl( - (productSlot.product.images as string[]) || [] + (product.images as string[]) || [] ), - isOutOfStock: productSlot.product.isOutOfStock, - storeId: productSlot.product.storeId, + isOutOfStock: product.isOutOfStock, + storeId: product.storeId, nextDeliveryDate: slot.deliveryTime, })), } @@ -118,19 +118,19 @@ export async function initializeSlotStore(): Promise { isActive: slot.isActive, isCapacityFull: slot.isCapacityFull, products: await Promise.all( - slot.productSlots.map(async (productSlot) => ({ - id: productSlot.product.id, - name: productSlot.product.name, - productQuantity: productSlot.product.productQuantity, - shortDescription: productSlot.product.shortDescription, - price: productSlot.product.price.toString(), - marketPrice: productSlot.product.marketPrice?.toString() || null, - unit: productSlot.product.unit?.shortNotation || null, + slot.products.map(async (product) => ({ + id: product.id, + name: product.name, + productQuantity: product.productQuantity, + shortDescription: product.shortDescription, + price: product.price.toString(), + marketPrice: product.marketPrice?.toString() || null, + unit: product.unit?.shortNotation || null, images: scaffoldAssetUrl( - (productSlot.product.images as string[]) || [] + (product.images as string[]) || [] ), - isOutOfStock: productSlot.product.isOutOfStock, - storeId: productSlot.product.storeId, + isOutOfStock: product.isOutOfStock, + storeId: product.storeId, nextDeliveryDate: slot.deliveryTime, })) ), @@ -229,7 +229,7 @@ export async function getProductSlots(productId: number): Promise { const productSlots: SlotInfo[] = [] for (const slot of slots) { - const hasProduct = slot.productSlots.some(ps => ps.product.id === productId) + const hasProduct = slot.products.some(p => p.id === productId) if (hasProduct) { productSlots.push(extractSlotInfo(slot)) } @@ -272,8 +272,8 @@ export async function getAllProductsSlots(): Promise> for (const slot of slots) { const slotInfo = extractSlotInfo(slot) - for (const productSlot of slot.productSlots) { - const productId = productSlot.product.id + for (const product of slot.products) { + const productId = product.id if (!result[productId]) { result[productId] = [] } @@ -322,8 +322,8 @@ export async function getMultipleProductsSlots( for (const slot of slots) { const slotInfo = extractSlotInfo(slot) - for (const productSlot of slot.productSlots) { - const pid = productSlot.product.id + for (const product of slot.products) { + const pid = product.id if (productIdSet.has(pid) && !slot.isCapacityFull) { result[pid].push(slotInfo) } diff --git a/apps/backend/wrangler.dev.toml b/apps/backend/wrangler.dev.toml index 101d901..2a4c9e1 100644 --- a/apps/backend/wrangler.dev.toml +++ b/apps/backend/wrangler.dev.toml @@ -18,6 +18,7 @@ bindings = [ [[migrations]] tag = "cache-creator-v1" new_classes = ["CacheCreator"] +migrations_dir = "../../packages/db_helper_sqlite/drizzle/" [[queues.producers]] binding = "NOTIF_QUEUE" diff --git a/packages/db_helper_postgres/drizzle/0077_migrate_product_slots.sql b/packages/db_helper_postgres/drizzle/0077_migrate_product_slots.sql new file mode 100644 index 0000000..9a6c9f7 --- /dev/null +++ b/packages/db_helper_postgres/drizzle/0077_migrate_product_slots.sql @@ -0,0 +1,21 @@ +-- Migration: Replace product_slots junction table with JSONB array in delivery_slot_info +-- Step 1: Add product_ids column to delivery_slot_info +ALTER TABLE delivery_slot_info ADD COLUMN product_ids JSONB DEFAULT '[]'; + +-- Step 2: Migrate data from product_slots to product_ids JSONB array +UPDATE delivery_slot_info +SET product_ids = COALESCE( + (SELECT jsonb_agg(product_id ORDER BY product_id) + FROM product_slots + WHERE product_slots.slot_id = delivery_slot_info.id), + '[]'::jsonb +); + +-- Step 3: Drop the old product_slots table +DROP TABLE IF EXISTS product_slots; + +-- Note: After running this migration, update the schema.ts file to: +-- 1. Add productIds: jsonb('product_ids').$defaultFn(() => []) to deliverySlotInfo +-- 2. Remove the productSlots table definition +-- 3. Remove productSlotsRelations +-- 4. Update deliverySlotInfoRelations to remove productSlots reference diff --git a/packages/db_helper_postgres/src/admin-apis/product.ts b/packages/db_helper_postgres/src/admin-apis/product.ts index f3e10f6..9a9bf6f 100644 --- a/packages/db_helper_postgres/src/admin-apis/product.ts +++ b/packages/db_helper_postgres/src/admin-apis/product.ts @@ -3,7 +3,7 @@ import { productInfo, units, specialDeals, - productSlots, + deliverySlotInfo, productTags, productReviews, productGroupInfo, @@ -203,37 +203,25 @@ export async function toggleProductOutOfStock(id: number): Promise { - const currentAssociations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, parseInt(slotId)), }) - const currentProductIds = currentAssociations.map((assoc) => assoc.productId) + if (!slot) { + throw new Error(`Slot ${slotId} not found`) + } + + const currentProductIds = slot.productIds || [] const newProductIds = productIds.map((id) => parseInt(id)) + // Simply update the productIds array + await db.update(deliverySlotInfo) + .set({ productIds: newProductIds }) + .where(eq(deliverySlotInfo.id, parseInt(slotId))) + const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id)) const productsToRemove = currentProductIds.filter((id) => !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, @@ -242,14 +230,11 @@ export async function updateSlotProducts(slotId: string, productIds: string[]): } export async function getSlotProductIds(slotId: string): Promise { - const associations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, parseInt(slotId)), }) - return associations.map((assoc) => assoc.productId) + return slot?.productIds || [] } export async function getAllUnits(): Promise { @@ -307,20 +292,13 @@ export async function getSlotsProductIds(slotIds: number[]): Promise = {} - for (const assoc of associations) { - if (!result[assoc.slotId]) { - result[assoc.slotId] = [] - } - result[assoc.slotId].push(assoc.productId) + for (const slot of slots) { + result[slot.id] = slot.productIds || [] } slotIds.forEach((slotId) => { diff --git a/packages/db_helper_postgres/src/admin-apis/slots.ts b/packages/db_helper_postgres/src/admin-apis/slots.ts index 2fb7333..96ef6af 100644 --- a/packages/db_helper_postgres/src/admin-apis/slots.ts +++ b/packages/db_helper_postgres/src/admin-apis/slots.ts @@ -1,12 +1,11 @@ import { db } from '../db/db_index' import { deliverySlotInfo, - productSlots, productInfo, vendorSnippets, productGroupInfo, } from '../db/schema' -import { and, asc, desc, eq, gt, inArray, lt } from 'drizzle-orm' +import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm' import type { AdminDeliverySlot, AdminSlotWithProducts, @@ -67,25 +66,35 @@ export async function getActiveSlotsWithProducts(limit: number = 20): Promise() + for (const slot of slots) { + for (const productId of (slot.productIds || [])) { + allProductIds.add(productId) + } + } + + // Fetch all products in one query + const productIdsArray = Array.from(allProductIds) + const productsData = productIdsArray.length > 0 + ? await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIdsArray), + columns: { id: true, name: true, images: true }, + }) + : [] + + // Create a map for quick lookup + const productMap = new Map(productsData.map(p => [p.id, p])) + return slots.map((slot) => ({ ...mapDeliverySlot(slot), deliverySequence: getNumberArray(slot.deliverySequence), - products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)), + products: (slot.productIds || []) + .map(productId => productMap.get(productId)) + .filter((p): p is NonNullable => p != null) + .map(product => mapSlotProductSummary(product)), })) } @@ -104,8 +113,11 @@ export async function staleSlotsCleanup(): Promise { // Get the threshold (the 20th most recent slot ID - minimum of the 20) const threshold = Math.min(...recentSlots.map((s) => s.id)) - // Delete product_slots for all slots older than threshold - const result = await db.delete(productSlots).where(lt(productSlots.slotId, threshold)) + // Clear productIds for all slots older than threshold + const result = await db + .update(deliverySlotInfo) + .set({ productIds: [] }) + .where(eq(deliverySlotInfo.id, threshold)) return result.rowCount || 0 } @@ -134,17 +146,6 @@ export async function getSlotByIdWithRelations(id: number): Promise 0 + ? await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIds), + columns: { id: true, name: true, images: true }, + }) + : [] + return { ...mapDeliverySlot(slot), deliverySequence: getNumberArray(slot.deliverySequence), groupIds: getNumberArray(slot.groupIds), - products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)), + products: productsData.map(product => mapSlotProductSummary(product)), vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet), } } @@ -180,17 +190,10 @@ export async function createSlotWithRelations(input: { freezeTime: new Date(freezeTime), isActive: isActive !== undefined ? isActive : true, groupIds: groupIds !== undefined ? groupIds : [], + productIds: productIds !== undefined ? productIds : [], }) .returning() - if (productIds && productIds.length > 0) { - const associations = productIds.map((productId) => ({ - productId, - slotId: newSlot.id, - })) - await tx.insert(productSlots).values(associations) - } - let createdSnippets: AdminVendorSnippet[] = [] if (snippets && snippets.length > 0) { for (const snippet of snippets) { @@ -257,6 +260,7 @@ export async function updateSlotWithRelations(input: { freezeTime: new Date(freezeTime), isActive: isActive !== undefined ? isActive : true, groupIds: validGroupIds !== undefined ? validGroupIds : [], + ...(productIds !== undefined && { productIds }), }) .where(eq(deliverySlotInfo.id, id)) .returning() @@ -265,18 +269,6 @@ export async function updateSlotWithRelations(input: { return null } - if (productIds !== undefined) { - await tx.delete(productSlots).where(eq(productSlots.slotId, id)) - - if (productIds.length > 0) { - const associations = productIds.map((productId) => ({ - productId, - slotId: id, - })) - await tx.insert(productSlots).values(associations) - } - } - let createdSnippets: AdminVendorSnippet[] = [] if (snippets && snippets.length > 0) { for (const snippet of snippets) { diff --git a/packages/db_helper_postgres/src/db/schema.ts b/packages/db_helper_postgres/src/db/schema.ts index a876101..97687af 100755 --- a/packages/db_helper_postgres/src/db/schema.ts +++ b/packages/db_helper_postgres/src/db/schema.ts @@ -195,6 +195,7 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', { isCapacityFull: boolean('is_capacity_full').notNull().default(false), deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}), groupIds: jsonb('group_ids').$defaultFn(() => []), + productIds: jsonb('product_ids').$defaultFn(() => []), }); export const vendorSnippets = mf.table('vendor_snippets', { @@ -211,13 +212,6 @@ export const vendorSnippetsRelations = relations(vendorSnippets, ({ one }) => ({ slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }), })); -export const productSlots = mf.table('product_slots', { - productId: integer('product_id').notNull().references(() => productInfo.id), - slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id), -}, (t) => ({ - pk: unique('product_slot_pk').on(t.productId, t.slotId), -})); - export const specialDeals = mf.table('special_deals', { id: integer().primaryKey().generatedAlwaysAsIdentity(), productId: integer('product_id').notNull().references(() => productInfo.id), @@ -509,7 +503,6 @@ export const unitsRelations = relations(units, ({ many }) => ({ export const productInfoRelations = relations(productInfo, ({ one, many }) => ({ unit: one(units, { fields: [productInfo.unitId], references: [units.id] }), store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }), - productSlots: many(productSlots), specialDeals: many(specialDeals), orderItems: many(orderItems), cartItems: many(cartItems), @@ -529,16 +522,10 @@ export const productTagsRelations = relations(productTags, ({ one }) => ({ })); export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({ - productSlots: many(productSlots), orders: many(orders), vendorSnippets: many(vendorSnippets), })); -export const productSlotsRelations = relations(productSlots, ({ one }) => ({ - product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }), - slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }), -})); - export const specialDealsRelations = relations(specialDeals, ({ one }) => ({ product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }), })); diff --git a/packages/db_helper_postgres/src/db/seed.ts b/packages/db_helper_postgres/src/db/seed.ts index f894534..bd43727 100644 --- a/packages/db_helper_postgres/src/db/seed.ts +++ b/packages/db_helper_postgres/src/db/seed.ts @@ -1,5 +1,5 @@ import { db } from "@/src/db/db_index" -import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema" +import { units, productInfo, deliverySlotInfo, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema" import { eq } from "drizzle-orm"; import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter' import { CONST_KEYS } from '@/src/lib/const-keys' diff --git a/packages/db_helper_postgres/src/db/types.ts b/packages/db_helper_postgres/src/db/types.ts index 64d42de..93c1f99 100755 --- a/packages/db_helper_postgres/src/db/types.ts +++ b/packages/db_helper_postgres/src/db/types.ts @@ -5,7 +5,6 @@ import type { units, productInfo, deliverySlotInfo, - productSlots, specialDeals, orders, orderItems, @@ -21,7 +20,6 @@ export type Address = InferSelectModel; export type Unit = InferSelectModel; export type ProductInfo = InferSelectModel; export type DeliverySlotInfo = InferSelectModel; -export type ProductSlot = InferSelectModel; export type SpecialDeal = InferSelectModel; export type Order = InferSelectModel; export type OrderItem = InferSelectModel; diff --git a/packages/db_helper_postgres/src/helper_methods/product.ts b/packages/db_helper_postgres/src/helper_methods/product.ts index 13c4b51..a9eb10b 100644 --- a/packages/db_helper_postgres/src/helper_methods/product.ts +++ b/packages/db_helper_postgres/src/helper_methods/product.ts @@ -1,5 +1,5 @@ import { db } from '../db/db_index'; -import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; +import { productInfo, units, specialDeals, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; export async function getAllProducts(): Promise { @@ -18,11 +18,6 @@ export async function getProductById(id: number): Promise { with: { unit: true, store: true, - productSlots: { - with: { - slot: true, - }, - }, specialDeals: true, productTags: { with: { diff --git a/packages/db_helper_postgres/src/helper_methods/slots.ts b/packages/db_helper_postgres/src/helper_methods/slots.ts index bcc2f01..6779bc2 100644 --- a/packages/db_helper_postgres/src/helper_methods/slots.ts +++ b/packages/db_helper_postgres/src/helper_methods/slots.ts @@ -1,30 +1,44 @@ import { db } from '../db/db_index'; -import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from '../db/schema'; +import { deliverySlotInfo, productInfo, vendorSnippets } from '../db/schema'; import { eq, and, inArray, desc } from 'drizzle-orm'; export async function getAllSlots(): Promise { - return await db.query.deliverySlotInfo.findMany({ + const slots = await db.query.deliverySlotInfo.findMany({ orderBy: desc(deliverySlotInfo.createdAt), with: { - productSlots: { - with: { - product: true, - }, - }, vendorSnippets: true, }, }); + + // Fetch products for all slots + const allProductIds = new Set(); + for (const slot of slots) { + for (const productId of (slot.productIds || [])) { + allProductIds.add(productId); + } + } + + const productIdsArray = Array.from(allProductIds); + const productsData = productIdsArray.length > 0 + ? await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIdsArray), + }) + : []; + + const productMap = new Map(productsData.map(p => [p.id, p])); + + return slots.map(slot => ({ + ...slot, + products: (slot.productIds || []) + .map(productId => productMap.get(productId)) + .filter((p): p is NonNullable => p != null), + })); } export async function getSlotById(id: number): Promise { - return await db.query.deliverySlotInfo.findFirst({ + const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, id), with: { - productSlots: { - with: { - product: true, - }, - }, vendorSnippets: { with: { slot: true, @@ -32,6 +46,23 @@ export async function getSlotById(id: number): Promise { }, }, }); + + if (!slot) { + return null; + } + + // Fetch products for this slot + const productIds = slot.productIds || []; + const productsData = productIds.length > 0 + ? await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIds), + }) + : []; + + return { + ...slot, + products: productsData, + }; } export async function createSlot(input: any): Promise { @@ -52,28 +83,62 @@ export async function deleteSlot(id: number): Promise { } export async function getSlotProducts(slotId: number): Promise { - return await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, slotId), - with: { - product: true, - }, + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); + + if (!slot) { + return []; + } + + const productIds = slot.productIds || []; + if (productIds.length === 0) { + return []; + } + + return await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIds), }); } export async function addProductToSlot(slotId: number, productId: number): Promise { - await db.insert(productSlots).values({ slotId, productId }); + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); + + if (!slot) { + throw new Error(`Slot ${slotId} not found`); + } + + const currentProductIds = slot.productIds || []; + if (!currentProductIds.includes(productId)) { + await db.update(deliverySlotInfo) + .set({ productIds: [...currentProductIds, productId] }) + .where(eq(deliverySlotInfo.id, slotId)); + } } export async function removeProductFromSlot(slotId: number, productId: number): Promise { - await db.delete(productSlots) - .where(and( - eq(productSlots.slotId, slotId), - eq(productSlots.productId, productId) - )); + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); + + if (!slot) { + throw new Error(`Slot ${slotId} not found`); + } + + const currentProductIds = slot.productIds || []; + const updatedProductIds = currentProductIds.filter(id => id !== productId); + + await db.update(deliverySlotInfo) + .set({ productIds: updatedProductIds }) + .where(eq(deliverySlotInfo.id, slotId)); } export async function clearSlotProducts(slotId: number): Promise { - await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); + await db.update(deliverySlotInfo) + .set({ productIds: [] }) + .where(eq(deliverySlotInfo.id, slotId)); } export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise { diff --git a/packages/db_helper_postgres/src/stores/store-helpers.ts b/packages/db_helper_postgres/src/stores/store-helpers.ts index a18ab53..fe8075c 100644 --- a/packages/db_helper_postgres/src/stores/store-helpers.ts +++ b/packages/db_helper_postgres/src/stores/store-helpers.ts @@ -6,7 +6,6 @@ import { homeBanners, productInfo, units, - productSlots, deliverySlotInfo, specialDeals, storeInfo, @@ -112,23 +111,30 @@ export async function getAllStoresForCache(): Promise { } export async function getAllDeliverySlotsForCache(): Promise { - return db - .select({ - productId: productSlots.productId, - id: deliverySlotInfo.id, - deliveryTime: deliverySlotInfo.deliveryTime, - freezeTime: deliverySlotInfo.freezeTime, - isCapacityFull: deliverySlotInfo.isCapacityFull, - }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where( - and( - eq(deliverySlotInfo.isActive, true), - eq(deliverySlotInfo.isCapacityFull, false), - gt(deliverySlotInfo.deliveryTime, sql`NOW()`) - ) - ) + const slots = await db.query.deliverySlotInfo.findMany({ + where: and( + eq(deliverySlotInfo.isActive, true), + eq(deliverySlotInfo.isCapacityFull, false), + gt(deliverySlotInfo.deliveryTime, sql`NOW()`) + ), + }) + + // Flatten slots with their product IDs + const result: DeliverySlotData[] = [] + for (const slot of slots) { + const productIds = slot.productIds || [] + for (const productId of productIds) { + result.push({ + productId, + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isCapacityFull: slot.isCapacityFull, + }) + } + } + + return result } export async function getAllSpecialDealsForCache(): Promise { @@ -203,45 +209,84 @@ export interface SlotWithProductsData { freezeTime: Date isActive: boolean isCapacityFull: boolean - productSlots: Array<{ - product: { - id: number - name: string - productQuantity: number - shortDescription: string | null - price: string - marketPrice: string | null - unit: { shortNotation: string } | null - store: { id: number; name: string; description: string | null } | null - images: unknown - isOutOfStock: boolean - storeId: number | null - } + products: Array<{ + id: number + name: string + productQuantity: number + shortDescription: string | null + price: string + marketPrice: string | null + unit: { shortNotation: string } | null + store: { id: number; name: string; description: string | null } | null + images: unknown + isOutOfStock: boolean + storeId: number | null }> } export async function getAllSlotsWithProductsForCache(): Promise { const now = new Date() - return db.query.deliverySlotInfo.findMany({ + // Get all active future slots + const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, now), + gt(deliverySlotInfo.deliveryTime, now) ), - with: { - productSlots: { - with: { - product: { - with: { - unit: true, - store: true, - }, - }, - }, - }, - }, orderBy: asc(deliverySlotInfo.deliveryTime), - }) as Promise + }) + + // Get all unique product IDs from all slots + const allProductIds = new Set() + for (const slot of slots) { + for (const productId of (slot.productIds || [])) { + allProductIds.add(productId) + } + } + + // Fetch all products in one query + const productIdsArray = Array.from(allProductIds) + const productsData = productIdsArray.length > 0 + ? await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIdsArray), + with: { + unit: true, + store: true, + }, + }) + : [] + + // Create a map for quick lookup + const productMap = new Map(productsData.map(p => [p.id, p])) + + // Build the result + return slots.map(slot => ({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isActive: slot.isActive, + isCapacityFull: slot.isCapacityFull, + products: (slot.productIds || []) + .map(productId => productMap.get(productId)) + .filter((p): p is NonNullable => p != null) + .map(product => ({ + id: product.id, + name: product.name, + productQuantity: product.productQuantity, + shortDescription: product.shortDescription, + price: String(product.price ?? '0'), + marketPrice: product.marketPrice ? String(product.marketPrice) : null, + unit: product.unit ? { shortNotation: product.unit.shortNotation } : null, + store: product.store ? { + id: product.store.id, + name: product.store.name, + description: product.store.description + } : null, + images: product.images, + isOutOfStock: product.isOutOfStock, + storeId: product.storeId, + })), + })) as SlotWithProductsData[] } // ============================================================================ diff --git a/packages/db_helper_postgres/src/user-apis/product.ts b/packages/db_helper_postgres/src/user-apis/product.ts index bdc6616..42ebcc8 100644 --- a/packages/db_helper_postgres/src/user-apis/product.ts +++ b/packages/db_helper_postgres/src/user-apis/product.ts @@ -1,5 +1,5 @@ import { db } from '../db/db_index' -import { productInfo, productReviews, specialDeals, storeInfo, units, users } from '../db/schema' +import { deliverySlotInfo, productInfo, productReviews, specialDeals, storeInfo, units, users } from '../db/schema' import { and, desc, eq, gt, sql } from 'drizzle-orm' import type { UserProductDetailData, UserProductReview } from '@packages/shared' @@ -229,20 +229,22 @@ export async function getSuspendedProductIds(): Promise { * This version filters by both isActive AND isCapacityFull */ export async function getNextDeliveryDateWithCapacity(productId: number): Promise { - const result = await db - .select({ deliveryTime: deliverySlotInfo.deliveryTime }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where( - and( - eq(productSlots.productId, productId), - eq(deliverySlotInfo.isActive, true), - eq(deliverySlotInfo.isCapacityFull, false), - gt(deliverySlotInfo.deliveryTime, sql`NOW()`) - ) - ) - .orderBy(deliverySlotInfo.deliveryTime) - .limit(1) + const slots = await db.query.deliverySlotInfo.findMany({ + where: and( + eq(deliverySlotInfo.isActive, true), + eq(deliverySlotInfo.isCapacityFull, false), + gt(deliverySlotInfo.deliveryTime, sql`NOW()`) + ), + orderBy: desc(deliverySlotInfo.deliveryTime), + }) - return result[0]?.deliveryTime || null + // Find the first slot that contains this product + for (const slot of slots) { + const productIds = slot.productIds || [] + if (productIds.includes(productId)) { + return slot.deliveryTime + } + } + + return null } diff --git a/packages/db_helper_sqlite/drizzle/0001_migrate_product_slots.sql b/packages/db_helper_sqlite/drizzle/0001_migrate_product_slots.sql new file mode 100644 index 0000000..2092245 --- /dev/null +++ b/packages/db_helper_sqlite/drizzle/0001_migrate_product_slots.sql @@ -0,0 +1,24 @@ +-- Migration: Replace product_slots junction table with JSON array in delivery_slot_info +-- Step 1: Add product_ids column to delivery_slot_info +ALTER TABLE `delivery_slot_info` ADD COLUMN `product_ids` text DEFAULT '[]'; + +-- Step 2: Migrate data from product_slots to product_ids JSON array +-- Use a subquery to aggregate product_ids for each slot +UPDATE `delivery_slot_info` +SET `product_ids` = ( + SELECT CASE + WHEN GROUP_CONCAT(`product_id`) IS NULL THEN '[]' + ELSE '[' || GROUP_CONCAT(`product_id`) || ']' + END + FROM `product_slots` + WHERE `product_slots`.`slot_id` = `delivery_slot_info`.`id` +); + +-- Step 3: Drop the old product_slots table +DROP TABLE IF EXISTS `product_slots`; + +-- Note: After running this migration, update the schema.ts file to: +-- 1. Add productIds: jsonText('product_ids').$defaultFn(() => []) to deliverySlotInfo +-- 2. Remove the productSlots table definition +-- 3. Remove productSlotsRelations +-- 4. Update deliverySlotInfoRelations to remove productSlots reference diff --git a/packages/db_helper_sqlite/src/admin-apis/product.ts b/packages/db_helper_sqlite/src/admin-apis/product.ts index 275ebb1..ba7da13 100644 --- a/packages/db_helper_sqlite/src/admin-apis/product.ts +++ b/packages/db_helper_sqlite/src/admin-apis/product.ts @@ -3,7 +3,7 @@ import { productInfo, units, specialDeals, - productSlots, + deliverySlotInfo, productTags, productReviews, productGroupInfo, @@ -229,37 +229,25 @@ export async function toggleProductOutOfStock(id: number): Promise { - const currentAssociations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, - }) as Array<{ productId: number }> + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, parseInt(slotId)), + }) - const currentProductIds = currentAssociations.map((assoc: { productId: number }) => assoc.productId) + if (!slot) { + throw new Error(`Slot ${slotId} not found`) + } + + const currentProductIds = slot.productIds || [] const newProductIds = productIds.map((id: string) => parseInt(id)) + // Simply update the productIds array + await db.update(deliverySlotInfo) + .set({ productIds: newProductIds }) + .where(eq(deliverySlotInfo.id, parseInt(slotId))) + 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, @@ -268,14 +256,11 @@ export async function updateSlotProducts(slotId: string, productIds: string[]): } export async function getSlotProductIds(slotId: string): Promise { - const associations = await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, parseInt(slotId)), - columns: { - productId: true, - }, + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, parseInt(slotId)), }) - return associations.map((assoc: { productId: number }) => assoc.productId) + return slot?.productIds || [] } export async function getAllUnits(): Promise { @@ -433,20 +418,13 @@ export async function getSlotsProductIds(slotIds: number[]): Promise + const slots = await db.query.deliverySlotInfo.findMany({ + where: inArray(deliverySlotInfo.id, slotIds), + }) const result: Record = {} - for (const assoc of associations) { - if (!result[assoc.slotId]) { - result[assoc.slotId] = [] - } - result[assoc.slotId].push(assoc.productId) + for (const slot of slots) { + result[slot.id] = slot.productIds || [] } slotIds.forEach((slotId) => { diff --git a/packages/db_helper_sqlite/src/admin-apis/slots.ts b/packages/db_helper_sqlite/src/admin-apis/slots.ts index 38290d3..68afef0 100644 --- a/packages/db_helper_sqlite/src/admin-apis/slots.ts +++ b/packages/db_helper_sqlite/src/admin-apis/slots.ts @@ -1,12 +1,11 @@ import { db } from '../db/db_index' import { deliverySlotInfo, - productSlots, productInfo, vendorSnippets, productGroupInfo, } from '../db/schema' -import { and, asc, desc, eq, gt, inArray, lt } from 'drizzle-orm' +import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm' import type { AdminDeliverySlot, AdminSlotWithProducts, @@ -54,7 +53,6 @@ const chunkArray = (items: T[], size: number): T[][] => { } const PRODUCT_ID_CHUNK_SIZE = 40 -const PRODUCT_SLOT_CHUNK_SIZE = 40 const fetchExistingProductIds = async (tx: any, productIds: number[]) => { const existingIds = new Set() @@ -103,25 +101,39 @@ export async function getActiveSlotsWithProducts(limit: number = 20): Promise ({ + // Get all unique product IDs from all slots + const allProductIds = new Set() + for (const slot of slots) { + for (const productId of (slot.productIds || [])) { + allProductIds.add(productId) + } + } + + // Fetch all products in one query + const productIdsArray = Array.from(allProductIds) + const productIdsSet = new Set(productIdsArray) + // const productsData = productIdsArray.length > 0 + // ? await db.query.productInfo.findMany({ + // where: inArray(productInfo.id, productIdsArray), + // columns: { id: true, name: true, images: true }, + // }) + // : [] + + let productsData = await db.query.productInfo.findMany({}); + productsData = productsData.filter(item => productIdsSet.has(item.id)) + + // Create a map for quick lookup + const productMap = new Map(productsData.map(p => [p.id, p])) + + return slots.map((slot) => ({ ...mapDeliverySlot(slot), deliverySequence: getNumberArray(slot.deliverySequence), - products: slot.productSlots.map((ps: any) => mapSlotProductSummary(ps.product)), + products: (slot.productIds || []) + .map(productId => productMap.get(productId)) + .filter((p): p is NonNullable => p != null) + .map(product => mapSlotProductSummary(product)), })) } @@ -140,10 +152,13 @@ export async function staleSlotsCleanup(): Promise { // Get the threshold (the 20th most recent slot ID - minimum of the 20) const threshold = Math.min(...recentSlots.map((s) => s.id)) - // Delete product_slots for all slots older than threshold - const result = await db.delete(productSlots).where(lt(productSlots.slotId, threshold)) + // Clear productIds for all slots older than threshold + const result = await db + .update(deliverySlotInfo) + .set({ productIds: [] }) + .where(eq(deliverySlotInfo.id, threshold)) - return result.changes || 0 + return 1 } export async function getActiveSlots(): Promise { @@ -170,17 +185,6 @@ export async function getSlotByIdWithRelations(id: number): Promise 0 + // ? await db.query.productInfo.findMany({ + // where: inArray(productInfo.id, productIds), + // columns: { id: true, name: true, images: true }, + // }) + // : [] + let productsData = productIds.length > 0 + ? await db.query.productInfo.findMany({ + columns: { id: true, name: true, images: true }, + }) + : [] + productsData = productsData.filter(item => productIdSet.has(item.id)) return { ...mapDeliverySlot(slot), deliverySequence: getNumberArray(slot.deliverySequence), groupIds: getNumberArray(slot.groupIds), - products: slot.productSlots.map((ps: any) => mapSlotProductSummary(ps.product)), + products: productsData.map(product => mapSlotProductSummary(product)), vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet), } } @@ -208,7 +227,18 @@ export async function createSlotWithRelations(input: { }): Promise { const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input + const normalizedProductIds = normalizeProductIds(productIds) + const result = await db.transaction(async (tx) => { + // Validate product IDs if provided + if (normalizedProductIds.length > 0) { + const existingIds = await fetchExistingProductIds(tx, normalizedProductIds) + const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId)) + if (missingIds.length > 0) { + throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`) + } + } + const [newSlot] = await tx .insert(deliverySlotInfo) .values({ @@ -216,27 +246,10 @@ export async function createSlotWithRelations(input: { freezeTime: new Date(freezeTime), isActive: isActive !== undefined ? isActive : true, groupIds: groupIds !== undefined ? groupIds : [], + productIds: normalizedProductIds, }) .returning() - const normalizedProductIds = normalizeProductIds(productIds) - if (productIds && normalizedProductIds.length > 0) { - const existingIds = await fetchExistingProductIds(tx, normalizedProductIds) - const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId)) - if (missingIds.length > 0) { - throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`) - } - - const associations = normalizedProductIds.map((productId) => ({ - productId, - slotId: newSlot.id, - })) - const associationChunks = chunkArray(associations, PRODUCT_SLOT_CHUNK_SIZE) - for (const chunk of associationChunks) { - await tx.insert(productSlots).values(chunk) - } - } - let createdSnippets: AdminVendorSnippet[] = [] if (snippets && snippets.length > 0) { for (const snippet of snippets) { @@ -295,7 +308,18 @@ export async function updateSlotWithRelations(input: { validGroupIds = existingGroups.map((group: { id: number }) => group.id) } + const normalizedProductIds = productIds !== undefined ? normalizeProductIds(productIds) : undefined + const result = await db.transaction(async (tx) => { + // Validate product IDs if provided + if (normalizedProductIds !== undefined && normalizedProductIds.length > 0) { + const existingIds = await fetchExistingProductIds(tx, normalizedProductIds) + const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId)) + if (missingIds.length > 0) { + throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`) + } + } + const [updatedSlot] = await tx .update(deliverySlotInfo) .set({ @@ -303,6 +327,7 @@ export async function updateSlotWithRelations(input: { freezeTime: new Date(freezeTime), isActive: isActive !== undefined ? isActive : true, groupIds: validGroupIds !== undefined ? validGroupIds : [], + ...(normalizedProductIds !== undefined && { productIds: normalizedProductIds }), }) .where(eq(deliverySlotInfo.id, id)) .returning() @@ -311,28 +336,6 @@ export async function updateSlotWithRelations(input: { return null } - if (productIds !== undefined) { - await tx.delete(productSlots).where(eq(productSlots.slotId, id)) - - const normalizedProductIds = normalizeProductIds(productIds) - if (productIds.length > 0 && normalizedProductIds.length > 0) { - const existingIds = await fetchExistingProductIds(tx, normalizedProductIds) - const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId)) - if (missingIds.length > 0) { - throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`) - } - - const associations = normalizedProductIds.map((productId) => ({ - productId, - slotId: id, - })) - const associationChunks = chunkArray(associations, PRODUCT_SLOT_CHUNK_SIZE) - for (const chunk of associationChunks) { - await tx.insert(productSlots).values(chunk) - } - } - } - let createdSnippets: AdminVendorSnippet[] = [] if (snippets && snippets.length > 0) { for (const snippet of snippets) { diff --git a/packages/db_helper_sqlite/src/db/schema.ts b/packages/db_helper_sqlite/src/db/schema.ts index 2154c87..4985f39 100644 --- a/packages/db_helper_sqlite/src/db/schema.ts +++ b/packages/db_helper_sqlite/src/db/schema.ts @@ -280,6 +280,7 @@ export const deliverySlotInfo = sqliteTable('delivery_slot_info', { isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false), deliverySequence: jsonText>('delivery_sequence').$defaultFn(() => ({})), groupIds: jsonText('group_ids').$defaultFn(() => []), + productIds: jsonText('product_ids').$defaultFn(() => []), }) export const vendorSnippets = sqliteTable('vendor_snippets', { @@ -292,13 +293,6 @@ export const vendorSnippets = sqliteTable('vendor_snippets', { createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`), }) -export const productSlots = sqliteTable('product_slots', { - productId: integer('product_id').notNull().references(() => productInfo.id), - slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id), -}, (t) => ({ - pk: primaryKey({ columns: [t.productId, t.slotId], name: 'product_slot_pk' }), -})) - export const specialDeals = sqliteTable('special_deals', { id: integer().primaryKey({ autoIncrement: true }), productId: integer('product_id').notNull().references(() => productInfo.id), @@ -561,7 +555,6 @@ export const unitsRelations = relations(units, ({ many }) => ({ export const productInfoRelations = relations(productInfo, ({ one, many }) => ({ unit: one(units, { fields: [productInfo.unitId], references: [units.id] }), store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }), - productSlots: many(productSlots), specialDeals: many(specialDeals), orderItems: many(orderItems), cartItems: many(cartItems), @@ -581,16 +574,10 @@ export const productTagsRelations = relations(productTags, ({ one }) => ({ })) export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({ - productSlots: many(productSlots), orders: many(orders), vendorSnippets: many(vendorSnippets), })) -export const productSlotsRelations = relations(productSlots, ({ one }) => ({ - product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }), - slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }), -})) - export const specialDealsRelations = relations(specialDeals, ({ one }) => ({ product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }), })) diff --git a/packages/db_helper_sqlite/src/db/seed.ts b/packages/db_helper_sqlite/src/db/seed.ts index 4cbb512..ab0d951 100644 --- a/packages/db_helper_sqlite/src/db/seed.ts +++ b/packages/db_helper_sqlite/src/db/seed.ts @@ -3,7 +3,6 @@ import { units, productInfo, deliverySlotInfo, - productSlots, keyValStore, staffRoles, staffPermissions, diff --git a/packages/db_helper_sqlite/src/db/types.ts b/packages/db_helper_sqlite/src/db/types.ts index d6e374e..5fb5214 100644 --- a/packages/db_helper_sqlite/src/db/types.ts +++ b/packages/db_helper_sqlite/src/db/types.ts @@ -5,7 +5,6 @@ import type { units, productInfo, deliverySlotInfo, - productSlots, specialDeals, orders, orderItems, @@ -21,7 +20,6 @@ export type Address = InferSelectModel export type Unit = InferSelectModel export type ProductInfo = InferSelectModel export type DeliverySlotInfo = InferSelectModel -export type ProductSlot = InferSelectModel export type SpecialDeal = InferSelectModel export type Order = InferSelectModel export type OrderItem = InferSelectModel diff --git a/packages/db_helper_sqlite/src/helper_methods/product.ts b/packages/db_helper_sqlite/src/helper_methods/product.ts index 192f0b6..52c3722 100644 --- a/packages/db_helper_sqlite/src/helper_methods/product.ts +++ b/packages/db_helper_sqlite/src/helper_methods/product.ts @@ -1,5 +1,5 @@ import { db } from '../db/db_index'; -import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; +import { productInfo, units, specialDeals, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; export async function getAllProducts(): Promise { @@ -18,11 +18,6 @@ export async function getProductById(id: number): Promise { with: { unit: true, store: true, - productSlots: { - with: { - slot: true, - }, - }, specialDeals: true, productTags: { with: { diff --git a/packages/db_helper_sqlite/src/helper_methods/slots.ts b/packages/db_helper_sqlite/src/helper_methods/slots.ts index f12d63b..43dfc9a 100644 --- a/packages/db_helper_sqlite/src/helper_methods/slots.ts +++ b/packages/db_helper_sqlite/src/helper_methods/slots.ts @@ -1,30 +1,44 @@ import { db } from '../db/db_index'; -import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from '../db/schema'; +import { deliverySlotInfo, productInfo, vendorSnippets } from '../db/schema'; import { eq, and, inArray, desc } from 'drizzle-orm'; export async function getAllSlots(): Promise { - return await db.query.deliverySlotInfo.findMany({ + const slots = await db.query.deliverySlotInfo.findMany({ orderBy: desc(deliverySlotInfo.deliveryTime), with: { - productSlots: { - with: { - product: true, - }, - }, vendorSnippets: true, }, }); + + // Fetch products for all slots + const allProductIds = new Set(); + for (const slot of slots) { + for (const productId of (slot.productIds || [])) { + allProductIds.add(productId); + } + } + + const productIdsArray = Array.from(allProductIds); + const productsData = productIdsArray.length > 0 + ? await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIdsArray), + }) + : []; + + const productMap = new Map(productsData.map(p => [p.id, p])); + + return slots.map(slot => ({ + ...slot, + products: (slot.productIds || []) + .map(productId => productMap.get(productId)) + .filter((p): p is NonNullable => p != null), + })); } export async function getSlotById(id: number): Promise { - return await db.query.deliverySlotInfo.findFirst({ + const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, id), with: { - productSlots: { - with: { - product: true, - }, - }, vendorSnippets: { with: { slot: true, @@ -32,6 +46,27 @@ export async function getSlotById(id: number): Promise { }, }, }); + + if (!slot) { + return null; + } + + // Fetch products for this slot + const productIds = slot.productIds || []; + const productIdSet = new Set(productIds) + // const productsData = productIds.length > 0 + // ? await db.query.productInfo.findMany({ + // where: inArray(productInfo.id, productIds), + // }) + // : []; + + let productsData = productIds.length > 0 ? await db.query.productInfo.findMany({}) : []; + productsData = productsData.filter(item => productIdSet.has(item.id)) + + return { + ...slot, + products: productsData, + }; } export async function createSlot(input: any): Promise { @@ -52,28 +87,62 @@ export async function deleteSlot(id: number): Promise { } export async function getSlotProducts(slotId: number): Promise { - return await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, slotId), - with: { - product: true, - }, + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); + + if (!slot) { + return []; + } + + const productIds = slot.productIds || []; + if (productIds.length === 0) { + return []; + } + + return await db.query.productInfo.findMany({ + where: inArray(productInfo.id, productIds), }); } export async function addProductToSlot(slotId: number, productId: number): Promise { - await db.insert(productSlots).values({ slotId, productId }); + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); + + if (!slot) { + throw new Error(`Slot ${slotId} not found`); + } + + const currentProductIds = slot.productIds || []; + if (!currentProductIds.includes(productId)) { + await db.update(deliverySlotInfo) + .set({ productIds: [...currentProductIds, productId] }) + .where(eq(deliverySlotInfo.id, slotId)); + } } export async function removeProductFromSlot(slotId: number, productId: number): Promise { - await db.delete(productSlots) - .where(and( - eq(productSlots.slotId, slotId), - eq(productSlots.productId, productId) - )); + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, slotId), + }); + + if (!slot) { + throw new Error(`Slot ${slotId} not found`); + } + + const currentProductIds = slot.productIds || []; + const updatedProductIds = currentProductIds.filter(id => id !== productId); + + await db.update(deliverySlotInfo) + .set({ productIds: updatedProductIds }) + .where(eq(deliverySlotInfo.id, slotId)); } export async function clearSlotProducts(slotId: number): Promise { - await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); + await db.update(deliverySlotInfo) + .set({ productIds: [] }) + .where(eq(deliverySlotInfo.id, slotId)); } export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise { diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts index 61d2b47..4629d5c 100644 --- a/packages/db_helper_sqlite/src/stores/store-helpers.ts +++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts @@ -6,7 +6,6 @@ import { homeBanners, productInfo, units, - productSlots, deliverySlotInfo, specialDeals, storeInfo, @@ -14,7 +13,7 @@ import { productTagInfo, userIncidents, } from '../db/schema' -import { eq, and, gt, sql, isNotNull, asc } from 'drizzle-orm' +import { eq, and, gt, sql, isNotNull, asc, inArray } from 'drizzle-orm' // ============================================================================ // BANNER STORE HELPERS @@ -119,23 +118,30 @@ export async function getAllStoresForCache(): Promise { } export async function getAllDeliverySlotsForCache(): Promise { - return db - .select({ - productId: productSlots.productId, - id: deliverySlotInfo.id, - deliveryTime: deliverySlotInfo.deliveryTime, - freezeTime: deliverySlotInfo.freezeTime, - isCapacityFull: deliverySlotInfo.isCapacityFull, - }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where( - and( - eq(deliverySlotInfo.isActive, true), - eq(deliverySlotInfo.isCapacityFull, false), - gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) - ) - ) + const slots = await db.query.deliverySlotInfo.findMany({ + where: and( + eq(deliverySlotInfo.isActive, true), + eq(deliverySlotInfo.isCapacityFull, false), + gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) + ), + }) + + // Flatten slots with their product IDs + const result: DeliverySlotData[] = [] + for (const slot of slots) { + const productIds = slot.productIds || [] + for (const productId of productIds) { + result.push({ + productId, + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isCapacityFull: slot.isCapacityFull, + }) + } + } + + return result } export async function getAllSpecialDealsForCache(): Promise { @@ -216,45 +222,96 @@ export interface SlotWithProductsData { freezeTime: Date isActive: boolean isCapacityFull: boolean - productSlots: Array<{ - product: { - id: number - name: string - productQuantity: number - shortDescription: string | null - price: string - marketPrice: string | null - unit: { shortNotation: string } | null - store: { id: number; name: string; description: string | null } | null - images: unknown - isOutOfStock: boolean - storeId: number | null - } + products: Array<{ + id: number + name: string + productQuantity: number + shortDescription: string | null + price: string + marketPrice: string | null + unit: { shortNotation: string } | null + store: { id: number; name: string; description: string | null } | null + images: unknown + isOutOfStock: boolean + storeId: number | null }> } export async function getAllSlotsWithProductsForCache(): Promise { const now = new Date() - return db.query.deliverySlotInfo.findMany({ + // Get all active future slots + const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), gt(deliverySlotInfo.deliveryTime, now) ), - with: { - productSlots: { - with: { - product: { - with: { - unit: true, - store: true, - }, - }, - }, - }, - }, orderBy: asc(deliverySlotInfo.deliveryTime), - }) as Promise + }) + + // Get all unique product IDs from all slots + const allProductIds = new Set() + for (const slot of slots) { + for (const productId of (slot.productIds || [])) { + allProductIds.add(productId) + } + } + + // Fetch all products in one query + const productIdsArray = Array.from(allProductIds) + const productIdSet = new Set(productIdsArray); + // const productsData = productIdsArray.length > 0 + // ? await db.query.productInfo.findMany({ + // where: inArray(productInfo.id, productIdsArray), + // with: { + // unit: true, + // store: true, + // }, + // }) + // : [] + let productsData = productIdsArray.length > 0 + ? await db.query.productInfo.findMany({ + // where: inArray(productInfo.id, productIdsArray), + with: { + unit: true, + store: true, + }, + }) + : [] + + productsData = productsData.filter(item => productIdSet.has(item.id)) + + // Create a map for quick lookup + const productMap = new Map(productsData.map(p => [p.id, p])) + + // Build the result + return slots.map(slot => ({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isActive: slot.isActive, + isCapacityFull: slot.isCapacityFull, + products: (slot.productIds || []) + .map(productId => productMap.get(productId)) + .filter((p): p is NonNullable => p != null) + .map(product => ({ + id: product.id, + name: product.name, + productQuantity: product.productQuantity, + shortDescription: product.shortDescription, + price: String(product.price ?? '0'), + marketPrice: product.marketPrice ? String(product.marketPrice) : null, + unit: product.unit ? { shortNotation: product.unit.shortNotation } : null, + store: product.store ? { + id: product.store.id, + name: product.store.name, + description: product.store.description + } : null, + images: product.images, + isOutOfStock: product.isOutOfStock, + storeId: product.storeId, + })), + })) as SlotWithProductsData[] } // ============================================================================ diff --git a/packages/db_helper_sqlite/src/user-apis/product.ts b/packages/db_helper_sqlite/src/user-apis/product.ts index 921f3f1..308a44f 100644 --- a/packages/db_helper_sqlite/src/user-apis/product.ts +++ b/packages/db_helper_sqlite/src/user-apis/product.ts @@ -1,6 +1,6 @@ import { db } from '../db/db_index' -import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema' -import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm' +import { deliverySlotInfo, productInfo, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema' +import { and, desc, eq, gt, sql } from 'drizzle-orm' import type { UserProductDetailData, UserProductReview } from '@packages/shared' const getStringArray = (value: unknown): string[] | null => { @@ -235,20 +235,22 @@ export async function getSuspendedProductIds(): Promise { * This version filters by both isActive AND isCapacityFull */ export async function getNextDeliveryDateWithCapacity(productId: number): Promise { - const result = await db - .select({ deliveryTime: deliverySlotInfo.deliveryTime }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where( - and( - eq(productSlots.productId, productId), - eq(deliverySlotInfo.isActive, true), - eq(deliverySlotInfo.isCapacityFull, false), - gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) - ) - ) - .orderBy(deliverySlotInfo.deliveryTime) - .limit(1) + const slots = await db.query.deliverySlotInfo.findMany({ + where: and( + eq(deliverySlotInfo.isActive, true), + eq(deliverySlotInfo.isCapacityFull, false), + gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) + ), + orderBy: desc(deliverySlotInfo.deliveryTime), + }) - return result[0]?.deliveryTime || null + // Find the first slot that contains this product + for (const slot of slots) { + const productIds = slot.productIds || [] + if (productIds.includes(productId)) { + return slot.deliveryTime + } + } + + return null }