From 903c5c27bcccd46e6e44be7e0819af9d24b271f5 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:57:48 +0530 Subject: [PATCH] enh --- apps/backend/src/lib/init.ts | 19 +- apps/backend/src/stores/banner-store.ts | 88 +++++++ .../src/{lib => stores}/product-store.ts | 9 +- apps/backend/src/stores/product-tag-store.ts | 115 +++++++++ apps/backend/src/stores/slot-store.ts | 125 ++++++++++ apps/backend/src/stores/store-initializer.ts | 51 ++++ apps/backend/src/trpc/admin-apis/banner.ts | 12 + apps/backend/src/trpc/admin-apis/product.ts | 22 ++ apps/backend/src/trpc/admin-apis/slots.ts | 13 + apps/backend/src/trpc/admin-apis/store.ts | 10 + apps/backend/src/trpc/common-apis/common.ts | 20 +- apps/backend/src/trpc/user-apis/product.ts | 231 ++---------------- apps/backend/src/trpc/user-apis/slots.ts | 183 +++++--------- .../app/(drawer)/(tabs)/home/index.tsx | 1 - 14 files changed, 530 insertions(+), 369 deletions(-) create mode 100644 apps/backend/src/stores/banner-store.ts rename apps/backend/src/{lib => stores}/product-store.ts (96%) create mode 100644 apps/backend/src/stores/product-tag-store.ts create mode 100644 apps/backend/src/stores/slot-store.ts create mode 100644 apps/backend/src/stores/store-initializer.ts diff --git a/apps/backend/src/lib/init.ts b/apps/backend/src/lib/init.ts index 0cf5f43..f69fbca 100755 --- a/apps/backend/src/lib/init.ts +++ b/apps/backend/src/lib/init.ts @@ -1,7 +1,5 @@ -import roleManager from './roles-manager'; import './notif-job'; -import { computeConstants } from './const-store'; -import { initializeProducts } from './product-store'; +import { initializeAllStores } from '../stores/store-initializer'; /** * Initialize all application services @@ -13,22 +11,13 @@ import { initializeProducts } from './product-store'; export const initFunc = async (): Promise => { try { console.log('Starting application initialization...'); - - // Initialize role manager - await roleManager.fetchRoles(); - console.log('Role manager initialized successfully'); - // Compute and store constants in Redis - await computeConstants(); - console.log('Const store initialized successfully'); - - // Initialize product store in Redis - await initializeProducts(); - console.log('Product store initialized successfully'); + // Initialize all stores + await initializeAllStores(); // Notification queue and worker are initialized via import console.log('Notification queue and worker initialized'); - + console.log('Application initialization completed successfully'); } catch (error) { console.error('Application initialization failed:', error); diff --git a/apps/backend/src/stores/banner-store.ts b/apps/backend/src/stores/banner-store.ts new file mode 100644 index 0000000..a8e5f0a --- /dev/null +++ b/apps/backend/src/stores/banner-store.ts @@ -0,0 +1,88 @@ +// import redisClient from './redis-client'; +import redisClient from 'src/lib/redis-client'; +import { db } from '../db/db_index'; +import { homeBanners } from '../db/schema'; +import { isNotNull, asc } from 'drizzle-orm'; +import { generateSignedUrlFromS3Url } from 'src/lib/s3-client'; + +// Banner Type (matches getBanners return) +interface Banner { + id: number; + name: string; + imageUrl: string | null; + serialNum: number | null; + productIds: number[] | null; + createdAt: Date; + // updatedAt: Date; +} + +export async function initializeBannerStore(): Promise { + try { + console.log('Initializing banner store in Redis...'); + + const banners = await db.query.homeBanners.findMany({ + where: isNotNull(homeBanners.serialNum), // Only show assigned banners + orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 + }); + + // Store each banner in Redis + for (const banner of banners) { + const signedImageUrl = banner.imageUrl ? await generateSignedUrlFromS3Url(banner.imageUrl) : banner.imageUrl; + + const bannerObj: Banner = { + id: banner.id, + name: banner.name, + imageUrl: signedImageUrl, + serialNum: banner.serialNum, + productIds: banner.productIds, + createdAt: banner.createdAt, + // updatedAt: banner.updatedAt, + }; + + await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj)); + } + + console.log('Banner store initialized successfully'); + } catch (error) { + console.error('Error initializing banner store:', error); + } +} + +export async function getBannerById(id: number): Promise { + try { + const key = `banner:${id}`; + const data = await redisClient.get(key); + if (!data) return null; + return JSON.parse(data) as Banner; + } catch (error) { + console.error(`Error getting banner ${id}:`, error); + return null; + } +} + +export async function getAllBanners(): Promise { + try { + // Get all keys matching the pattern "banner:*" + const keys = await redisClient.KEYS('banner:*'); + + if (keys.length === 0) return []; + + // Get all banners using MGET for better performance + const bannersData = await redisClient.MGET(keys); + + const banners: Banner[] = []; + for (const bannerData of bannersData) { + if (bannerData) { + banners.push(JSON.parse(bannerData) as Banner); + } + } + + // Sort by serialNum to maintain the same order as the original query + banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0)); + + return banners; + } catch (error) { + console.error('Error getting all banners:', error); + return []; + } +} \ No newline at end of file diff --git a/apps/backend/src/lib/product-store.ts b/apps/backend/src/stores/product-store.ts similarity index 96% rename from apps/backend/src/lib/product-store.ts rename to apps/backend/src/stores/product-store.ts index cf60716..a02848b 100644 --- a/apps/backend/src/lib/product-store.ts +++ b/apps/backend/src/stores/product-store.ts @@ -1,8 +1,9 @@ -import redisClient from './redis-client'; +// import redisClient from './redis-client'; +import redisClient from 'src/lib/redis-client'; import { db } from '../db/db_index'; import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo } from '../db/schema'; -import { generateSignedUrlsFromS3Urls } from './s3-client'; import { eq, and, gt, sql } from 'drizzle-orm'; +import { generateSignedUrlsFromS3Urls } from 'src/lib/s3-client'; // Uniform Product Type (matches getProductDetails return) interface Product { @@ -145,7 +146,9 @@ export async function getAllProducts(): Promise { // Get all keys matching the pattern "product:*" const keys = await redisClient.KEYS('product:*'); - if (keys.length === 0) return []; + if (keys.length === 0) { + return []; + } // Get all products using MGET for better performance const productsData = await redisClient.MGET(keys); diff --git a/apps/backend/src/stores/product-tag-store.ts b/apps/backend/src/stores/product-tag-store.ts new file mode 100644 index 0000000..74750cc --- /dev/null +++ b/apps/backend/src/stores/product-tag-store.ts @@ -0,0 +1,115 @@ +// import redisClient from './redis-client'; +import redisClient from 'src/lib/redis-client'; +import { db } from '../db/db_index'; +import { productTagInfo } from '../db/schema'; +import { eq } from 'drizzle-orm'; +import { generateSignedUrlFromS3Url } from 'src/lib/s3-client'; + +// Tag Type (matches getDashboardTags return) +interface Tag { + id: number; + tagName: string; + imageUrl: string | null; + isDashboardTag: boolean; +} + +export async function initializeProductTagStore(): Promise { + try { + console.log('Initializing product tag store in Redis...'); + + // Fetch all tags + const tagsData = await db + .select({ + id: productTagInfo.id, + tagName: productTagInfo.tagName, + imageUrl: productTagInfo.imageUrl, + isDashboardTag: productTagInfo.isDashboardTag, + }) + .from(productTagInfo); + + // Store each tag in Redis + for (const tag of tagsData) { + const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null; + + const tagObj: Tag = { + id: tag.id, + tagName: tag.tagName, + imageUrl: signedImageUrl, + isDashboardTag: tag.isDashboardTag, + }; + + await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj)); + } + + console.log('Product tag store initialized successfully'); + } catch (error) { + console.error('Error initializing product tag store:', error); + } +} + +export async function getTagById(id: number): Promise { + try { + const key = `tag:${id}`; + const data = await redisClient.get(key); + if (!data) return null; + return JSON.parse(data) as Tag; + } catch (error) { + console.error(`Error getting tag ${id}:`, error); + return null; + } +} + +export async function getAllTags(): Promise { + try { + // Get all keys matching the pattern "tag:*" + const keys = await redisClient.KEYS('tag:*'); + + if (keys.length === 0) { + return []; + } + + // Get all tags using MGET for better performance + const tagsData = await redisClient.MGET(keys); + + const tags: Tag[] = []; + for (const tagData of tagsData) { + if (tagData) { + tags.push(JSON.parse(tagData) as Tag); + } + } + + return tags; + } catch (error) { + console.error('Error getting all tags:', error); + return []; + } +} + +export async function getDashboardTags(): Promise { + try { + // Get all keys matching the pattern "tag:*" + const keys = await redisClient.KEYS('tag:*'); + + if (keys.length === 0) { + return []; + } + + // Get all tags using MGET for better performance + const tagsData = await redisClient.MGET(keys); + + const dashboardTags: Tag[] = []; + for (const tagData of tagsData) { + if (tagData) { + const tag = JSON.parse(tagData) as Tag; + if (tag.isDashboardTag) { + dashboardTags.push(tag); + } + } + } + + return dashboardTags; + } catch (error) { + console.error('Error getting dashboard tags:', error); + return []; + } +} \ No newline at end of file diff --git a/apps/backend/src/stores/slot-store.ts b/apps/backend/src/stores/slot-store.ts new file mode 100644 index 0000000..eed9c7e --- /dev/null +++ b/apps/backend/src/stores/slot-store.ts @@ -0,0 +1,125 @@ +import redisClient from 'src/lib/redis-client'; +import { db } from '../db/db_index'; +import { deliverySlotInfo, productSlots, productInfo, units } from '../db/schema'; +import { eq, and, gt, asc } from 'drizzle-orm'; +import { generateSignedUrlsFromS3Urls } from 'src/lib/s3-client'; + +// Define the structure for slot with products +interface SlotWithProducts { + id: number; + deliveryTime: Date; + freezeTime: Date; + isActive: boolean; + products: Array<{ + id: number; + name: string; + shortDescription: string | null; + price: string; + marketPrice: string | null; + unit: string | null; + images: string[]; + isOutOfStock: boolean; + storeId: number | null; + nextDeliveryDate: Date; + }>; +} + +export async function initializeSlotStore(): Promise { + try { + console.log('Initializing slot store in Redis...'); + + const now = new Date(); + + // Fetch active delivery slots with future delivery times + const slots = await db.query.deliverySlotInfo.findMany({ + where: and( + eq(deliverySlotInfo.isActive, true), + gt(deliverySlotInfo.deliveryTime, now), // Only future slots + ), + with: { + productSlots: { + with: { + product: { + with: { + unit: true, + store: true, + }, + }, + }, + }, + }, + orderBy: asc(deliverySlotInfo.deliveryTime), + }); + + // Transform data for storage + const slotsWithProducts = await Promise.all( + slots.map(async (slot) => ({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isActive: slot.isActive, + products: await Promise.all( + slot.productSlots.map(async (productSlot) => ({ + id: productSlot.product.id, + name: productSlot.product.name, + shortDescription: productSlot.product.shortDescription, + price: productSlot.product.price.toString(), + marketPrice: productSlot.product.marketPrice?.toString() || null, + unit: productSlot.product.unit?.shortNotation || null, + images: await generateSignedUrlsFromS3Urls( + (productSlot.product.images as string[]) || [], + ), + isOutOfStock: productSlot.product.isOutOfStock, + storeId: productSlot.product.storeId, + nextDeliveryDate: slot.deliveryTime, + })), + ), + })), + ); + + // Store each slot in Redis with key pattern "slot:{id}" + for (const slot of slotsWithProducts) { + await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot)); + } + + console.log('Slot store initialized successfully'); + } catch (error) { + console.error('Error initializing slot store:', error); + } +} + +export async function getSlotById(slotId: number): Promise { + try { + const key = `slot:${slotId}`; + const data = await redisClient.get(key); + if (!data) return null; + return JSON.parse(data) as SlotWithProducts; + } catch (error) { + console.error(`Error getting slot ${slotId}:`, error); + return null; + } +} + +export async function getAllSlots(): Promise { + try { + // Get all keys matching the pattern "slot:*" + const keys = await redisClient.KEYS('slot:*'); + + if (keys.length === 0) return []; + + // Get all slots using MGET for better performance + const slotsData = await redisClient.MGET(keys); + + const slots: SlotWithProducts[] = []; + for (const slotData of slotsData) { + if (slotData) { + slots.push(JSON.parse(slotData) as SlotWithProducts); + } + } + + return slots; + } catch (error) { + console.error('Error getting all slots:', error); + return []; + } +} \ No newline at end of file diff --git a/apps/backend/src/stores/store-initializer.ts b/apps/backend/src/stores/store-initializer.ts new file mode 100644 index 0000000..81052dc --- /dev/null +++ b/apps/backend/src/stores/store-initializer.ts @@ -0,0 +1,51 @@ +import roleManager from '../lib/roles-manager'; +import { computeConstants } from '../lib/const-store'; +import { initializeProducts } from './product-store'; +import { initializeProductTagStore } from './product-tag-store'; +import { initializeSlotStore } from './slot-store'; +import { initializeBannerStore } from './banner-store'; + +/** + * Initialize all application stores + * This function handles initialization of: + * - Role Manager (fetches and caches all roles) + * - Const Store (syncs constants from DB to Redis) + * - Product Store (caches all products in Redis) + * - Product Tag Store (caches all product tags in Redis) + * - Slot Store (caches all delivery slots with products in Redis) + * - Banner Store (caches all banners in Redis) + */ +export const initializeAllStores = async (): Promise => { + try { + console.log('Starting application stores initialization...'); + + // Initialize role manager + await roleManager.fetchRoles(); + console.log('Role manager initialized successfully'); + + // Compute and store constants in Redis + await computeConstants(); + console.log('Const store initialized successfully'); + + // Initialize product store in Redis + await initializeProducts(); + console.log('Product store initialized successfully'); + + // Initialize product tag store in Redis + await initializeProductTagStore(); + console.log('Product tag store initialized successfully'); + + // Initialize slot store in Redis + await initializeSlotStore(); + console.log('Slot store initialized successfully'); + + // Initialize banner store in Redis + await initializeBannerStore(); + console.log('Banner store initialized successfully'); + + console.log('All application stores initialized successfully'); + } catch (error) { + console.error('Application stores initialization failed:', error); + throw error; + } +}; \ No newline at end of file diff --git a/apps/backend/src/trpc/admin-apis/banner.ts b/apps/backend/src/trpc/admin-apis/banner.ts index f50880b..4850ebc 100644 --- a/apps/backend/src/trpc/admin-apis/banner.ts +++ b/apps/backend/src/trpc/admin-apis/banner.ts @@ -5,6 +5,7 @@ import { eq, and, desc, sql } from 'drizzle-orm'; import { protectedProcedure, router } from '../trpc-index'; import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { ApiError } from 'src/lib/api-error'; +import { initializeAllStores } from '../../stores/store-initializer'; export const bannerRouter = router({ // Get all banners @@ -102,6 +103,9 @@ export const bannerRouter = router({ isActive: false, // Default to inactive }).returning(); + // Reinitialize stores to reflect changes + await initializeAllStores(); + return banner; } catch (error) { console.error('Error creating banner:', error); @@ -145,6 +149,10 @@ export const bannerRouter = router({ .set({ ...finalData, lastUpdated: new Date(), }) .where(eq(homeBanners.id, id)) .returning(); + + // Reinitialize stores to reflect changes + await initializeAllStores(); + return banner; } catch (error) { console.error('Error updating banner:', error); @@ -157,6 +165,10 @@ export const bannerRouter = router({ .input(z.object({ id: z.number() })) .mutation(async ({ input }) => { await db.delete(homeBanners).where(eq(homeBanners.id, input.id)); + + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { success: true }; }), }); \ No newline at end of file diff --git a/apps/backend/src/trpc/admin-apis/product.ts b/apps/backend/src/trpc/admin-apis/product.ts index 8c3d0a6..93d87e0 100644 --- a/apps/backend/src/trpc/admin-apis/product.ts +++ b/apps/backend/src/trpc/admin-apis/product.ts @@ -7,6 +7,7 @@ import { ApiError } from '../../lib/api-error'; import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '../../lib/s3-client'; import { deleteS3Image } from '../../lib/delete-image'; import type { SpecialDeal } from '../../db/types'; +import { initializeAllStores } from '../../stores/store-initializer'; type CreateDeal = { quantity: number; @@ -100,6 +101,9 @@ export const productRouter = router({ throw new ApiError("Product not found", 404); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: "Product deleted successfully", }; @@ -128,6 +132,9 @@ export const productRouter = router({ .where(eq(productInfo.id, id)) .returning(); + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { product: updatedProduct, message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, @@ -181,6 +188,9 @@ export const productRouter = router({ await db.insert(productSlots).values(newAssociations); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: "Slot products updated successfully", added: productsToAdd.length, @@ -380,6 +390,9 @@ export const productRouter = router({ await db.insert(productGroupMembership).values(memberships); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { group: newGroup, message: 'Group created successfully', @@ -425,6 +438,9 @@ export const productRouter = router({ } } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { group: updatedGroup, message: 'Group updated successfully', @@ -451,6 +467,9 @@ export const productRouter = router({ throw new ApiError('Group not found', 404); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: 'Group deleted successfully', }; @@ -504,6 +523,9 @@ export const productRouter = router({ await Promise.all(updatePromises); + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: `Updated prices for ${updates.length} product(s)`, updatedCount: updates.length, diff --git a/apps/backend/src/trpc/admin-apis/slots.ts b/apps/backend/src/trpc/admin-apis/slots.ts index 671c4c9..5ed73d0 100644 --- a/apps/backend/src/trpc/admin-apis/slots.ts +++ b/apps/backend/src/trpc/admin-apis/slots.ts @@ -8,6 +8,7 @@ import { ApiError } from "../../lib/api-error"; import { appUrl } from "../../lib/env-exporter"; import redisClient from "../../lib/redis-client"; import { getSlotSequenceKey } from "../../lib/redisKeyGetters"; +import { initializeAllStores } from '../../stores/store-initializer'; interface CachedDeliverySequence { [userId: string]: number[]; @@ -211,6 +212,9 @@ export const slotsRouter = router({ await db.insert(productSlots).values(newAssociations); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: "Slot products updated successfully", added: productsToAdd.length, @@ -283,6 +287,9 @@ export const slotsRouter = router({ } } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { slot: newSlot, createdSnippets, @@ -425,6 +432,9 @@ export const slotsRouter = router({ } } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { slot: updatedSlot, createdSnippets, @@ -457,6 +467,9 @@ export const slotsRouter = router({ throw new ApiError("Slot not found", 404); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: "Slot deleted successfully", }; diff --git a/apps/backend/src/trpc/admin-apis/store.ts b/apps/backend/src/trpc/admin-apis/store.ts index 94f3021..4231ec4 100644 --- a/apps/backend/src/trpc/admin-apis/store.ts +++ b/apps/backend/src/trpc/admin-apis/store.ts @@ -6,6 +6,7 @@ import { eq, inArray } from 'drizzle-orm'; import { ApiError } from '../../lib/api-error'; import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { initializeAllStores } from '../../stores/store-initializer'; export const storeRouter = router({ getStores: protectedProcedure @@ -83,6 +84,9 @@ export const storeRouter = router({ .where(inArray(productInfo.id, products)); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { store: newStore, message: "Store created successfully", @@ -159,6 +163,9 @@ export const storeRouter = router({ } } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { store: updatedStore, message: "Store updated successfully", @@ -189,6 +196,9 @@ export const storeRouter = router({ throw new ApiError("Store not found", 404); } + // Reinitialize stores to reflect changes + await initializeAllStores(); + return { message: "Store deleted successfully", }; diff --git a/apps/backend/src/trpc/common-apis/common.ts b/apps/backend/src/trpc/common-apis/common.ts index c43265b..c3010b2 100644 --- a/apps/backend/src/trpc/common-apis/common.ts +++ b/apps/backend/src/trpc/common-apis/common.ts @@ -4,7 +4,8 @@ import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productT import { eq, gt, and, sql, inArray } from 'drizzle-orm'; import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { z } from 'zod'; -import { getAllProducts as getAllProductsFromCache } from '../../lib/product-store'; +import { getAllProducts as getAllProductsFromCache } from '../../stores/product-store'; +import { getDashboardTags as getDashboardTagsFromCache } from '../../stores/product-tag-store'; export const getNextDeliveryDate = async (productId: number): Promise => { const result = await db @@ -32,22 +33,11 @@ export const getNextDeliveryDate = async (productId: number): Promise { - const tags = await db - .select() - .from(productTagInfo) - .where(eq(productTagInfo.isDashboardTag, true)) - .orderBy(productTagInfo.tagName); - - // Generate signed URLs for tag images - const tagsWithSignedUrls = await Promise.all( - tags.map(async (tag) => ({ - ...tag, - imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null, - })) - ); + // Get dashboard tags from cache + const tags = await getDashboardTagsFromCache(); return { - tags: tagsWithSignedUrls, + tags: tags, }; }), diff --git a/apps/backend/src/trpc/user-apis/product.ts b/apps/backend/src/trpc/user-apis/product.ts index 92b74e5..70478a1 100644 --- a/apps/backend/src/trpc/user-apis/product.ts +++ b/apps/backend/src/trpc/user-apis/product.ts @@ -5,7 +5,7 @@ import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, store import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, generateUploadUrl, claimUploadUrl, extractKeyFromPresignedUrl } from '../../lib/s3-client'; import { ApiError } from '../../lib/api-error'; import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm'; -import { getProductById as getProductByIdFromCache } from '../../lib/product-store'; +import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '../../stores/product-store'; // Uniform Product Type interface Product { @@ -28,27 +28,6 @@ interface Product { } export const productRouter = router({ - getDashboardTags: publicProcedure - .query(async () => { - const tags = await db - .select() - .from(productTagInfo) - .where(eq(productTagInfo.isDashboardTag, true)) - .orderBy(productTagInfo.tagName); - - // Generate signed URLs for tag images - const tagsWithSignedUrls = await Promise.all( - tags.map(async (tag) => ({ - ...tag, - imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null, - })) - ); - - return { - tags: tagsWithSignedUrls, - }; - }), - getProductDetails: publicProcedure .input(z.object({ id: z.string().regex(/^\d+$/, 'Invalid product ID'), @@ -61,9 +40,11 @@ export const productRouter = router({ throw new Error('Invalid product ID'); } + console.log('from the api to get product details') + // First, try to get the product from Redis cache const cachedProduct = await getProductByIdFromCache(productId); - + if (cachedProduct) { return cachedProduct; } @@ -255,202 +236,18 @@ export const productRouter = router({ getAllProductsSummary: publicProcedure .query(async (): Promise => { - // Get all product IDs first - const productIdsResult = await db - .select({ id: productInfo.id }) - .from(productInfo); + // Get all products from cache + const allCachedProducts = await getAllProductsFromCache(); - const productIds = productIdsResult.map(p => p.id); + // Transform the cached products to match the expected summary format + // (with empty deliverySlots and specialDeals arrays for summary view) + const transformedProducts = allCachedProducts.map(product => ({ + ...product, + deliverySlots: [], // Empty for summary view + specialDeals: [], // Empty for summary view + })); - // Try to get all products from cache first - const cachedProducts: Product[] = []; - const uncachedProductIds: number[] = []; - - console.log(uncachedProductIds) - - for (const id of productIds) { - const cachedProduct = await getProductByIdFromCache(id); - if (cachedProduct) { - cachedProducts.push(cachedProduct); - } else { - uncachedProductIds.push(id); - } - } - - // If there are uncached products, fetch them from DB - let uncachedProductsFromDb: Product[] = []; - if (uncachedProductIds.length > 0) { - const products = await db - .select({ - id: productInfo.id, - name: productInfo.name, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - storeId: productInfo.storeId, - unitShortNotation: units.shortNotation, - incrementStep: productInfo.incrementStep, - isFlashAvailable: productInfo.isFlashAvailable, - flashPrice: productInfo.flashPrice, - productQuantity: productInfo.productQuantity, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where(inArray(productInfo.id, uncachedProductIds)); - - // Fetch all stores - const allStores = await db.query.storeInfo.findMany({ - columns: { id: true, name: true, description: true }, - }); - const storeMap = new Map(allStores.map(s => [s.id, s])); - - // Generate signed URLs for images and build uniform structure - uncachedProductsFromDb = await Promise.all( - products.map(async (product) => { - const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []); - const store = product.storeId ? storeMap.get(product.storeId) || null : null; - - return { - id: product.id, - name: product.name, - shortDescription: null, - longDescription: null, - price: product.price.toString(), - marketPrice: product.marketPrice?.toString() || null, - unitNotation: product.unitShortNotation, - images: signedImages, - isOutOfStock: product.isOutOfStock, - store: store ? { - id: store.id, - name: store.name, - description: store.description, - } : null, - incrementStep: product.incrementStep, - productQuantity: product.productQuantity, - isFlashAvailable: product.isFlashAvailable, - flashPrice: product.flashPrice?.toString() || null, - deliverySlots: [], - specialDeals: [], - }; - }) - ); - } - - // Combine cached and uncached products - return [...cachedProducts, ...uncachedProductsFromDb]; + return transformedProducts; }), - productMega: publicProcedure - .query(async (): Promise => { - // Fetch all products with unit info - const productsData = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - longDescription: productInfo.longDescription, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - storeId: productInfo.storeId, - unitShortNotation: units.shortNotation, - incrementStep: productInfo.incrementStep, - productQuantity: productInfo.productQuantity, - isFlashAvailable: productInfo.isFlashAvailable, - flashPrice: productInfo.flashPrice, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)); - - // Fetch all stores - const allStores = await db.query.storeInfo.findMany({ - columns: { id: true, name: true, description: true }, - }); - const storeMap = new Map(allStores.map(s => [s.id, s])); - - // Fetch all delivery slots for all products - const allDeliverySlots = await db - .select({ - productId: productSlots.productId, - id: deliverySlotInfo.id, - deliveryTime: deliverySlotInfo.deliveryTime, - freezeTime: deliverySlotInfo.freezeTime, - }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where( - and( - eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, sql`NOW()`) - ) - ) - .orderBy(deliverySlotInfo.deliveryTime); - - // Group by productId - const deliverySlotsMap = new Map(); - for (const slot of allDeliverySlots) { - if (!deliverySlotsMap.has(slot.productId)) { - deliverySlotsMap.set(slot.productId, []); - } - deliverySlotsMap.get(slot.productId)!.push(slot); - } - - // Fetch all special deals for all products - const allSpecialDeals = await db - .select({ - productId: specialDeals.productId, - quantity: specialDeals.quantity, - price: specialDeals.price, - validTill: specialDeals.validTill, - }) - .from(specialDeals) - .where(gt(specialDeals.validTill, sql`NOW()`)) - .orderBy(specialDeals.quantity); - - // Group by productId - const specialDealsMap = new Map(); - for (const deal of allSpecialDeals) { - if (!specialDealsMap.has(deal.productId)) { - specialDealsMap.set(deal.productId, []); - } - specialDealsMap.get(deal.productId)!.push(deal); - } - - // Build the response - const response: Product[] = await Promise.all( - productsData.map(async (product) => { - const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []); - const store = product.storeId ? storeMap.get(product.storeId) || null : null; - const deliverySlots = deliverySlotsMap.get(product.id) || []; - const specialDeals = specialDealsMap.get(product.id) || []; - - return { - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - longDescription: product.longDescription, - price: product.price.toString(), - marketPrice: product.marketPrice?.toString() || null, - unitNotation: product.unitShortNotation, - images: signedImages, - isOutOfStock: product.isOutOfStock, - store: store ? { - id: store.id, - name: store.name, - description: store.description, - } : null, - incrementStep: product.incrementStep, - productQuantity: product.productQuantity, - isFlashAvailable: product.isFlashAvailable, - flashPrice: product.flashPrice?.toString() || null, - deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime })), - specialDeals: specialDeals.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })), - }; - }) - ); - - return response; - }), }); \ No newline at end of file diff --git a/apps/backend/src/trpc/user-apis/slots.ts b/apps/backend/src/trpc/user-apis/slots.ts index c0511a8..484780a 100644 --- a/apps/backend/src/trpc/user-apis/slots.ts +++ b/apps/backend/src/trpc/user-apis/slots.ts @@ -1,49 +1,27 @@ -import { router, publicProcedure } from '../trpc-index'; -import { z } from 'zod'; -import { db } from '../../db/db_index'; -import { deliverySlotInfo, productSlots, productInfo, units } from '../../db/schema'; -import { eq, and, gt, asc } from 'drizzle-orm'; -import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client'; +import { router, publicProcedure } from "../trpc-index"; +import { z } from "zod"; +import { db } from "../../db/db_index"; +import { + deliverySlotInfo, + productSlots, + productInfo, + units, +} from "../../db/schema"; +import { eq, and, gt, asc } from "drizzle-orm"; +import { generateSignedUrlsFromS3Urls } from "../../lib/s3-client"; +import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "../../stores/slot-store"; // Helper method to get formatted slot data by ID async function getSlotData(slotId: number) { - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, slotId), - with: { - productSlots: { - with: { - product: { - with: { - unit: true, - store: true, - }, - }, - }, - }, - }, - }); + // Get slot from cache + const slot = await getSlotByIdFromCache(slotId); if (!slot) { return null; // Slot not found } // Filter out out-of-stock products and format to match home page display structure - const products = await Promise.all( - slot.productSlots - .filter(productSlot => !productSlot.product.isOutOfStock) - .map(async (productSlot) => ({ - id: productSlot.product.id, - name: productSlot.product.name, - price: productSlot.product.price, - unit: productSlot.product.unit?.shortNotation || 'unit', - images: await generateSignedUrlsFromS3Urls( - (productSlot.product.images as string[]) || [] - ), - isOutOfStock: productSlot.product.isOutOfStock, - storeId: productSlot.product.storeId, - nextDeliveryDate: slot.deliveryTime, // For home page compatibility - })) - ); + const products = slot.products.filter((product) => !product.isOutOfStock); return { deliveryTime: slot.deliveryTime, @@ -54,95 +32,64 @@ async function getSlotData(slotId: number) { } export const slotsRouter = router({ - getSlots: publicProcedure - .query(async () => { - const slots = await db.query.deliverySlotInfo.findMany({ - where: eq(deliverySlotInfo.isActive, true), - }); - return { - slots, - count: slots.length, - }; - }), + getSlots: publicProcedure.query(async () => { + const slots = await db.query.deliverySlotInfo.findMany({ + where: eq(deliverySlotInfo.isActive, true), + }); + return { + slots, + count: slots.length, + }; + }), - getSlotsWithProducts: publicProcedure - .query(async () => { - const now = new Date(); + getSlotsWithProducts: publicProcedure.query(async () => { + // Get all slots from cache + const slotsWithProducts = await getAllSlotsFromCache(); - // Fetch active delivery slots with future delivery times - const slots = await db.query.deliverySlotInfo.findMany({ - where: and( - eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, now) // Only future slots - ), - with: { - productSlots: { - with: { - product: { - with: { - unit: true, - }, - }, - }, - }, - }, - orderBy: asc(deliverySlotInfo.deliveryTime), - }); + return { + slots: slotsWithProducts, + count: slotsWithProducts.length, + }; + }), - // Transform data for frontend - const slotsWithProducts = await Promise.all( - slots.map(async (slot) => ({ - id: slot.id, - deliveryTime: slot.deliveryTime, - freezeTime: slot.freezeTime, - isActive: slot.isActive, - products: await Promise.all( - slot.productSlots.map(async (productSlot) => ({ - id: productSlot.product.id, - name: productSlot.product.name, - shortDescription: productSlot.product.shortDescription, - price: productSlot.product.price, - marketPrice: productSlot.product.marketPrice, - unit: productSlot.product.unit?.shortNotation, - images: await generateSignedUrlsFromS3Urls( - (productSlot.product.images as string[]) || [] - ), - isOutOfStock: productSlot.product.isOutOfStock, - })) - ), - })) - ); + nextMajorDelivery: publicProcedure.query(async () => { + const now = new Date(); - return { - slots: slotsWithProducts, - count: slotsWithProducts.length, - }; - }), + // Find the next upcoming active delivery slot ID + const nextSlot = await db.query.deliverySlotInfo.findFirst({ + where: and( + eq(deliverySlotInfo.isActive, true), + gt(deliverySlotInfo.deliveryTime, now), + ), + orderBy: asc(deliverySlotInfo.deliveryTime), + }); - nextMajorDelivery: publicProcedure - .query(async () => { - const now = new Date(); + if (!nextSlot) { + return null; // No upcoming delivery slots + } - // Find the next upcoming active delivery slot ID - const nextSlot = await db.query.deliverySlotInfo.findFirst({ - where: and( - eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, now) - ), - orderBy: asc(deliverySlotInfo.deliveryTime), - }); - - if (!nextSlot) { - return null; // No upcoming delivery slots - } - - // Get formatted data using helper method - return await getSlotData(nextSlot.id); - }), + // Get formatted data using helper method + return await getSlotData(nextSlot.id); + }), getSlotById: publicProcedure .input(z.object({ slotId: z.number() })) .query(async ({ input }) => { - return await getSlotData(input.slotId); + // Get slot from cache + const slot = await getSlotByIdFromCache(input.slotId); + + if (!slot) { + return null; // Slot not found + } + + // Filter out out-of-stock products and format to match home page display structure + const products = slot.products.filter((product) => !product.isOutOfStock); + + return { + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + slotId: slot.id, + products, + }; }), -}); \ No newline at end of file +}); diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx index 168761d..1c1d073 100755 --- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx @@ -111,7 +111,6 @@ export default function Dashboard() { const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError } = useGetEssentialConsts(); const { data: tagsData } = trpc.common.product.getDashboardTags.useQuery(); - const { data: cartData, refetch: refetchCart } = useGetCart(); const { data: storesData } = trpc.user.stores.getStores.useQuery(); const { data: defaultAddressResponse } = trpc.user.address.getDefaultAddress.useQuery();