Compare commits

..

2 commits

Author SHA1 Message Date
shafi54
7432f8dfd5 enh 2026-03-27 18:47:12 +05:30
shafi54
18f36107d8 enh 2026-03-27 01:59:26 +05:30
67 changed files with 401064 additions and 1236 deletions

View file

@ -1,7 +1,6 @@
# Agent Instructions for Meat Farmer Monorepo # Agent Instructions for Meat Farmer Monorepo
## Important instructions ## Important instructions
- Don't try to build the code or run or compile it. Just make changes and leave the rest for the user.
- Don't run any drizzle migrations. User will handle it. - Don't run any drizzle migrations. User will handle it.
## Code Style Guidelines ## Code Style Guidelines
@ -48,6 +47,4 @@ react-native. They are available in the common-ui as MyText, MyTextInput, MyTouc
- Database: Drizzle ORM with PostgreSQL - Database: Drizzle ORM with PostgreSQL
## Important Notes ## Important Notes
- **Do not run build, compile, or migration commands** - These should be handled manually by developers
- Avoid running `npm run build`, `tsc`, `drizzle-kit generate`, or similar compilation/migration commands
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user - Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user

View file

@ -0,0 +1,11 @@
import worker, * as OTHER_EXPORTS from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/worker.ts";
import * as __MIDDLEWARE_0__ from "/Users/mohammedshafiuddin/WebDev/freshyo/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts";
import * as __MIDDLEWARE_1__ from "/Users/mohammedshafiuddin/WebDev/freshyo/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts";
export * from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/worker.ts";
export const __INTERNAL_WRANGLER_MIDDLEWARE__ = [
__MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default
]
export default worker;

View file

@ -0,0 +1,134 @@
// This loads all middlewares exposed on the middleware object and then starts
// the invocation chain. The big idea is that we can add these to the middleware
// export dynamically through wrangler, or we can potentially let users directly
// add them as a sort of "plugin" system.
import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-aek2Ls/middleware-insertion-facade.js";
import { __facade_invoke__, __facade_register__, Dispatcher } from "/Users/mohammedshafiuddin/WebDev/freshyo/node_modules/wrangler/templates/middleware/common.ts";
import type { WorkerEntrypointConstructor } from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-aek2Ls/middleware-insertion-facade.js";
// Preserve all the exports from the worker
export * from "/Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/.wrangler/tmp/bundle-aek2Ls/middleware-insertion-facade.js";
class __Facade_ScheduledController__ implements ScheduledController {
readonly #noRetry: ScheduledController["noRetry"];
constructor(
readonly scheduledTime: number,
readonly cron: string,
noRetry: ScheduledController["noRetry"]
) {
this.#noRetry = noRetry;
}
noRetry() {
if (!(this instanceof __Facade_ScheduledController__)) {
throw new TypeError("Illegal invocation");
}
// Need to call native method immediately in case uncaught error thrown
this.#noRetry();
}
}
function wrapExportedHandler(worker: ExportedHandler): ExportedHandler {
// If we don't have any middleware defined, just return the handler as is
if (
__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
) {
return worker;
}
// Otherwise, register all middleware once
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
__facade_register__(middleware);
}
const fetchDispatcher: ExportedHandlerFetchHandler = function (
request,
env,
ctx
) {
if (worker.fetch === undefined) {
throw new Error("Handler does not export a fetch() function.");
}
return worker.fetch(request, env, ctx);
};
return {
...worker,
fetch(request, env, ctx) {
const dispatcher: Dispatcher = function (type, init) {
if (type === "scheduled" && worker.scheduled !== undefined) {
const controller = new __Facade_ScheduledController__(
Date.now(),
init.cron ?? "",
() => {}
);
return worker.scheduled(controller, env, ctx);
}
};
return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);
},
};
}
function wrapWorkerEntrypoint(
klass: WorkerEntrypointConstructor
): WorkerEntrypointConstructor {
// If we don't have any middleware defined, just return the handler as is
if (
__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
) {
return klass;
}
// Otherwise, register all middleware once
for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
__facade_register__(middleware);
}
// `extend`ing `klass` here so other RPC methods remain callable
return class extends klass {
#fetchDispatcher: ExportedHandlerFetchHandler<Record<string, unknown>> = (
request,
env,
ctx
) => {
this.env = env;
this.ctx = ctx;
if (super.fetch === undefined) {
throw new Error("Entrypoint class does not define a fetch() function.");
}
return super.fetch(request);
};
#dispatcher: Dispatcher = (type, init) => {
if (type === "scheduled" && super.scheduled !== undefined) {
const controller = new __Facade_ScheduledController__(
Date.now(),
init.cron ?? "",
() => {}
);
return super.scheduled(controller);
}
};
fetch(request: Request<unknown, IncomingRequestCfProperties>) {
return __facade_invoke__(
request,
this.env,
this.ctx,
this.#dispatcher,
this.#fetchDispatcher
);
}
};
}
let WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined;
if (typeof ENTRY === "object") {
WRAPPED_ENTRY = wrapExportedHandler(ENTRY);
} else if (typeof ENTRY === "function") {
WRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY);
}
export default WRAPPED_ENTRY;

View file

@ -0,0 +1,13 @@
function stripCfConnectingIPHeader(input, init) {
const request = new Request(input, init);
request.headers.delete("CF-Connecting-IP");
return request;
}
globalThis.fetch = new Proxy(globalThis.fetch, {
apply(target, thisArg, argArray) {
return Reflect.apply(target, thisArg, [
stripCfConnectingIPHeader.apply(null, argArray),
]);
},
});

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

48
apps/backend/drop_all.sql Normal file
View file

@ -0,0 +1,48 @@
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
DROP TABLE IF EXISTS "vendor_snippets";
DROP TABLE IF EXISTS "users";
DROP TABLE IF EXISTS "user_notifications";
DROP TABLE IF EXISTS "user_incidents";
DROP TABLE IF EXISTS "user_details";
DROP TABLE IF EXISTS "user_creds";
DROP TABLE IF EXISTS "upload_url_status";
DROP TABLE IF EXISTS "unlogged_user_tokens";
DROP TABLE IF EXISTS "units";
DROP TABLE IF EXISTS "store_info";
DROP TABLE IF EXISTS "staff_users";
DROP TABLE IF EXISTS "staff_roles";
DROP TABLE IF EXISTS "staff_role_permissions";
DROP TABLE IF EXISTS "staff_permissions";
DROP TABLE IF EXISTS "special_deals";
DROP TABLE IF EXISTS "reserved_coupons";
DROP TABLE IF EXISTS "refunds";
DROP TABLE IF EXISTS "product_tags";
DROP TABLE IF EXISTS "product_tag_info";
DROP TABLE IF EXISTS "product_slots";
DROP TABLE IF EXISTS "product_reviews";
DROP TABLE IF EXISTS "product_info";
DROP TABLE IF EXISTS "product_group_membership";
DROP TABLE IF EXISTS "product_group_info";
DROP TABLE IF EXISTS "product_categories";
DROP TABLE IF EXISTS "product_availability_schedules";
DROP TABLE IF EXISTS "payments";
DROP TABLE IF EXISTS "payment_info";
DROP TABLE IF EXISTS "orders";
DROP TABLE IF EXISTS "order_status";
DROP TABLE IF EXISTS "order_items";
DROP TABLE IF EXISTS "notifications";
DROP TABLE IF EXISTS "notif_creds";
DROP TABLE IF EXISTS "key_val_store";
DROP TABLE IF EXISTS "home_banners";
DROP TABLE IF EXISTS "delivery_slot_info";
DROP TABLE IF EXISTS "coupons";
DROP TABLE IF EXISTS "coupon_usage";
DROP TABLE IF EXISTS "coupon_applicable_users";
DROP TABLE IF EXISTS "coupon_applicable_products";
DROP TABLE IF EXISTS "complaints";
DROP TABLE IF EXISTS "cart_items";
DROP TABLE IF EXISTS "addresses";
DROP TABLE IF EXISTS "address_zones";
DROP TABLE IF EXISTS "address_areas";
COMMIT;

View file

@ -1,17 +1,8 @@
import 'dotenv/config'; import 'dotenv/config';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { trpcServer } from '@hono/trpc-server';
import { getStaffUserById, isUserSuspended } from '@/src/dbService';
import mainRouter from '@/src/main-router';
import initFunc from '@/src/lib/init'; import initFunc from '@/src/lib/init';
import { appRouter } from '@/src/trpc/router'; import { createApp } from '@/src/app'
import { TRPCError } from '@trpc/server'; // import signedUrlCache from '@/src/lib/signed-url-cache';
import { jwtVerify } from 'jose'
import { encodedJwtSecret } from '@/src/lib/env-exporter';
import signedUrlCache from '@/src/lib/signed-url-cache';
import { seed } from '@/src/lib/seed'; import { seed } from '@/src/lib/seed';
import '@/src/jobs/jobs-index'; import '@/src/jobs/jobs-index';
import { startAutomatedJobs } from '@/src/lib/automatedJobs'; import { startAutomatedJobs } from '@/src/lib/automatedJobs';
@ -20,120 +11,9 @@ seed()
initFunc() initFunc()
startAutomatedJobs() startAutomatedJobs()
const app = new Hono(); // signedUrlCache.loadFromDisk(); // Disabled for Workers compatibility
// CORS middleware const app = createApp()
app.use(cors({
origin: 'http://localhost:5174',
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization'],
credentials: true,
}));
signedUrlCache.loadFromDisk();
// Logger middleware
app.use(logger());
// tRPC middleware
app.use('/api/trpc', trpcServer({
router: appRouter,
createContext: async ({ req }) => {
let user = null;
let staffUser = null;
const authHeader = req.headers.get('authorization');
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
const { payload } = await jwtVerify(token, encodedJwtSecret);
const decoded = payload as any;
// Check if this is a staff token (has staffId)
if (decoded.staffId) {
// This is a staff token, verify staff exists
const staff = await getStaffUserById(decoded.staffId);
if (staff) {
user = staffUser
staffUser = {
id: staff.id,
name: staff.name,
};
}
} else {
// This is a regular user token
user = decoded;
// Check if user is suspended
const suspended = await isUserSuspended(user.userId);
if (suspended) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Account suspended',
});
}
}
} catch (err) {
// Invalid token, both user and staffUser remain null
}
}
return { req, user, staffUser };
},
onError({ error, path, type, ctx }) {
console.error('🚨 tRPC Error :', {
path,
type,
code: error.code,
message: error.message,
userId: ctx?.user?.userId,
stack: error.stack,
});
},
}));
// Mount main router
app.route('/api', mainRouter);
// Global error handler
app.onError((err, c) => {
console.error(err);
// Handle different error types
let status = 500;
let message = 'Internal Server Error';
if (err instanceof TRPCError) {
// Map TRPC error codes to HTTP status codes
const trpcStatusMap: Record<string, number> = {
'BAD_REQUEST': 400,
'UNAUTHORIZED': 401,
'FORBIDDEN': 403,
'NOT_FOUND': 404,
'TIMEOUT': 408,
'CONFLICT': 409,
'PRECONDITION_FAILED': 412,
'PAYLOAD_TOO_LARGE': 413,
'METHOD_NOT_SUPPORTED': 405,
'UNPROCESSABLE_CONTENT': 422,
'TOO_MANY_REQUESTS': 429,
'INTERNAL_SERVER_ERROR': 500,
};
status = trpcStatusMap[err.code] || 500;
message = err.message;
} else if ((err as any).statusCode) {
status = (err as any).statusCode;
message = err.message;
} else if ((err as any).status) {
status = (err as any).status;
message = err.message;
} else if (err.message) {
message = err.message;
}
return c.json({ message }, status as any);
});
serve({ serve({
fetch: app.fetch, fetch: app.fetch,

21840
apps/backend/migrated.sql Normal file

File diff suppressed because it is too large Load diff

21840
apps/backend/migrated_nofk.sql Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,8 @@
"dev2": "tsx watch index.ts", "dev2": "tsx watch index.ts",
"dev_node": "tsx watch index.ts", "dev_node": "tsx watch index.ts",
"dev": "bun --watch index.ts", "dev": "bun --watch index.ts",
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
"wrangler:deploy": "wrangler deploy worker.ts --config wrangler.toml",
"docker:build": "cd .. && docker buildx build --platform linux/amd64 -t mohdshafiuddin54/health_petal:latest --progress=plain -f backend/Dockerfile .", "docker:build": "cd .. && docker buildx build --platform linux/amd64 -t mohdshafiuddin54/health_petal:latest --progress=plain -f backend/Dockerfile .",
"docker:push": "docker push mohdshafiuddin54/health_petal:latest" "docker:push": "docker push mohdshafiuddin54/health_petal:latest"
}, },
@ -36,11 +38,13 @@
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20260304.0",
"@types/node": "^24.5.2", "@types/node": "^24.5.2",
"rimraf": "^6.1.2", "rimraf": "^6.1.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsc-alias": "^1.8.16", "tsc-alias": "^1.8.16",
"tsx": "^4.20.5", "tsx": "^4.20.5",
"typescript": "^5.9.2" "typescript": "^5.9.2",
"wrangler": "^3.114.0"
} }
} }

View file

@ -1,236 +0,0 @@
import { Context } from 'hono';
import {
checkProductExistsByName,
checkUnitExists,
createProduct as createProductRecord,
createSpecialDealsForProduct,
getProductImagesById,
replaceProductTags,
updateProduct as updateProductRecord,
updateProductDeals,
} from '@/src/dbService'
import { ApiError } from "@/src/lib/api-error";
import type { AdminSpecialDeal } from '@packages/shared'
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
import { deleteS3Image } from "@/src/lib/delete-image";
import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
type CreateDeal = {
quantity: number;
price: number;
validTill: string;
};
/**
* Create a new product
*/
export const createProduct = async (c: Context) => {
const body = await c.req.parseBody({ all: true });
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals, tagIds } = body;
// Validate required fields
if (!name || !unitId || !storeId || !price) {
throw new ApiError("Name, unitId, storeId, and price are required", 400);
}
// Check for duplicate name
const existingProduct = await checkProductExistsByName((name as string).trim())
if (existingProduct) {
throw new ApiError("A product with this name already exists", 400);
}
// Check if unit exists
const unitExists = await checkUnitExists(parseInt(unitId as string))
if (!unitExists) {
throw new ApiError("Invalid unit ID", 400);
}
// Extract images from body
const images = body.images;
let uploadedImageUrls: string[] = [];
if (images) {
const imageFiles = Array.isArray(images) ? images : [images];
const imageUploadPromises = imageFiles.map((file, index) => {
if (file instanceof File) {
const key = `product-images/${Date.now()}-${index}`;
return imageUploadS3(Buffer.from(file.stream() as any), file.type, key);
}
return null;
}).filter(Boolean);
uploadedImageUrls = await Promise.all(imageUploadPromises as Promise<string>[]);
}
// Create product
const productData: any = {
name: name as string,
shortDescription: shortDescription as string | undefined,
longDescription: longDescription as string | undefined,
unitId: parseInt(unitId as string),
storeId: parseInt(storeId as string),
price: parseFloat(price as string),
marketPrice: marketPrice ? parseFloat(marketPrice as string) : null,
incrementStep: incrementStep ? parseInt(incrementStep as string) : 1,
productQuantity: productQuantity ? parseInt(productQuantity as string) : 1,
isSuspended: isSuspended === 'true',
isFlashAvailable: isFlashAvailable === 'true',
images: uploadedImageUrls,
};
if (flashPrice) {
productData.flashPrice = parseFloat(flashPrice as string);
}
const newProduct = await createProductRecord(productData)
// Handle deals if provided
let createdDeals: AdminSpecialDeal[] = []
if (deals) {
const parsedDeals = typeof deals === 'string' ? JSON.parse(deals) : deals;
if (Array.isArray(parsedDeals)) {
createdDeals = await createSpecialDealsForProduct(newProduct.id, parsedDeals)
}
}
// Handle tag assignments if provided
if (tagIds) {
const parsedTagIds = typeof tagIds === 'string' ? JSON.parse(tagIds) : tagIds;
if (Array.isArray(parsedTagIds)) {
await replaceProductTags(newProduct.id, parsedTagIds)
}
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
// Send response first
return c.json({
product: newProduct,
deals: createdDeals,
message: "Product created successfully",
}, 201);
};
/**
* Update a product
*/
export const updateProduct = async (c: Context) => {
const id = c.req.param('id')
const body = await c.req.parseBody({ all: true });
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = body;
const deals = dealsRaw ? (typeof dealsRaw === 'string' ? JSON.parse(dealsRaw) : dealsRaw) : null;
const imagesToDelete = imagesToDeleteRaw ? (typeof imagesToDeleteRaw === 'string' ? JSON.parse(imagesToDeleteRaw) : imagesToDeleteRaw) : [];
if (!name || !unitId || !storeId || !price) {
throw new ApiError("Name, unitId, storeId, and price are required", 400);
}
// Check if unit exists
const unitExists = await checkUnitExists(parseInt(unitId as string))
if (!unitExists) {
throw new ApiError("Invalid unit ID", 400);
}
// Get current product to handle image updates
const currentImages = await getProductImagesById(parseInt(id as string))
if (!currentImages) {
throw new ApiError("Product not found", 404);
}
// Handle image deletions
let updatedImages = currentImages || []
if (imagesToDelete && imagesToDelete.length > 0) {
// Convert signed URLs to original S3 URLs for comparison
const originalUrlsToDelete = imagesToDelete
.map((signedUrl: string) => getOriginalUrlFromSignedUrl(signedUrl))
.filter(Boolean); // Remove nulls
// Find which stored images match the ones to delete
const imagesToRemoveFromDb = updatedImages.filter(storedUrl =>
originalUrlsToDelete.includes(storedUrl)
);
// Delete the matching images from S3
const deletePromises = imagesToRemoveFromDb.map(imageUrl => deleteS3Image(imageUrl));
await Promise.all(deletePromises);
// Remove deleted images from current images array
updatedImages = updatedImages.filter(img => !imagesToRemoveFromDb.includes(img));
}
// Extract new images from body
const images = body.images;
let uploadedImageUrls: string[] = [];
if (images) {
const imageFiles = Array.isArray(images) ? images : [images];
const imageUploadPromises = imageFiles.map((file, index) => {
if (file instanceof File) {
const key = `product-images/${Date.now()}-${index}`;
return imageUploadS3(Buffer.from(file.stream() as any), file.type, key);
}
return null;
}).filter(Boolean);
uploadedImageUrls = await Promise.all(imageUploadPromises as Promise<string>[]);
}
// Combine remaining current images with new uploaded images
const finalImages = [...updatedImages, ...uploadedImageUrls];
const updateData: any = {
name: name as string,
shortDescription: shortDescription as string | undefined,
longDescription: longDescription as string | undefined,
unitId: parseInt(unitId as string),
storeId: parseInt(storeId as string),
price: parseFloat(price as string),
marketPrice: marketPrice ? parseFloat(marketPrice as string) : null,
incrementStep: incrementStep ? parseInt(incrementStep as string) : 1,
productQuantity: productQuantity ? parseInt(productQuantity as string) : 1,
isSuspended: isSuspended === 'true',
images: finalImages.length > 0 ? finalImages : undefined,
};
if (isFlashAvailable !== undefined) {
updateData.isFlashAvailable = isFlashAvailable === 'true';
}
if (flashPrice !== undefined) {
updateData.flashPrice = flashPrice ? parseFloat(flashPrice as string) : null;
}
const updatedProduct = await updateProductRecord(parseInt(id as string), updateData)
if (!updatedProduct) {
throw new ApiError("Product not found", 404);
}
// Handle deals if provided
if (deals && Array.isArray(deals)) {
await updateProductDeals(parseInt(id as string), deals)
}
// Handle tag assignments if provided
if (tagIds) {
const parsedTagIds = typeof tagIds === 'string' ? [parseInt(tagIds)] : (Array.isArray(tagIds) ? tagIds.map((t: any) => parseInt(t)) : [parseInt(tagIds as any)])
await replaceProductTags(parseInt(id as string), parsedTagIds)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
// Send response first
return c.json({
product: updatedProduct,
message: "Product updated successfully",
}, 200);
};

126
apps/backend/src/app.ts Normal file
View file

@ -0,0 +1,126 @@
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { trpcServer } from '@hono/trpc-server'
import { getStaffUserById, isUserSuspended } from '@/src/dbService'
import mainRouter from '@/src/main-router'
import { appRouter } from '@/src/trpc/router'
import { TRPCError } from '@trpc/server'
import { jwtVerify } from 'jose'
import { getEncodedJwtSecret } from '@/src/lib/env-exporter'
export const createApp = () => {
const app = new Hono()
// CORS middleware
app.use(cors({
origin: 'http://localhost:5174',
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization'],
credentials: true,
}))
// Logger middleware
app.use(logger())
// tRPC middleware
app.use('/api/trpc/*', trpcServer({
router: appRouter,
createContext: async ({ req }) => {
let user = null
let staffUser = null
const authHeader = req.headers.get('authorization')
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7)
try {
const { payload } = await jwtVerify(token, getEncodedJwtSecret())
const decoded = payload as any
// Check if this is a staff token (has staffId)
if (decoded.staffId) {
// This is a staff token, verify staff exists
const staff = await getStaffUserById(decoded.staffId)
if (staff) {
user = staffUser
staffUser = {
id: staff.id,
name: staff.name,
}
}
} else {
// This is a regular user token
user = decoded
// Check if user is suspended
const suspended = await isUserSuspended(user.userId)
if (suspended) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Account suspended',
})
}
}
} catch (err) {
// Invalid token, both user and staffUser remain null
}
}
return { req, user, staffUser }
},
onError({ error, path, type, ctx }) {
console.error('🚨 tRPC Error :', {
path,
type,
code: error.code,
message: error.message,
userId: ctx?.user?.userId,
stack: error.stack,
})
},
}))
// Mount main router
app.route('/api', mainRouter)
// Global error handler
app.onError((err, c) => {
console.error(err)
// Handle different error types
let status = 500
let message = 'Internal Server Error'
if (err instanceof TRPCError) {
// Map TRPC error codes to HTTP status codes
const trpcStatusMap: Record<string, number> = {
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
TIMEOUT: 408,
CONFLICT: 409,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
METHOD_NOT_SUPPORTED: 405,
UNPROCESSABLE_CONTENT: 422,
TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
}
status = trpcStatusMap[err.code] || 500
message = err.message
} else if ((err as any).statusCode) {
status = (err as any).statusCode
message = err.message
} else if ((err as any).status) {
status = (err as any).status
message = err.message
} else if (err.message) {
message = err.message
}
return c.json({ message }, status as any)
})
return app
}

View file

@ -2,10 +2,15 @@
// This file re-exports everything from postgresImporter to provide a clean abstraction layer // This file re-exports everything from postgresImporter to provide a clean abstraction layer
import type { AdminOrderDetails } from '@packages/shared' import type { AdminOrderDetails } from '@packages/shared'
import { getOrderDetails } from '@/src/postgresImporter' // import { getOrderDetails } from '@/src/postgresImporter'
import { getOrderDetails, initDb } from '@/src/sqliteImporter'
// Re-export everything from postgresImporter // Re-export everything from postgresImporter
export * from '@/src/postgresImporter' // export * from '@/src/postgresImporter'
export * from '@/src/sqliteImporter'
export { initDb }
// Re-export getOrderDetails with the correct signature // Re-export getOrderDetails with the correct signature
export async function getOrderDetailsWrapper(orderId: number): Promise<AdminOrderDetails | null> { export async function getOrderDetailsWrapper(orderId: number): Promise<AdminOrderDetails | null> {

View file

@ -9,6 +9,6 @@ export class ApiError extends Error {
this.name = 'ApiError'; this.name = 'ApiError';
this.statusCode = statusCode; this.statusCode = statusCode;
this.details = details; this.details = details;
Error.captureStackTrace?.(this, ApiError); // Error.captureStackTrace?.(this, ApiError);
} }
} }

View file

@ -1,57 +1,122 @@
export const appUrl = process.env.APP_URL as string; // Old env loading (Node only)
// export const appUrl = process.env.APP_URL as string;
//
// export const jwtSecret: string = process.env.JWT_SECRET as string
//
// export const defaultRoleName = 'gen_user';
//
// export const encodedJwtSecret = new TextEncoder().encode(jwtSecret)
//
// export const s3AccessKeyId = process.env.S3_ACCESS_KEY_ID as string
//
// export const s3SecretAccessKey = process.env.S3_SECRET_ACCESS_KEY as string
//
// export const s3BucketName = process.env.S3_BUCKET_NAME as string
//
// export const s3Region = process.env.S3_REGION as string
//
// export const assetsDomain = process.env.ASSETS_DOMAIN as string;
//
// export const apiCacheKey = process.env.API_CACHE_KEY as string;
//
// export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string;
//
// export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string;
//
// export const s3Url = process.env.S3_URL as string
//
// export const redisUrl = process.env.REDIS_URL as string
//
//
// export const expoAccessToken = process.env.EXPO_ACCESS_TOKEN as string;
//
// export const phonePeBaseUrl = process.env.PHONE_PE_BASE_URL as string;
//
// export const phonePeClientId = process.env.PHONE_PE_CLIENT_ID as string;
//
// export const phonePeClientVersion = Number(process.env.PHONE_PE_CLIENT_VERSION as string);
//
// export const phonePeClientSecret = process.env.PHONE_PE_CLIENT_SECRET as string;
//
// export const phonePeMerchantId = process.env.PHONE_PE_MERCHANT_ID as string;
//
// export const razorpayId = process.env.RAZORPAY_KEY as string;
//
// export const razorpaySecret = process.env.RAZORPAY_SECRET as string;
//
// export const otpSenderAuthToken = process.env.OTP_SENDER_AUTH_TOKEN as string;
//
// export const minOrderValue = Number(process.env.MIN_ORDER_VALUE as string);
//
// export const deliveryCharge = Number(process.env.DELIVERY_CHARGE as string);
//
// export const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN as string;
//
// export const telegramChatIds = (process.env.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || [];
//
// export const isDevMode = (process.env.ENV_MODE as string) === 'dev';
export const jwtSecret: string = process.env.JWT_SECRET as string const getRuntimeEnv = () => (globalThis as any).ENV || (globalThis as any).process?.env || {}
const runtimeEnv = getRuntimeEnv()
export const appUrl = runtimeEnv.APP_URL as string
export const jwtSecret: string = runtimeEnv.JWT_SECRET as string
export const defaultRoleName = 'gen_user'; export const defaultRoleName = 'gen_user';
export const encodedJwtSecret = new TextEncoder().encode(jwtSecret) export const getEncodedJwtSecret = () => {
const env = getRuntimeEnv()
const secret = (env.JWT_SECRET as string) || ''
return new TextEncoder().encode(secret)
}
export const s3AccessKeyId = process.env.S3_ACCESS_KEY_ID as string export const s3AccessKeyId = runtimeEnv.S3_ACCESS_KEY_ID as string
export const s3SecretAccessKey = process.env.S3_SECRET_ACCESS_KEY as string export const s3SecretAccessKey = runtimeEnv.S3_SECRET_ACCESS_KEY as string
export const s3BucketName = process.env.S3_BUCKET_NAME as string export const s3BucketName = runtimeEnv.S3_BUCKET_NAME as string
export const s3Region = process.env.S3_REGION as string export const s3Region = runtimeEnv.S3_REGION as string
export const assetsDomain = process.env.ASSETS_DOMAIN as string; export const assetsDomain = runtimeEnv.ASSETS_DOMAIN as string
export const apiCacheKey = process.env.API_CACHE_KEY as string; export const apiCacheKey = runtimeEnv.API_CACHE_KEY as string
export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string; export const cloudflareApiToken = runtimeEnv.CLOUDFLARE_API_TOKEN as string
export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string; export const cloudflareZoneId = runtimeEnv.CLOUDFLARE_ZONE_ID as string
export const s3Url = process.env.S3_URL as string export const s3Url = runtimeEnv.S3_URL as string
export const redisUrl = process.env.REDIS_URL as string export const redisUrl = runtimeEnv.REDIS_URL as string
export const expoAccessToken = runtimeEnv.EXPO_ACCESS_TOKEN as string
export const expoAccessToken = process.env.EXPO_ACCESS_TOKEN as string; export const phonePeBaseUrl = runtimeEnv.PHONE_PE_BASE_URL as string
export const phonePeBaseUrl = process.env.PHONE_PE_BASE_URL as string; export const phonePeClientId = runtimeEnv.PHONE_PE_CLIENT_ID as string
export const phonePeClientId = process.env.PHONE_PE_CLIENT_ID as string; export const phonePeClientVersion = Number(runtimeEnv.PHONE_PE_CLIENT_VERSION as string)
export const phonePeClientVersion = Number(process.env.PHONE_PE_CLIENT_VERSION as string); export const phonePeClientSecret = runtimeEnv.PHONE_PE_CLIENT_SECRET as string
export const phonePeClientSecret = process.env.PHONE_PE_CLIENT_SECRET as string; export const phonePeMerchantId = runtimeEnv.PHONE_PE_MERCHANT_ID as string
export const phonePeMerchantId = process.env.PHONE_PE_MERCHANT_ID as string; export const razorpayId = runtimeEnv.RAZORPAY_KEY as string
export const razorpayId = process.env.RAZORPAY_KEY as string; export const razorpaySecret = runtimeEnv.RAZORPAY_SECRET as string
export const razorpaySecret = process.env.RAZORPAY_SECRET as string; export const otpSenderAuthToken = runtimeEnv.OTP_SENDER_AUTH_TOKEN as string
export const otpSenderAuthToken = process.env.OTP_SENDER_AUTH_TOKEN as string; export const minOrderValue = Number(runtimeEnv.MIN_ORDER_VALUE as string)
export const minOrderValue = Number(process.env.MIN_ORDER_VALUE as string); export const deliveryCharge = Number(runtimeEnv.DELIVERY_CHARGE as string)
export const deliveryCharge = Number(process.env.DELIVERY_CHARGE as string); export const telegramBotToken = runtimeEnv.TELEGRAM_BOT_TOKEN as string
export const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN as string; export const telegramChatIds = (runtimeEnv.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || []
export const telegramChatIds = (process.env.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || []; export const isDevMode = (runtimeEnv.ENV_MODE as string) === 'dev'
export const isDevMode = (process.env.ENV_MODE as string) === 'dev';

View file

@ -1,4 +1,4 @@
import { Queue, Worker } from 'bullmq'; // import { Queue, Worker } from 'bullmq';
import { Expo } from 'expo-server-sdk'; import { Expo } from 'expo-server-sdk';
import { redisUrl } from '@/src/lib/env-exporter' import { redisUrl } from '@/src/lib/env-exporter'
// import { db } from '@/src/db/db_index' // import { db } from '@/src/db/db_index'
@ -14,31 +14,35 @@ import {
REFUND_INITIATED_MESSAGE REFUND_INITIATED_MESSAGE
} from '@/src/lib/const-strings'; } from '@/src/lib/const-strings';
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
connection: { url: redisUrl },
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 10,
attempts: 3,
},
});
export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => { export const notificationQueue:any = {};
if (!job) return;
// export const notificationQueue = new Queue(NOTIFS_QUEUE, {
const { name, data } = job; // connection: { url: redisUrl },
console.log(`Processing notification job ${job.id} - ${name}`); // defaultJobOptions: {
// removeOnComplete: true,
if (name === 'send-admin-notification') { // removeOnFail: 10,
await sendAdminNotification(data); // attempts: 3,
} else if (name === 'send-notification') { // },
// Handle legacy notification type // });
console.log('Legacy notification job - not implemented yet');
} export const notificationWorker:any = {};
}, { // export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
connection: { url: redisUrl }, // if (!job) return;
concurrency: 5, //
}); // const { name, data } = job;
// console.log(`Processing notification job ${job.id} - ${name}`);
//
// if (name === 'send-admin-notification') {
// await sendAdminNotification(data);
// } else if (name === 'send-notification') {
// // Handle legacy notification type
// console.log('Legacy notification job - not implemented yet');
// }
// }, {
// connection: { url: redisUrl },
// concurrency: 5,
// });
async function sendAdminNotification(data: { async function sendAdminNotification(data: {
token: string; token: string;
@ -84,12 +88,12 @@ async function sendAdminNotification(data: {
} }
} }
notificationWorker.on('completed', (job) => { // notificationWorker.on('completed', (job) => {
if (job) console.log(`Notification job ${job.id} completed`); // if (job) console.log(`Notification job ${job.id} completed`);
}); // });
notificationWorker.on('failed', (job, err) => { // notificationWorker.on('failed', (job, err) => {
if (job) console.error(`Notification job ${job.id} failed:`, err); // if (job) console.error(`Notification job ${job.id} failed:`, err);
}); // });
export async function scheduleNotification(userId: number, payload: any, options?: { delay?: number; priority?: number }) { export async function scheduleNotification(userId: number, payload: any, options?: { delay?: number; priority?: number }) {
const jobData = { userId, ...payload }; const jobData = { userId, ...payload };
@ -159,8 +163,8 @@ export async function sendRefundInitiatedNotification(userId: number, orderId?:
orderId orderId
}); });
} }
//
process.on('SIGTERM', async () => { // process.on('SIGTERM', async () => {
await notificationQueue.close(); // await notificationQueue.close();
await notificationWorker.close(); // await notificationWorker.close();
}); // });

View file

@ -21,23 +21,23 @@ class RedisClient {
// console.error('Redis Client Error:', err); // console.error('Redis Client Error:', err);
// }); // });
// //
this.client.on('connect', () => { // this.client.on('connect', () => {
console.log('Redis Client Connected'); // console.log('Redis Client Connected');
this.isConnected = true; // this.isConnected = true;
}); // });
//
this.client.on('disconnect', () => { // this.client.on('disconnect', () => {
console.log('Redis Client Disconnected'); // console.log('Redis Client Disconnected');
this.isConnected = false; // this.isConnected = false;
}); // });
//
this.client.on('ready', () => { // this.client.on('ready', () => {
console.log('Redis Client Ready'); // console.log('Redis Client Ready');
}); // });
//
this.client.on('reconnecting', () => { // this.client.on('reconnecting', () => {
console.log('Redis Client Reconnecting'); // console.log('Redis Client Reconnecting');
}); // });
// Connect immediately (fire and forget) // Connect immediately (fire and forget)
// this.client.connect().catch((err) => { // this.client.connect().catch((err) => {

View file

@ -1,7 +1,7 @@
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter" // import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3" import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import signedUrlCache from "@/src/lib/signed-url-cache" // import signedUrlCache from "@/src/lib/signed-url-cache" // Disabled for Workers compatibility
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService' import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter" import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter"
@ -89,12 +89,11 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
const s3Url = s3UrlRaw const s3Url = s3UrlRaw
try { try {
// Check if we have a cached signed URL // Cache disabled for Workers compatibility
const cachedUrl = signedUrlCache.get(s3Url); // const cachedUrl = signedUrlCache.get(s3Url);
if (cachedUrl) { // if (cachedUrl) {
// Found in cache, return it // return cachedUrl;
return cachedUrl; // }
}
// Create the command to get the object // Create the command to get the object
const command = new GetObjectCommand({ const command = new GetObjectCommand({
@ -105,8 +104,8 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
// Generate the signed URL // Generate the signed URL
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn }); const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
// Cache the signed URL with TTL matching the expiration time (convert seconds to milliseconds) // Cache disabled for Workers compatibility
signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000); // Subtract 1 minute to ensure it doesn't expire before use // signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
return signedUrl; return signedUrl;
} catch (error) { } catch (error) {
@ -121,14 +120,9 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
* @returns The original S3 URL if found in cache, otherwise null * @returns The original S3 URL if found in cache, otherwise null
*/ */
export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null { export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
if (!signedUrl) { // Cache disabled for Workers compatibility - cannot retrieve original URL without cache
return null; // To re-enable, migrate signed-url-cache to object storage (R2/S3)
} return null;
// Try to find the original URL in our cache
const originalUrl = signedUrlCache.getOriginalUrl(signedUrl);
return originalUrl || null;
} }
/** /**

View file

@ -0,0 +1,263 @@
import fs from 'fs';
import path from 'path';
const CACHE_FILE_PATH = path.join('.', 'assets', 'signed-url-cache.json');
// Interface for cache entries with TTL
interface CacheEntry {
value: string;
expiresAt: number; // Timestamp when this entry expires
}
class SignedURLCache {
private originalToSignedCache: Map<string, CacheEntry>;
private signedToOriginalCache: Map<string, CacheEntry>;
constructor() {
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
// Create cache directory if it doesn't exist
const cacheDir = path.dirname(CACHE_FILE_PATH);
if (!fs.existsSync(cacheDir)) {
console.log('creating the directory')
fs.mkdirSync(cacheDir, { recursive: true });
}
else {
console.log('the directory is already present')
}
}
/**
* Get a signed URL from the cache using an original URL as the key
*/
get(originalUrl: string): string | undefined {
const entry = this.originalToSignedCache.get(originalUrl);
// If no entry or entry has expired, return undefined
if (!entry || Date.now() > entry.expiresAt) {
if (entry) {
// Remove expired entry
this.originalToSignedCache.delete(originalUrl);
// Also remove from reverse mapping if it exists
this.signedToOriginalCache.delete(entry.value);
}
return undefined;
}
return entry.value;
}
/**
* Get the original URL from the cache using a signed URL as the key
*/
getOriginalUrl(signedUrl: string): string | undefined {
const entry = this.signedToOriginalCache.get(signedUrl);
// If no entry or entry has expired, return undefined
if (!entry || Date.now() > entry.expiresAt) {
if (entry) {
// Remove expired entry
this.signedToOriginalCache.delete(signedUrl);
// Also remove from primary mapping if it exists
this.originalToSignedCache.delete(entry.value);
}
return undefined;
}
return entry.value;
}
/**
* Set a value in the cache with a TTL (Time To Live)
* @param originalUrl The original S3 URL
* @param signedUrl The signed URL
* @param ttlMs Time to live in milliseconds (default: 3 days)
*/
set(originalUrl: string, signedUrl: string, ttlMs: number = 259200000): void {
const expiresAt = Date.now() + ttlMs;
const entry: CacheEntry = {
value: signedUrl,
expiresAt
};
const reverseEntry: CacheEntry = {
value: originalUrl,
expiresAt
};
this.originalToSignedCache.set(originalUrl, entry);
this.signedToOriginalCache.set(signedUrl, reverseEntry);
}
has(originalUrl: string): boolean {
const entry = this.originalToSignedCache.get(originalUrl);
// Entry exists and hasn't expired
return !!entry && Date.now() <= entry.expiresAt;
}
hasSignedUrl(signedUrl: string): boolean {
const entry = this.signedToOriginalCache.get(signedUrl);
// Entry exists and hasn't expired
return !!entry && Date.now() <= entry.expiresAt;
}
clear(): void {
this.originalToSignedCache.clear();
this.signedToOriginalCache.clear();
this.saveToDisk();
}
/**
* Remove all expired entries from the cache
* @returns The number of expired entries that were removed
*/
clearExpired(): number {
const now = Date.now();
let removedCount = 0;
// Clear expired entries from original to signed cache
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
if (now > entry.expiresAt) {
this.originalToSignedCache.delete(originalUrl);
removedCount++;
}
}
// Clear expired entries from signed to original cache
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
if (now > entry.expiresAt) {
this.signedToOriginalCache.delete(signedUrl);
// No need to increment removedCount as we've already counted these in the first loop
}
}
if (removedCount > 0) {
console.log(`SignedURLCache: Cleared ${removedCount} expired entries`);
}
return removedCount;
}
/**
* Save the cache to disk
*/
saveToDisk(): void {
try {
// Remove expired entries before saving
const removedCount = this.clearExpired();
// Convert Maps to serializable objects
const serializedOriginalToSigned: Record<string, { value: string; expiresAt: number }> = {};
const serializedSignedToOriginal: Record<string, { value: string; expiresAt: number }> = {};
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
serializedOriginalToSigned[originalUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
serializedSignedToOriginal[signedUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
const serializedCache = {
originalToSigned: serializedOriginalToSigned,
signedToOriginal: serializedSignedToOriginal
};
// Write to file
fs.writeFileSync(
CACHE_FILE_PATH,
JSON.stringify(serializedCache),
'utf8'
);
console.log(`SignedURLCache: Saved ${this.originalToSignedCache.size} entries to disk`);
} catch (error) {
console.error('Error saving SignedURLCache to disk:', error);
}
}
/**
* Load the cache from disk
*/
loadFromDisk(): void {
try {
if (fs.existsSync(CACHE_FILE_PATH)) {
// Read from file
const data = fs.readFileSync(CACHE_FILE_PATH, 'utf8');
// Parse the data
const parsedData = JSON.parse(data) as {
originalToSigned: Record<string, { value: string; expiresAt: number }>,
signedToOriginal: Record<string, { value: string; expiresAt: number }>
};
// Only load entries that haven't expired yet
const now = Date.now();
let loadedCount = 0;
let expiredCount = 0;
// Load original to signed mappings
if (parsedData.originalToSigned) {
for (const [originalUrl, entry] of Object.entries(parsedData.originalToSigned)) {
if (now <= entry.expiresAt) {
this.originalToSignedCache.set(originalUrl, entry);
loadedCount++;
} else {
expiredCount++;
}
}
}
// Load signed to original mappings
if (parsedData.signedToOriginal) {
for (const [signedUrl, entry] of Object.entries(parsedData.signedToOriginal)) {
if (now <= entry.expiresAt) {
this.signedToOriginalCache.set(signedUrl, entry);
// Don't increment loadedCount as these are pairs of what we already counted
} else {
// Don't increment expiredCount as these are pairs of what we already counted
}
}
}
console.log(`SignedURLCache: Loaded ${loadedCount} valid entries from disk (skipped ${expiredCount} expired entries)`);
} else {
console.log('SignedURLCache: No cache file found, starting with empty cache');
}
} catch (error) {
console.error('Error loading SignedURLCache from disk:', error);
// Start with empty caches if loading fails
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
}
}
}
// Create a singleton instance to be used throughout the application
const signedUrlCache = new SignedURLCache();
process.on('SIGINT', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
export default signedUrlCache;

285
apps/backend/src/lib/signed-url-cache.ts Executable file → Normal file
View file

@ -1,263 +1,24 @@
import fs from 'fs'; // SIGNED URL CACHE - DISABLED
import path from 'path'; // This file has been disabled to make the backend compatible with Cloudflare Workers.
// File system operations are not available in the Workers environment.
//
// To re-enable caching, migrate to Cloudflare R2 or another object storage solution.
// Original file saved as: signed-url-cache-old.ts
//
// Impact of disabling:
// - S3 signed URLs are generated fresh on every request
// - Increased AWS API calls (higher costs)
// - Slightly slower image loading
// - No file system dependencies (Workers-compatible)
const CACHE_FILE_PATH = path.join('.', 'assets', 'signed-url-cache.json'); export default {
get: () => undefined,
// Interface for cache entries with TTL set: () => {},
interface CacheEntry { getOriginalUrl: () => undefined,
value: string; has: () => false,
expiresAt: number; // Timestamp when this entry expires hasSignedUrl: () => false,
} clear: () => {},
clearExpired: () => 0,
class SignedURLCache { saveToDisk: () => {},
private originalToSignedCache: Map<string, CacheEntry>; loadFromDisk: () => {},
private signedToOriginalCache: Map<string, CacheEntry>; };
constructor() {
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
// Create cache directory if it doesn't exist
const cacheDir = path.dirname(CACHE_FILE_PATH);
if (!fs.existsSync(cacheDir)) {
console.log('creating the directory')
fs.mkdirSync(cacheDir, { recursive: true });
}
else {
console.log('the directory is already present')
}
}
/**
* Get a signed URL from the cache using an original URL as the key
*/
get(originalUrl: string): string | undefined {
const entry = this.originalToSignedCache.get(originalUrl);
// If no entry or entry has expired, return undefined
if (!entry || Date.now() > entry.expiresAt) {
if (entry) {
// Remove expired entry
this.originalToSignedCache.delete(originalUrl);
// Also remove from reverse mapping if it exists
this.signedToOriginalCache.delete(entry.value);
}
return undefined;
}
return entry.value;
}
/**
* Get the original URL from the cache using a signed URL as the key
*/
getOriginalUrl(signedUrl: string): string | undefined {
const entry = this.signedToOriginalCache.get(signedUrl);
// If no entry or entry has expired, return undefined
if (!entry || Date.now() > entry.expiresAt) {
if (entry) {
// Remove expired entry
this.signedToOriginalCache.delete(signedUrl);
// Also remove from primary mapping if it exists
this.originalToSignedCache.delete(entry.value);
}
return undefined;
}
return entry.value;
}
/**
* Set a value in the cache with a TTL (Time To Live)
* @param originalUrl The original S3 URL
* @param signedUrl The signed URL
* @param ttlMs Time to live in milliseconds (default: 3 days)
*/
set(originalUrl: string, signedUrl: string, ttlMs: number = 259200000): void {
const expiresAt = Date.now() + ttlMs;
const entry: CacheEntry = {
value: signedUrl,
expiresAt
};
const reverseEntry: CacheEntry = {
value: originalUrl,
expiresAt
};
this.originalToSignedCache.set(originalUrl, entry);
this.signedToOriginalCache.set(signedUrl, reverseEntry);
}
has(originalUrl: string): boolean {
const entry = this.originalToSignedCache.get(originalUrl);
// Entry exists and hasn't expired
return !!entry && Date.now() <= entry.expiresAt;
}
hasSignedUrl(signedUrl: string): boolean {
const entry = this.signedToOriginalCache.get(signedUrl);
// Entry exists and hasn't expired
return !!entry && Date.now() <= entry.expiresAt;
}
clear(): void {
this.originalToSignedCache.clear();
this.signedToOriginalCache.clear();
this.saveToDisk();
}
/**
* Remove all expired entries from the cache
* @returns The number of expired entries that were removed
*/
clearExpired(): number {
const now = Date.now();
let removedCount = 0;
// Clear expired entries from original to signed cache
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
if (now > entry.expiresAt) {
this.originalToSignedCache.delete(originalUrl);
removedCount++;
}
}
// Clear expired entries from signed to original cache
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
if (now > entry.expiresAt) {
this.signedToOriginalCache.delete(signedUrl);
// No need to increment removedCount as we've already counted these in the first loop
}
}
if (removedCount > 0) {
console.log(`SignedURLCache: Cleared ${removedCount} expired entries`);
}
return removedCount;
}
/**
* Save the cache to disk
*/
saveToDisk(): void {
try {
// Remove expired entries before saving
const removedCount = this.clearExpired();
// Convert Maps to serializable objects
const serializedOriginalToSigned: Record<string, { value: string; expiresAt: number }> = {};
const serializedSignedToOriginal: Record<string, { value: string; expiresAt: number }> = {};
for (const [originalUrl, entry] of this.originalToSignedCache.entries()) {
serializedOriginalToSigned[originalUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
for (const [signedUrl, entry] of this.signedToOriginalCache.entries()) {
serializedSignedToOriginal[signedUrl] = {
value: entry.value,
expiresAt: entry.expiresAt
};
}
const serializedCache = {
originalToSigned: serializedOriginalToSigned,
signedToOriginal: serializedSignedToOriginal
};
// Write to file
fs.writeFileSync(
CACHE_FILE_PATH,
JSON.stringify(serializedCache),
'utf8'
);
console.log(`SignedURLCache: Saved ${this.originalToSignedCache.size} entries to disk`);
} catch (error) {
console.error('Error saving SignedURLCache to disk:', error);
}
}
/**
* Load the cache from disk
*/
loadFromDisk(): void {
try {
if (fs.existsSync(CACHE_FILE_PATH)) {
// Read from file
const data = fs.readFileSync(CACHE_FILE_PATH, 'utf8');
// Parse the data
const parsedData = JSON.parse(data) as {
originalToSigned: Record<string, { value: string; expiresAt: number }>,
signedToOriginal: Record<string, { value: string; expiresAt: number }>
};
// Only load entries that haven't expired yet
const now = Date.now();
let loadedCount = 0;
let expiredCount = 0;
// Load original to signed mappings
if (parsedData.originalToSigned) {
for (const [originalUrl, entry] of Object.entries(parsedData.originalToSigned)) {
if (now <= entry.expiresAt) {
this.originalToSignedCache.set(originalUrl, entry);
loadedCount++;
} else {
expiredCount++;
}
}
}
// Load signed to original mappings
if (parsedData.signedToOriginal) {
for (const [signedUrl, entry] of Object.entries(parsedData.signedToOriginal)) {
if (now <= entry.expiresAt) {
this.signedToOriginalCache.set(signedUrl, entry);
// Don't increment loadedCount as these are pairs of what we already counted
} else {
// Don't increment expiredCount as these are pairs of what we already counted
}
}
}
console.log(`SignedURLCache: Loaded ${loadedCount} valid entries from disk (skipped ${expiredCount} expired entries)`);
} else {
console.log('SignedURLCache: No cache file found, starting with empty cache');
}
} catch (error) {
console.error('Error loading SignedURLCache from disk:', error);
// Start with empty caches if loading fails
this.originalToSignedCache = new Map();
this.signedToOriginalCache = new Map();
}
}
}
// Create a singleton instance to be used throughout the application
const signedUrlCache = new SignedURLCache();
process.on('SIGINT', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('SignedURLCache: Saving cache before shutdown...');
signedUrlCache.saveToDisk();
process.exit(0);
});
export default signedUrlCache;

View file

@ -2,7 +2,7 @@ import { Context, Next } from 'hono';
import { jwtVerify } from 'jose'; import { jwtVerify } from 'jose';
import { getStaffUserById, isUserSuspended } from '@/src/dbService'; import { getStaffUserById, isUserSuspended } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { encodedJwtSecret } from '@/src/lib/env-exporter'; import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
interface UserContext { interface UserContext {
userId: number; userId: number;
@ -27,7 +27,7 @@ export const authenticateUser = async (c: Context, next: Next) => {
const token = authHeader.substring(7); const token = authHeader.substring(7);
console.log(c.req.header) console.log(c.req.header)
const { payload } = await jwtVerify(token, encodedJwtSecret); const { payload } = await jwtVerify(token, getEncodedJwtSecret());
const decoded = payload as any; const decoded = payload as any;
// Check if this is a staff token (has staffId) // Check if this is a staff token (has staffId)

View file

@ -1,7 +1,7 @@
import { Context, Next } from 'hono'; import { Context, Next } from 'hono';
import { jwtVerify, errors } from 'jose'; import { jwtVerify, errors } from 'jose';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { encodedJwtSecret } from '@/src/lib/env-exporter'; import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
export const verifyToken = async (c: Context, next: Next) => { export const verifyToken = async (c: Context, next: Next) => {
try { try {
@ -20,7 +20,7 @@ export const verifyToken = async (c: Context, next: Next) => {
} }
// Verify token // Verify token
const { payload } = await jwtVerify(token, encodedJwtSecret); const { payload } = await jwtVerify(token, getEncodedJwtSecret());
// Add user info to context // Add user info to context

View file

@ -2,14 +2,14 @@ import { Context, Next } from 'hono';
import { jwtVerify } from 'jose'; import { jwtVerify } from 'jose';
import { getStaffUserById } from '@/src/dbService'; import { getStaffUserById } from '@/src/dbService';
import { ApiError } from '@/src/lib/api-error'; import { ApiError } from '@/src/lib/api-error';
import { encodedJwtSecret } from '@/src/lib/env-exporter'; import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
/** /**
* Verify JWT token and extract payload * Verify JWT token and extract payload
*/ */
const verifyStaffToken = async (token: string) => { const verifyStaffToken = async (token: string) => {
try { try {
const { payload } = await jwtVerify(token, encodedJwtSecret); const { payload } = await jwtVerify(token, getEncodedJwtSecret());
return payload; return payload;
} catch (error) { } catch (error) {
throw new ApiError('Access denied. Invalid auth credentials', 401); throw new ApiError('Access denied. Invalid auth credentials', 401);

View file

@ -0,0 +1,294 @@
// SQLite Importer - Intermediate layer to avoid direct db_helper_sqlite imports in dbService
// This file re-exports everything from sqliteService
// Re-export database connection
export { db, initDb } from 'sqliteService'
// Re-export all schema exports
export * from 'sqliteService'
// Re-export all helper methods from sqliteService
export {
// Admin - Banner
getBanners,
getBannerById,
createBanner,
updateBanner,
deleteBanner,
// Admin - Complaint
getComplaints,
resolveComplaint,
// Admin - Constants
getAllConstants,
upsertConstants,
// Admin - Coupon
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
// Admin - Order
updateOrderNotes,
getOrderDetails,
updateOrderPackaged,
updateOrderDelivered,
updateOrderItemPackaging,
removeDeliveryCharge,
getSlotOrders,
updateAddressCoords,
getAllOrders,
rebalanceSlots,
cancelOrder,
deleteOrderById,
// Admin - Product
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
createSpecialDealsForProduct,
updateProductDeals,
replaceProductTags,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getAllProductTagInfos,
getProductTagInfoById,
createProductTag,
getProductTagById,
updateProductTag,
deleteProductTag,
checkProductTagExistsByName,
getProductReviews,
respondToReview,
getAllProductGroups,
createProductGroup,
updateProductGroup,
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
// Admin - Slots
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
// Admin - Staff User
getStaffUserByName,
getStaffUserById,
getAllStaff,
getAllUsers,
getUserWithDetails,
updateUserSuspensionStatus,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
// Admin - Store
getAllStores,
getStoreById,
createStore,
updateStore,
deleteStore,
// Admin - User
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
getLastOrdersByUserIds,
getSuspensionStatusesByUserIds,
getUserBasicInfo,
getUserSuspensionStatus,
getUserOrders,
getOrderStatusesByOrderIds,
getItemCountsByOrderIds,
upsertUserSuspension,
searchUsers,
getAllNotifCreds,
getAllUnloggedTokens,
getNotifTokensByUserIds,
getUserIncidentsWithRelations,
createUserIncident,
// Admin - Vendor Snippets
checkVendorSnippetExists,
getVendorSnippetById,
getVendorSnippetByCode,
getAllVendorSnippets,
createVendorSnippet,
updateVendorSnippet,
deleteVendorSnippet,
getProductsByIds,
getVendorSlotById,
getVendorOrdersBySlotId,
getOrderItemsByOrderIds,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
// User - Address
getUserDefaultAddress,
getUserAddresses,
getUserAddressById,
clearUserDefaultAddress,
createUserAddress,
updateUserAddress,
deleteUserAddress,
hasOngoingOrdersForAddress,
// User - Banners
getUserActiveBanners,
// User - Cart
getUserCartItemsWithProducts,
getUserProductById,
getUserCartItemByUserProduct,
incrementUserCartItemQuantity,
insertUserCartItem,
updateUserCartItemQuantity,
deleteUserCartItem,
clearUserCart,
// User - Complaint
getUserComplaints,
createUserComplaint,
// User - Stores
getUserStoreSummaries,
getUserStoreDetail,
// User - Product
getUserProductDetailById,
getUserProductReviews,
getUserProductByIdBasic,
createUserProductReview,
getAllProductsWithUnits,
type ProductSummaryData,
// User - Slots
getUserActiveSlotsList,
getUserProductAvailability,
// User - Payments
getUserPaymentOrderById,
getUserPaymentByOrderId,
getUserPaymentByMerchantOrderId,
updateUserPaymentSuccess,
updateUserOrderPaymentStatus,
markUserPaymentFailed,
// User - Auth
getUserAuthByEmail,
getUserAuthByMobile,
getUserAuthById,
getUserAuthCreds,
getUserAuthDetails,
isUserSuspended,
createUserAuthWithCreds,
createUserAuthWithMobile,
upsertUserAuthPassword,
deleteUserAuthAccount,
// UV API helpers
createUserWithProfile,
getUserDetailsByUserId,
updateUserProfile,
// User - Coupon
getUserActiveCouponsWithRelations,
getUserAllCouponsWithRelations,
getUserReservedCouponByCode,
redeemUserReservedCoupon,
// User - Profile
getUserProfileById,
getUserProfileDetailById,
getUserWithCreds,
getUserNotifCred,
upsertUserNotifCred,
deleteUserUnloggedToken,
getUserUnloggedToken,
upsertUserUnloggedToken,
// User - Order
validateAndGetUserCoupon,
applyDiscountToUserOrder,
getUserAddressByIdAndUser,
getOrderProductById,
checkUserSuspended,
getUserSlotCapacityStatus,
placeUserOrderTransaction,
deleteUserCartItemsForOrder,
recordUserCouponUsage,
getUserOrdersWithRelations,
getUserOrderCount,
getUserOrderByIdWithRelations,
getUserCouponUsageForOrder,
getUserOrderBasic,
cancelUserOrderTransaction,
updateUserOrderNotes,
getUserRecentlyDeliveredOrderIds,
getUserProductIdsFromOrders,
getUserProductsForRecentOrders,
// Store Helpers
getAllBannersForCache,
getAllProductsForCache,
getAllStoresForCache,
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
getAllProductTagsForCache,
getAllTagsForCache,
getAllTagProductMappings,
getAllSlotsWithProductsForCache,
getAllUserNegativityScores,
getUserNegativityScore,
type BannerData,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
type TagBasicData,
type TagProductMapping,
type SlotWithProductsData,
type UserNegativityData,
// Automated Jobs
toggleFlashDeliveryForItems,
toggleKeyVal,
getAllKeyValStore,
// Post-order handler helpers
getOrdersByIdsWithFullData,
getOrderByIdWithFullData,
type OrderWithFullData,
type OrderWithCancellationData,
// Common API helpers
getSuspendedProductIds,
getNextDeliveryDateWithCapacity,
getStoresSummary,
healthCheck,
// Delete orders helper
deleteOrdersWithRelations,
// Seed helpers
seedUnits,
seedStaffRoles,
seedStaffPermissions,
seedRolePermissions,
seedKeyValStore,
type UnitSeedData,
type RolePermissionAssignment,
type KeyValSeedData,
type StaffRoleName,
type StaffPermissionName,
// Upload URL Helpers
createUploadUrlStatus,
claimUploadUrlStatus,
} from 'sqliteService'

View file

@ -661,7 +661,7 @@ export const productRouter = router({
return { return {
groups: groups.map(group => ({ groups: groups.map(group => ({
...group, ...group,
products: group.memberships.map(m => ({ products: group.memberships.map((m: any) => ({
...(m.product as AdminProduct), ...(m.product as AdminProduct),
images: (m.product.images as string[]) || null, images: (m.product.images as string[]) || null,
})), })),

View file

@ -2,7 +2,7 @@ import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-ind
import { z } from 'zod'; import { z } from 'zod';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { SignJWT } from 'jose'; import { SignJWT } from 'jose';
import { encodedJwtSecret } from '@/src/lib/env-exporter'; import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { import {
getStaffUserByName, getStaffUserByName,
@ -44,7 +44,7 @@ export const staffUserRouter = router({
const token = await new SignJWT({ staffId: staff.id, name: staff.name }) const token = await new SignJWT({ staffId: staff.id, name: staff.name })
.setProtectedHeader({ alg: 'HS256' }) .setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('30d') .setExpirationTime('30d')
.sign(encodedJwtSecret); .sign(getEncodedJwtSecret());
return { return {
message: 'Login successful', message: 'Login successful',

View file

@ -420,9 +420,9 @@ export const vendorSnippetsRouter = router({
productName: item.product.name, productName: item.product.name,
quantity: parseFloat(item.quantity), quantity: parseFloat(item.quantity),
productSize: item.product.productQuantity, productSize: item.product.productQuantity,
price: parseFloat(item.price.toString()), price: parseFloat((item.price ?? 0).toString()),
unit: item.product.unit?.shortNotation || 'unit', unit: item.product.unit?.shortNotation || 'unit',
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
is_packaged: item.is_packaged, is_packaged: item.is_packaged,
is_package_verified: item.is_package_verified, is_package_verified: item.is_package_verified,
})); }));
@ -604,9 +604,9 @@ export const vendorSnippetsRouter = router({
productId: item.productId, productId: item.productId,
productName: item.product.name, productName: item.product.name,
quantity: parseFloat(item.quantity), quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()), price: parseFloat((item.price ?? 0).toString()),
unit: item.product.unit?.shortNotation || 'unit', unit: item.product.unit?.shortNotation || 'unit',
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
productSize: item.product.productQuantity, productSize: item.product.productQuantity,
is_packaged: item.is_packaged, is_packaged: item.is_packaged,
is_package_verified: item.is_package_verified, is_package_verified: item.is_package_verified,

View file

@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs'
import { SignJWT } from 'jose'; import { SignJWT } from 'jose';
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { encodedJwtSecret } from '@/src/lib/env-exporter' import { getEncodedJwtSecret } from '@/src/lib/env-exporter'
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils' import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'
import { import {
getUserAuthByEmail as getUserAuthByEmailInDb, getUserAuthByEmail as getUserAuthByEmailInDb,
@ -45,7 +45,7 @@ const generateToken = async (userId: number): Promise<string> => {
return await new SignJWT({ userId }) return await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' }) .setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d') .setExpirationTime('7d')
.sign(encodedJwtSecret); .sign(getEncodedJwtSecret());
}; };
@ -110,7 +110,9 @@ export const authRouter = router({
createdAt: foundUser.createdAt.toISOString(), createdAt: foundUser.createdAt.toISOString(),
profileImage: profileImageSignedUrl, profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null, bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null, dateOfBirth: userDetail?.dateOfBirth
? new Date(userDetail.dateOfBirth as any).toISOString()
: null,
gender: userDetail?.gender || null, gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null, occupation: userDetail?.occupation || null,
}, },
@ -370,7 +372,9 @@ export const authRouter = router({
createdAt: updatedUser.createdAt?.toISOString?.() || new Date().toISOString(), createdAt: updatedUser.createdAt?.toISOString?.() || new Date().toISOString(),
profileImage: profileImageSignedUrl, profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null, bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null, dateOfBirth: userDetail?.dateOfBirth
? new Date(userDetail.dateOfBirth as any).toISOString()
: null,
gender: userDetail?.gender || null, gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null, occupation: userDetail?.occupation || null,
}, },

View file

@ -1,25 +1,29 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"; import { router, protectedProcedure } from "@/src/trpc/trpc-index";
import { z } from "zod"; import { z } from "zod";
import { import {
validateAndGetUserCoupon,
applyDiscountToUserOrder, applyDiscountToUserOrder,
getUserAddressByIdAndUser, cancelUserOrderTransaction,
getOrderProductById,
checkUserSuspended, checkUserSuspended,
getUserSlotCapacityStatus, db,
placeUserOrderTransaction,
deleteUserCartItemsForOrder, deleteUserCartItemsForOrder,
recordUserCouponUsage, getOrderProductById,
getUserOrdersWithRelations, getUserAddressByIdAndUser,
getUserOrderCount,
getUserOrderByIdWithRelations,
getUserCouponUsageForOrder, getUserCouponUsageForOrder,
getUserOrderBasic, getUserOrderBasic,
cancelUserOrderTransaction, getUserOrderByIdWithRelations,
updateUserOrderNotes, getUserOrderCount,
getUserRecentlyDeliveredOrderIds, getUserOrdersWithRelations,
getUserProductIdsFromOrders, getUserProductIdsFromOrders,
getUserProductsForRecentOrders, getUserProductsForRecentOrders,
getUserRecentlyDeliveredOrderIds,
getUserSlotCapacityStatus,
orders,
orderItems,
orderStatus,
placeUserOrderTransaction,
recordUserCouponUsage,
updateUserOrderNotes,
validateAndGetUserCoupon,
} from "@/src/dbService"; } from "@/src/dbService";
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common"; import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
import { scaffoldAssetUrl } from "@/src/lib/s3-client"; import { scaffoldAssetUrl } from "@/src/lib/s3-client";
@ -115,9 +119,10 @@ const placeOrderUtil = async (params: {
const orderTotal = items.reduce( const orderTotal = items.reduce(
(sum, item) => { (sum, item) => {
if (!item.product) return sum if (!item.product) return sum
const itemPrice = params.isFlash const basePrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString()) ? (item.product.flashPrice ?? item.product.price)
: parseFloat(item.product.price.toString()); : item.product.price
const itemPrice = parseFloat((basePrice ?? 0).toString())
return sum + itemPrice * item.quantity; return sum + itemPrice * item.quantity;
}, },
0 0
@ -132,9 +137,6 @@ const placeOrderUtil = async (params: {
const totalWithDelivery = totalAmount + expectedDeliveryCharge; const totalWithDelivery = totalAmount + expectedDeliveryCharge;
const { db } = await import("postgresService");
const { orders, orderItems, orderStatus } = await import("postgresService");
type OrderData = { type OrderData = {
order: Omit<typeof orders.$inferInsert, "id">; order: Omit<typeof orders.$inferInsert, "id">;
orderItems: Omit<typeof orderItems.$inferInsert, "id">[]; orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
@ -148,9 +150,10 @@ const placeOrderUtil = async (params: {
const subOrderTotal = items.reduce( const subOrderTotal = items.reduce(
(sum, item) => { (sum, item) => {
if (!item.product) return sum if (!item.product) return sum
const itemPrice = params.isFlash const basePrice = params.isFlash
? parseFloat((item.product.flashPrice || item.product.price).toString()) ? (item.product.flashPrice ?? item.product.price)
: parseFloat(item.product.price.toString()); : item.product.price
const itemPrice = parseFloat((basePrice ?? 0).toString())
return sum + itemPrice * item.quantity; return sum + itemPrice * item.quantity;
}, },
0 0
@ -182,23 +185,26 @@ const placeOrderUtil = async (params: {
isFlashDelivery: params.isFlash, isFlashDelivery: params.isFlash,
}; };
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items const validItems = items.filter(
.filter((item) => item.product !== null && item.product !== undefined) (item): item is typeof item & { product: NonNullable<typeof item.product> } =>
.map( item.product !== null && item.product !== undefined
(item) => ({ )
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = validItems.map(
(item) => {
const basePrice = params.isFlash
? (item.product.flashPrice ?? item.product.price)
: item.product.price
const priceString = (basePrice ?? 0).toString()
return {
orderId: 0, orderId: 0,
productId: item.productId, productId: item.productId,
quantity: item.quantity.toString(), quantity: item.quantity.toString(),
price: params.isFlash price: priceString,
? item.product!.flashPrice || item.product!.price discountedPrice: priceString,
: item.product!.price, }
discountedPrice: ( }
params.isFlash );
? item.product!.flashPrice || item.product!.price
: item.product!.price
).toString(),
})
);
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = { const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
userId, userId,

View file

@ -65,9 +65,12 @@ export const paymentRouter = router({
}; };
} }
// Create Razorpay order and insert payment record // Create Razorpay order and insert payment record
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount); if (order.totalAmount === null) {
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder); throw new ApiError('Order total is missing', 400)
}
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
return { return {
razorpayOrderId: 0, razorpayOrderId: 0,

View file

@ -2,7 +2,7 @@ import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-ind
import { SignJWT } from 'jose' import { SignJWT } from 'jose'
import { z } from 'zod' import { z } from 'zod'
import { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { encodedJwtSecret } from '@/src/lib/env-exporter' import { getEncodedJwtSecret } from '@/src/lib/env-exporter'
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client' import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { import {
getUserProfileById as getUserProfileByIdInDb, getUserProfileById as getUserProfileByIdInDb,
@ -23,7 +23,7 @@ const generateToken = async (userId: number): Promise<string> => {
return await new SignJWT({ userId }) return await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' }) .setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d') .setExpirationTime('7d')
.sign(encodedJwtSecret); .sign(getEncodedJwtSecret());
}; };
export const userRouter = router({ export const userRouter = router({
@ -59,7 +59,9 @@ export const userRouter = router({
mobile: user.mobile, mobile: user.mobile,
profileImage: profileImageSignedUrl, profileImage: profileImageSignedUrl,
bio: userDetail?.bio || null, bio: userDetail?.bio || null,
dateOfBirth: userDetail?.dateOfBirth || null, dateOfBirth: userDetail?.dateOfBirth
? new Date(userDetail.dateOfBirth as any).toISOString()
: null,
gender: userDetail?.gender || null, gender: userDetail?.gender || null,
occupation: userDetail?.occupation || null, occupation: userDetail?.occupation || null,
}, },

View file

@ -35,8 +35,10 @@
"@commonTypes/*": ["../../packages/ui/shared-types/*"], "@commonTypes/*": ["../../packages/ui/shared-types/*"],
"@packages/shared": ["../../packages/shared"], "@packages/shared": ["../../packages/shared"],
"@packages/shared/*": ["../../packages/shared/*"], "@packages/shared/*": ["../../packages/shared/*"],
"postgresService": ["../../packages/db_helper_postgres"], // "postgresService": ["../../packages/db_helper_postgres"],
"postgresService/*": ["../../packages/db_helper_postgres/*"], // "postgresService/*": ["../../packages/db_helper_postgres/*"],
"sqliteService": ["../../packages/db_helper_sqlite"],
"sqliteService/*": ["../../packages/db_helper_sqlite/*"],
"global-shared": ["../../packages/shared"], "global-shared": ["../../packages/shared"],
"global-shared/*": ["../../packages/shared/*"] "global-shared/*": ["../../packages/shared/*"]
}, },
@ -122,6 +124,5 @@
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"include": ["src", "types", "index.ts", "../shared-types", "../../packages/shared"] "include": ["src", "types", "index.ts", "worker.ts", "../shared-types", "../../packages/shared"]
} }

18
apps/backend/worker.ts Normal file
View file

@ -0,0 +1,18 @@
import type { ExecutionContext, D1Database } from '@cloudflare/workers-types'
export default {
async fetch(
request: Request,
env: Record<string, string> & { DB?: D1Database },
ctx: ExecutionContext
) {
;(globalThis as any).ENV = env
const { createApp } = await import('./src/app')
const { initDb } = await import('./src/dbService')
if (env.DB) {
initDb(env.DB)
}
const app = createApp()
return app.fetch(request, env, ctx)
},
}

View file

@ -0,0 +1,41 @@
name = "freshyo-backend"
main = "worker.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "freshyo-dev"
database_id = "45e81d12-9043-45ad-a8ba-3b93127dc5ea"
[vars]
ENV_MODE = "PROD"
DATABASE_URL = "postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer"
PHONE_PE_BASE_URL = "https://api-preprod.phonepe.com/"
PHONE_PE_CLIENT_ID = "TEST-M23F2IGP34ZAR_25090"
PHONE_PE_CLIENT_VERSION = "1"
PHONE_PE_CLIENT_SECRET = "MTU1MmIzOTgtM2Q0Mi00N2M5LTkyMWUtNzBiMjdmYzVmZWUy"
PHONE_PE_MERCHANT_ID = "M23F2IGP34ZAR"
S3_REGION = "apac"
S3_ACCESS_KEY_ID = "8fab47503efb9547b50e4fb317e35cc7"
S3_SECRET_ACCESS_KEY = "47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950"
S3_URL = "https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com"
S3_BUCKET_NAME = "meatfarmer-dev"
EXPO_ACCESS_TOKEN = "Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-"
JWT_SECRET = "my_meatfarmer_jwt_secret_key"
ASSETS_DOMAIN = "https://assets2.freshyo.in/"
API_CACHE_KEY = "api-cache-dev"
CLOUDFLARE_API_TOKEN = "N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh"
CLOUDFLARE_ZONE_ID = "edefbf750bfc3ff26ccd11e8e28dc8d7"
REDIS_URL = "redis://default:redis_shafi_password@57.128.212.174:6379"
APP_URL = "http://localhost:4000"
RAZORPAY_KEY = "rzp_test_RdCBBUJ56NLaJK"
RAZORPAY_SECRET = "namEwKBE1ypWxH0QDVg6fWOe"
OTP_SENDER_AUTH_TOKEN = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJDLTM5OENEMkJDRTM0MjQ4OCIsImlhdCI6MTc0Nzg0MTEwMywiZXhwIjoxOTA1NTIxMTAzfQ.IV64ofVKjcwveIanxu_P2XlACtPeA9sJQ74uM53osDeyUXsFv0rwkCl6NNBIX93s_wnh4MKITLbcF_ClwmFQ0A"
MIN_ORDER_VALUE = "300"
DELIVERY_CHARGE = "20"
TELEGRAM_BOT_TOKEN = "8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U"
TELEGRAM_CHAT_IDS = "5147760058"
[build]
upload_source_maps = true

View file

@ -21,6 +21,7 @@ type StoreWithProductsResponse = StoreWithProductsApiType;
function useCacheUrl(filename: string): string | null { function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts() const { data: essentialConsts } = useGetEssentialConsts()
console.log(essentialConsts)
const assetsDomain = essentialConsts?.assetsDomain const assetsDomain = essentialConsts?.assetsDomain
const apiCacheKey = essentialConsts?.apiCacheKey const apiCacheKey = essentialConsts?.apiCacheKey

570
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,501 @@
CREATE TABLE `address_areas` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`place_name` text NOT NULL,
`zone_id` integer,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`zone_id`) REFERENCES `address_zones`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `address_zones` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`zone_name` text NOT NULL,
`added_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `addresses` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`name` text NOT NULL,
`phone` text NOT NULL,
`address_line1` text NOT NULL,
`address_line2` text,
`city` text NOT NULL,
`state` text NOT NULL,
`pincode` text NOT NULL,
`is_default` integer DEFAULT false NOT NULL,
`latitude` real,
`longitude` real,
`google_maps_url` text,
`admin_latitude` real,
`admin_longitude` real,
`zone_id` integer,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`zone_id`) REFERENCES `address_zones`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `cart_items` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`product_id` integer NOT NULL,
`quantity` text NOT NULL,
`added_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_user_product` ON `cart_items` (`user_id`,`product_id`);--> statement-breakpoint
CREATE TABLE `complaints` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`order_id` integer,
`complaint_body` text NOT NULL,
`images` text,
`response` text,
`is_resolved` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `coupon_applicable_products` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`coupon_id` integer NOT NULL,
`product_id` integer NOT NULL,
FOREIGN KEY (`coupon_id`) REFERENCES `coupons`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_coupon_product` ON `coupon_applicable_products` (`coupon_id`,`product_id`);--> statement-breakpoint
CREATE TABLE `coupon_applicable_users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`coupon_id` integer NOT NULL,
`user_id` integer NOT NULL,
FOREIGN KEY (`coupon_id`) REFERENCES `coupons`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_coupon_user` ON `coupon_applicable_users` (`coupon_id`,`user_id`);--> statement-breakpoint
CREATE TABLE `coupon_usage` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`coupon_id` integer NOT NULL,
`order_id` integer,
`order_item_id` integer,
`used_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`coupon_id`) REFERENCES `coupons`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`order_item_id`) REFERENCES `order_items`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `coupons` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`coupon_code` text NOT NULL,
`is_user_based` integer DEFAULT false NOT NULL,
`discount_percent` text,
`flat_discount` text,
`min_order` text,
`product_ids` text,
`created_by` integer NOT NULL,
`max_value` text,
`is_apply_for_all` integer DEFAULT false NOT NULL,
`valid_till` integer,
`max_limit_for_user` integer,
`is_invalidated` integer DEFAULT false NOT NULL,
`exclusive_apply` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`created_by`) REFERENCES `staff_users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `coupons_coupon_code_unique` ON `coupons` (`coupon_code`);--> statement-breakpoint
CREATE TABLE `delivery_slot_info` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`delivery_time` integer NOT NULL,
`freeze_time` integer NOT NULL,
`is_active` integer DEFAULT true NOT NULL,
`is_flash` integer DEFAULT false NOT NULL,
`is_capacity_full` integer DEFAULT false NOT NULL,
`delivery_sequence` text,
`group_ids` text
);
--> statement-breakpoint
CREATE TABLE `home_banners` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`image_url` text NOT NULL,
`description` text,
`product_ids` text,
`redirect_url` text,
`serial_num` integer,
`is_active` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`last_updated` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `key_val_store` (
`key` text PRIMARY KEY NOT NULL,
`value` text
);
--> statement-breakpoint
CREATE TABLE `notif_creds` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`token` text NOT NULL,
`added_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`user_id` integer NOT NULL,
`last_verified` integer,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `notif_creds_token_unique` ON `notif_creds` (`token`);--> statement-breakpoint
CREATE TABLE `notifications` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`title` text NOT NULL,
`body` text NOT NULL,
`type` text,
`is_read` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `order_items` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`order_id` integer NOT NULL,
`product_id` integer NOT NULL,
`quantity` text NOT NULL,
`price` text NOT NULL,
`discounted_price` text,
`is_packaged` integer DEFAULT false NOT NULL,
`is_package_verified` integer DEFAULT false NOT NULL,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `order_status` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`order_time` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`user_id` integer NOT NULL,
`order_id` integer NOT NULL,
`is_packaged` integer DEFAULT false NOT NULL,
`is_delivered` integer DEFAULT false NOT NULL,
`is_cancelled` integer DEFAULT false NOT NULL,
`cancel_reason` text,
`is_cancelled_by_admin` integer,
`payment_state` text DEFAULT 'pending' NOT NULL,
`cancellation_user_notes` text,
`cancellation_admin_notes` text,
`cancellation_reviewed` integer DEFAULT false NOT NULL,
`cancellation_reviewed_at` integer,
`refund_coupon_id` integer,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`refund_coupon_id`) REFERENCES `coupons`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `orders` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`address_id` integer NOT NULL,
`slot_id` integer,
`is_cod` integer DEFAULT false NOT NULL,
`is_online_payment` integer DEFAULT false NOT NULL,
`payment_info_id` integer,
`total_amount` text NOT NULL,
`delivery_charge` text DEFAULT '0' NOT NULL,
`readable_id` integer NOT NULL,
`admin_notes` text,
`user_notes` text,
`order_group_id` text,
`order_group_proportion` text,
`is_flash_delivery` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`address_id`) REFERENCES `addresses`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`slot_id`) REFERENCES `delivery_slot_info`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`payment_info_id`) REFERENCES `payment_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `payment_info` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`status` text NOT NULL,
`gateway` text NOT NULL,
`order_id` text,
`token` text,
`merchant_order_id` text NOT NULL,
`payload` text
);
--> statement-breakpoint
CREATE UNIQUE INDEX `payment_info_merchant_order_id_unique` ON `payment_info` (`merchant_order_id`);--> statement-breakpoint
CREATE TABLE `payments` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`status` text NOT NULL,
`gateway` text NOT NULL,
`order_id` integer NOT NULL,
`token` text,
`merchant_order_id` text NOT NULL,
`payload` text,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `payments_merchant_order_id_unique` ON `payments` (`merchant_order_id`);--> statement-breakpoint
CREATE TABLE `product_categories` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`description` text
);
--> statement-breakpoint
CREATE TABLE `product_group_info` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`group_name` text NOT NULL,
`description` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `product_group_membership` (
`product_id` integer NOT NULL,
`group_id` integer NOT NULL,
`added_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
PRIMARY KEY(`product_id`, `group_id`),
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`group_id`) REFERENCES `product_group_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `product_info` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`short_description` text,
`long_description` text,
`unit_id` integer NOT NULL,
`price` text NOT NULL,
`market_price` text,
`images` text,
`is_out_of_stock` integer DEFAULT false NOT NULL,
`is_suspended` integer DEFAULT false NOT NULL,
`is_flash_available` integer DEFAULT false NOT NULL,
`flash_price` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`increment_step` real DEFAULT 1 NOT NULL,
`product_quantity` real DEFAULT 1 NOT NULL,
`store_id` integer,
FOREIGN KEY (`unit_id`) REFERENCES `units`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`store_id`) REFERENCES `store_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `product_reviews` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`product_id` integer NOT NULL,
`review_body` text NOT NULL,
`image_urls` text,
`review_time` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`ratings` real NOT NULL,
`admin_response` text,
`admin_response_images` text,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action,
CONSTRAINT "rating_check" CHECK("product_reviews"."ratings" >= 1 AND "product_reviews"."ratings" <= 5)
);
--> statement-breakpoint
CREATE TABLE `product_slots` (
`product_id` integer NOT NULL,
`slot_id` integer NOT NULL,
PRIMARY KEY(`product_id`, `slot_id`),
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`slot_id`) REFERENCES `delivery_slot_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `product_tag_info` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`tag_name` text NOT NULL,
`tag_description` text,
`image_url` text,
`is_dashboard_tag` integer DEFAULT false NOT NULL,
`related_stores` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `product_tag_info_tag_name_unique` ON `product_tag_info` (`tag_name`);--> statement-breakpoint
CREATE TABLE `product_tags` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`product_id` integer NOT NULL,
`tag_id` integer NOT NULL,
`assigned_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`tag_id`) REFERENCES `product_tag_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_product_tag` ON `product_tags` (`product_id`,`tag_id`);--> statement-breakpoint
CREATE TABLE `refunds` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`order_id` integer NOT NULL,
`refund_amount` text,
`refund_status` text DEFAULT 'none',
`merchant_refund_id` text,
`refund_processed_at` integer,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `reserved_coupons` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`secret_code` text NOT NULL,
`coupon_code` text NOT NULL,
`discount_percent` text,
`flat_discount` text,
`min_order` text,
`product_ids` text,
`max_value` text,
`valid_till` integer,
`max_limit_for_user` integer,
`exclusive_apply` integer DEFAULT false NOT NULL,
`is_redeemed` integer DEFAULT false NOT NULL,
`redeemed_by` integer,
`redeemed_at` integer,
`created_by` integer NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`redeemed_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`created_by`) REFERENCES `staff_users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `reserved_coupons_secret_code_unique` ON `reserved_coupons` (`secret_code`);--> statement-breakpoint
CREATE TABLE `special_deals` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`product_id` integer NOT NULL,
`quantity` text NOT NULL,
`price` text NOT NULL,
`valid_till` integer NOT NULL,
FOREIGN KEY (`product_id`) REFERENCES `product_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `staff_permissions` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`permission_name` text NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_permission_name` ON `staff_permissions` (`permission_name`);--> statement-breakpoint
CREATE TABLE `staff_role_permissions` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`staff_role_id` integer NOT NULL,
`staff_permission_id` integer NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`staff_role_id`) REFERENCES `staff_roles`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`staff_permission_id`) REFERENCES `staff_permissions`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_role_permission` ON `staff_role_permissions` (`staff_role_id`,`staff_permission_id`);--> statement-breakpoint
CREATE TABLE `staff_roles` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`role_name` text NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_role_name` ON `staff_roles` (`role_name`);--> statement-breakpoint
CREATE TABLE `staff_users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`password` text NOT NULL,
`staff_role_id` integer,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`staff_role_id`) REFERENCES `staff_roles`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `store_info` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`description` text,
`image_url` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`owner` integer NOT NULL,
FOREIGN KEY (`owner`) REFERENCES `staff_users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `units` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`short_notation` text NOT NULL,
`full_name` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_short_notation` ON `units` (`short_notation`);--> statement-breakpoint
CREATE TABLE `unlogged_user_tokens` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`token` text NOT NULL,
`added_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`last_verified` integer
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unlogged_user_tokens_token_unique` ON `unlogged_user_tokens` (`token`);--> statement-breakpoint
CREATE TABLE `upload_url_status` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`key` text NOT NULL,
`status` text DEFAULT 'pending' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `user_creds` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`user_password` text NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `user_details` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`bio` text,
`date_of_birth` integer,
`gender` text,
`occupation` text,
`profile_image` text,
`is_suspended` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_details_user_id_unique` ON `user_details` (`user_id`);--> statement-breakpoint
CREATE TABLE `user_incidents` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`order_id` integer,
`date_added` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`admin_comment` text,
`added_by` integer,
`negativity_score` integer,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`added_by`) REFERENCES `staff_users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `user_notifications` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`title` text NOT NULL,
`image_url` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`body` text NOT NULL,
`applicable_users` text
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text,
`email` text,
`mobile` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `unique_email` ON `users` (`email`);--> statement-breakpoint
CREATE TABLE `vendor_snippets` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`snippet_code` text NOT NULL,
`slot_id` integer,
`is_permanent` integer DEFAULT false NOT NULL,
`product_ids` text NOT NULL,
`valid_till` integer,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
FOREIGN KEY (`slot_id`) REFERENCES `delivery_slot_info`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `vendor_snippets_snippet_code_unique` ON `vendor_snippets` (`snippet_code`);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1774588140474,
"tag": "0000_nifty_sauron",
"breakpoints": true
}
]
}

View file

@ -3,7 +3,6 @@
// Re-export database connection // Re-export database connection
export { db, initDb } from './src/db/db_index' export { db, initDb } from './src/db/db_index'
// Re-export schema // Re-export schema
export * from './src/db/schema' export * from './src/db/schema'

View file

@ -1,7 +1,8 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { homeBanners } from '../db/schema' import { homeBanners, staffUsers } from '../db/schema'
import { eq, desc } from 'drizzle-orm' import { eq, desc } from 'drizzle-orm'
export interface Banner { export interface Banner {
id: number id: number
name: string name: string

View file

@ -487,7 +487,7 @@ export async function getUsersForCoupon(
}) })
return { return {
users: userList.map((user: typeof users.$inferSelect) => ({ users: userList.map((user) => ({
id: user.id, id: user.id,
name: user.name || 'Unknown', name: user.name || 'Unknown',
mobile: user.mobile, mobile: user.mobile,

View file

@ -143,7 +143,7 @@ export async function getOrderDetails(orderId: number): Promise<AdminOrderDetail
let couponData = null let couponData = null
if (couponUsageData.length > 0) { if (couponUsageData.length > 0) {
let totalDiscountAmount = 0 let totalDiscountAmount = 0
const orderTotal = parseFloat(orderData.totalAmount.toString()) const orderTotal = parseFloat((orderData.totalAmount ?? '0').toString())
for (const usage of couponUsageData) { for (const usage of couponUsageData) {
let discountAmount = 0 let discountAmount = 0

View file

@ -0,0 +1,29 @@
export function coerceDate(value: unknown): Date | null {
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value
}
if (value === null || value === undefined) {
return null
}
if (typeof value === 'number') {
const date = new Date(value)
return Number.isNaN(date.getTime()) ? null : date
}
if (typeof value === 'string') {
const parsed = Date.parse(value)
if (!Number.isNaN(parsed)) {
return new Date(parsed)
}
const asNumber = Number(value)
if (!Number.isNaN(asNumber)) {
const date = new Date(asNumber)
return Number.isNaN(date.getTime()) ? null : date
}
}
return null
}

View file

@ -20,6 +20,7 @@ import type {
UserOrderDetail, UserOrderDetail,
UserRecentProduct, UserRecentProduct,
} from '@packages/shared' } from '@packages/shared'
import { coerceDate } from '../lib/date'
export interface OrderItemInput { export interface OrderItemInput {
productId: number productId: number
@ -366,7 +367,7 @@ export async function getOrdersWithRelations(
offset: number, offset: number,
pageSize: number pageSize: number
): Promise<OrderWithRelations[]> { ): Promise<OrderWithRelations[]> {
return db.query.orders.findMany({ const ordersWithRelations = await db.query.orders.findMany({
where: eq(orders.userId, userId), where: eq(orders.userId, userId),
with: { with: {
orderItems: { orderItems: {
@ -407,10 +408,26 @@ export async function getOrdersWithRelations(
}, },
}, },
}, },
orderBy: (ordersTable: typeof orders) => [desc(ordersTable.createdAt)], orderBy: [desc(orders.createdAt)],
limit: pageSize, limit: pageSize,
offset: offset, offset: offset,
}) as Promise<OrderWithRelations[]> })
return ordersWithRelations.map((order) => {
const createdAt = coerceDate(order.createdAt) ?? new Date(0)
const slot = order.slot
? {
...order.slot,
deliveryTime: coerceDate(order.slot.deliveryTime) ?? new Date(0),
}
: null
return {
...order,
createdAt,
slot,
}
}) as OrderWithRelations[]
} }
export async function getOrderCount(userId: number): Promise<number> { export async function getOrderCount(userId: number): Promise<number> {
@ -477,7 +494,23 @@ export async function getOrderByIdWithRelations(
}, },
}) })
return order as OrderDetailWithRelations | null if (!order) {
return null
}
const createdAt = coerceDate(order.createdAt) ?? new Date(0)
const slot = order.slot
? {
...order.slot,
deliveryTime: coerceDate(order.slot.deliveryTime) ?? new Date(0),
}
: null
return {
...order,
createdAt,
slot,
} as OrderDetailWithRelations
} }
export async function getCouponUsageForOrder( export async function getCouponUsageForOrder(

0
packages/migrated.db Normal file
View file

View file

@ -0,0 +1,85 @@
# @packages/migrator
Database migration tool for moving data between PostgreSQL and SQLite.
## Setup
Install dependencies:
```bash
npm install
```
## Configuration
Edit `src/config.ts` directly to configure database settings:
```typescript
// PostgreSQL Configuration
export const postgresConfig = {
connectionString: 'postgresql://postgres:postgres@localhost:5432/freshyo',
ssl: false,
schema: 'public',
};
// SQLite Configuration
export const sqliteConfig = {
filename: './data/migrated.db',
};
// Migration Settings
export const migrationConfig = {
batchSize: 1000, // Rows per batch
truncateBeforeInsert: true, // Clear tables before migration
excludedTables: [], // Tables to skip
includedTables: [], // Tables to include (empty = all)
};
// Logging
export const logConfig = {
verbose: true,
logFile: './migration.log',
};
```
## Usage
### PostgreSQL to SQLite
Migrate data from PostgreSQL to SQLite:
```bash
npm run migrate:pg-to-sqlite
```
### SQLite to PostgreSQL
Migrate data from SQLite to PostgreSQL:
```bash
npm run migrate:sqlite-to-pg
```
### Full Cycle (Testing)
Run both migrations in sequence:
```bash
npm run migrate:full-cycle
```
## Features
- ✅ Automatic schema conversion between PostgreSQL and SQLite
- ✅ Batch processing for large datasets
- ✅ Type mapping between databases
- ✅ JSON/array handling
- ✅ Configurable table filtering
- ✅ Progress logging
- ✅ Transaction support
## Notes
- Arrays and JSON data are stored as TEXT in SQLite and parsed back when migrating to PostgreSQL
- Date/timestamps are stored as ISO strings in SQLite
- Foreign key constraints are enabled in SQLite
- Edit `src/config.ts` to change any settings

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
{
"name": "@packages/migrator",
"version": "1.0.0",
"description": "Database migration tool between PostgreSQL and SQLite",
"main": "index.ts",
"types": "index.ts",
"private": true,
"scripts": {
"migrate:pg-to-sqlite": "tsx src/postgresToSqlite/index.ts",
"migrate:sqlite-to-pg": "tsx src/sqliteToPostgres/index.ts",
"migrate:full-cycle": "npm run migrate:pg-to-sqlite && npm run migrate:sqlite-to-pg"
},
"dependencies": {
"better-sqlite3": "^12.1.0",
"dotenv": "^17.2.1",
"drizzle-orm": "^0.44.5",
"pg": "^8.16.3"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^24.5.2",
"@types/pg": "^8.15.5",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}

View file

@ -0,0 +1,37 @@
import fs from 'fs'
import path from 'path'
const inputPath = process.argv[2]
const outputPath = process.argv[3]
if (!inputPath || !outputPath) {
console.error('Usage: node generate-drop.js <input.sql> <output.sql>')
process.exit(1)
}
const input = fs.readFileSync(path.resolve(inputPath), 'utf8')
const tableRegex = /CREATE TABLE IF NOT EXISTS "([^"]+)"/g
const tables = []
let match
while ((match = tableRegex.exec(input)) !== null) {
tables.push(match[1])
}
const uniqueTables = Array.from(new Set(tables))
const drops = [
'PRAGMA foreign_keys=OFF;',
'BEGIN TRANSACTION;'
]
// Drop in reverse order of creation
for (const table of uniqueTables.reverse()) {
drops.push(`DROP TABLE IF EXISTS "${table}";`)
}
drops.push('COMMIT;')
fs.writeFileSync(path.resolve(outputPath), drops.join('\n') + '\n', 'utf8')
console.log(`Wrote ${outputPath} with ${uniqueTables.length} DROP statements`)

View file

@ -0,0 +1,21 @@
import fs from 'fs'
import path from 'path'
const inputPath = process.argv[2]
const outputPath = process.argv[3]
if (!inputPath || !outputPath) {
console.error('Usage: node strip-fk.js <input.sql> <output.sql>')
process.exit(1)
}
const input = fs.readFileSync(path.resolve(inputPath), 'utf8')
// Remove FOREIGN KEY clauses from CREATE TABLE statements
const output = input
.replace(/,?\s*FOREIGN KEY\s*\([^\)]*\)\s*REFERENCES\s*[^;\n]*?/g, '')
.replace(/\n\s*FOREIGN KEY\s*\([^\)]*\)\s*REFERENCES\s*[^;\n]*\n/gi, '\n')
fs.writeFileSync(path.resolve(outputPath), output, 'utf8')
console.log(`Wrote ${outputPath} without foreign keys`)

View file

@ -0,0 +1,37 @@
/**
* Database migration configuration
* Edit this file directly to configure migration settings
*/
// PostgreSQL Configuration
export const postgresConfig = {
connectionString: 'postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer',
ssl: false as boolean | { rejectUnauthorized: boolean },
schema: 'mf',
};
// SQLite Configuration
export const sqliteConfig = {
filename: './data/migrated.db',
};
// Migration Settings
export const migrationConfig = {
// Batch size for bulk inserts (to avoid memory issues)
batchSize: 1000,
// Enable/disable table truncation before migration
truncateBeforeInsert: true,
// Tables to exclude from migration
excludedTables: [] as string[],
// Tables to include (if empty, includes all)
includedTables: [] as string[],
};
// Logging
export const logConfig = {
verbose: true,
logFile: './migration.log',
};

View file

@ -0,0 +1,336 @@
import { Client } from 'pg';
import Database from 'better-sqlite3';
import { postgresConfig, sqliteConfig, migrationConfig, logConfig } from '../config';
import * as fs from 'fs';
import * as path from 'path';
interface TableInfo {
tableName: string;
columns: ColumnInfo[];
}
interface ColumnInfo {
name: string;
type: string;
isNullable: boolean;
defaultValue: string | null;
isPrimaryKey: boolean;
}
/**
* Maps PostgreSQL data types to SQLite data types
*/
function mapPostgresTypeToSqlite(pgType: string): string {
const typeMap: Record<string, string> = {
'bigint': 'INTEGER',
'bigserial': 'INTEGER',
'boolean': 'INTEGER',
'character': 'TEXT',
'character varying': 'TEXT',
'date': 'TEXT',
'double precision': 'REAL',
'integer': 'INTEGER',
'json': 'TEXT',
'jsonb': 'TEXT',
'numeric': 'REAL',
'real': 'REAL',
'serial': 'INTEGER',
'smallint': 'INTEGER',
'text': 'TEXT',
'timestamp with time zone': 'TEXT',
'timestamp without time zone': 'TEXT',
'uuid': 'TEXT',
'ARRAY': 'TEXT', // Arrays stored as JSON text
};
// Check for array types
if (pgType.endsWith('[]')) {
return 'TEXT';
}
return typeMap[pgType] || 'TEXT';
}
/**
* Gets all table names from PostgreSQL
*/
async function getPostgresTables(client: Client): Promise<string[]> {
const result = await client.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = $1
AND table_type = 'BASE TABLE'
ORDER BY table_name
`, [postgresConfig.schema]);
return result.rows.map(row => row.table_name);
}
/**
* Gets column information for a specific table
*/
async function getTableColumns(client: Client, tableName: string): Promise<ColumnInfo[]> {
const pkResult = await client.query(
`
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = $1
AND tc.table_name = $2
ORDER BY kcu.ordinal_position
`,
[postgresConfig.schema, tableName]
)
const primaryKeys = new Set(pkResult.rows.map(row => row.column_name))
const result = await client.query(
`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = $2
ORDER BY ordinal_position
`,
[tableName, postgresConfig.schema]
)
return result.rows.map(row => ({
name: row.column_name,
type: row.data_type,
isNullable: row.is_nullable === 'YES',
defaultValue: row.column_default,
isPrimaryKey: primaryKeys.has(row.column_name),
}))
}
/**
* Creates SQLite table based on PostgreSQL schema
*/
function createSqliteTable(db: Database.Database, tableName: string, columns: ColumnInfo[]): void {
const primaryKeyColumns = columns.filter(col => col.isPrimaryKey).map(col => col.name)
const columnDefs = columns.map(col => {
const mappedType = mapPostgresTypeToSqlite(col.type)
const isSinglePk = primaryKeyColumns.length === 1 && col.isPrimaryKey
if (isSinglePk && mappedType === 'INTEGER') {
return `"${col.name}" INTEGER PRIMARY KEY`
}
let def = `"${col.name}" ${mappedType}`
if (!col.isNullable && !col.isPrimaryKey) {
def += ' NOT NULL'
}
if (col.defaultValue !== null && !col.isPrimaryKey) {
// Convert PostgreSQL default values to SQLite
let defaultVal = col.defaultValue
if (defaultVal.includes('nextval')) {
// Skip auto-increment defaults, SQLite handles this with INTEGER PRIMARY KEY
} else if (defaultVal === 'now()' || defaultVal.includes('CURRENT_TIMESTAMP')) {
def += ` DEFAULT CURRENT_TIMESTAMP`
} else {
// Strip Postgres type casts (e.g. 'pending'::payment_status)
defaultVal = defaultVal.replace(/::[\w\."]+/g, '')
// Remove remaining type keywords from defaults (e.g. 'none' varying)
defaultVal = defaultVal.replace(/\s+character\s+varying\b/gi, '')
defaultVal = defaultVal.replace(/\s+varying\b/gi, '')
defaultVal = defaultVal.trim()
// Convert Postgres array literal defaults like '{}'[] to JSON array string
if (defaultVal.includes('[]') && defaultVal.includes('{')) {
defaultVal = "'[]'"
}
def += ` DEFAULT ${defaultVal}`
}
}
return def
})
if (primaryKeyColumns.length > 1) {
const pkDef = `PRIMARY KEY (${primaryKeyColumns.map(col => `"${col}"`).join(', ')})`
columnDefs.push(pkDef)
}
const createSql = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs.join(', ')})`;
if (logConfig.verbose) {
console.log(`Creating table: ${tableName}`);
console.log(createSql);
}
db.exec(createSql);
}
/**
* Migrates data from PostgreSQL to SQLite
*/
async function migrateTableData(
pgClient: Client,
sqliteDb: Database.Database,
tableName: string,
columns: ColumnInfo[]
): Promise<number> {
// Check if table should be excluded
if (migrationConfig.excludedTables.includes(tableName)) {
console.log(`Skipping excluded table: ${tableName}`);
return 0;
}
// Check if only specific tables should be included
if (migrationConfig.includedTables.length > 0 && !migrationConfig.includedTables.includes(tableName)) {
console.log(`Skipping table not in include list: ${tableName}`);
return 0;
}
console.log(`Migrating table: ${tableName}`);
// Get total count first
const countResult = await pgClient.query(
`SELECT COUNT(*) FROM "${postgresConfig.schema}"."${tableName}"`
);
const totalRows = parseInt(countResult.rows[0].count);
console.log(` Total rows to migrate: ${totalRows}`);
if (totalRows === 0) {
console.log(` No data to migrate`);
return 0;
}
// Clear existing data if configured
if (migrationConfig.truncateBeforeInsert) {
sqliteDb.exec(`DELETE FROM "${tableName}"`);
console.log(` Cleared existing data`);
}
// Get column names
const columnNames = columns.map(c => `"${c.name}"`).join(', ');
const placeholders = columns.map(() => '?').join(', ');
const insertStmt = sqliteDb.prepare(`INSERT INTO "${tableName}" (${columnNames}) VALUES (${placeholders})`);
let migratedCount = 0;
let offset = 0;
while (offset < totalRows) {
const result = await pgClient.query(
`SELECT * FROM "${postgresConfig.schema}"."${tableName}" ORDER BY 1 LIMIT $1 OFFSET $2`,
[migrationConfig.batchSize, offset]
);
const insertMany = sqliteDb.transaction((rows) => {
for (const row of rows) {
const values = columns.map(col => {
const val = row[col.name];
// Handle special cases
if (val === null || val === undefined) return null;
if (typeof val === 'boolean') return val ? 1 : 0;
if (val instanceof Date) return val.toISOString();
if (Array.isArray(val)) return JSON.stringify(val);
if (typeof val === 'object') return JSON.stringify(val);
// Ensure it's a primitive type SQLite can handle
if (typeof val === 'number') return val;
if (typeof val === 'bigint') return Number(val);
return String(val);
});
insertStmt.run(values);
}
});
insertMany(result.rows);
migratedCount += result.rows.length;
offset += migrationConfig.batchSize;
if (logConfig.verbose || offset % 10000 === 0) {
console.log(` Progress: ${migratedCount}/${totalRows} rows`);
}
}
console.log(` Completed: ${migratedCount} rows migrated`);
return migratedCount;
}
/**
* Main migration function
*/
async function migratePostgresToSqlite(): Promise<void> {
console.log('Starting PostgreSQL to SQLite migration...\n');
// Ensure SQLite directory exists
const sqliteDir = path.dirname(sqliteConfig.filename);
if (!fs.existsSync(sqliteDir)) {
fs.mkdirSync(sqliteDir, { recursive: true });
}
// Remove existing SQLite file if starting fresh
if (migrationConfig.truncateBeforeInsert && fs.existsSync(sqliteConfig.filename)) {
fs.unlinkSync(sqliteConfig.filename);
console.log('Removed existing SQLite database');
}
// Connect to PostgreSQL
const pgClient = new Client(postgresConfig);
await pgClient.connect();
console.log('Connected to PostgreSQL');
// Connect to SQLite
const sqliteDb = new Database(sqliteConfig.filename);
console.log('Connected to SQLite');
// Enable foreign keys
sqliteDb.exec('PRAGMA foreign_keys = ON');
try {
// Get all tables
const tables = await getPostgresTables(pgClient);
console.log(`\nFound ${tables.length} tables to migrate\n`);
let totalMigrated = 0;
// Migrate each table
for (const tableName of tables) {
try {
const columns = await getTableColumns(pgClient, tableName);
// Create table in SQLite
createSqliteTable(sqliteDb, tableName, columns);
// Migrate data
const migrated = await migrateTableData(pgClient, sqliteDb, tableName, columns);
totalMigrated += migrated;
console.log('');
} catch (error) {
console.error(`Error migrating table ${tableName}:`, error);
if (logConfig.verbose) {
throw error;
}
}
}
console.log('=================================');
console.log('Migration completed successfully!');
console.log(`Total rows migrated: ${totalMigrated}`);
console.log(`SQLite database: ${sqliteConfig.filename}`);
console.log('=================================');
} catch (error) {
console.error('Migration failed:', error);
throw error;
} finally {
await pgClient.end();
sqliteDb.close();
}
}
// Run migration if called directly
if (require.main === module) {
migratePostgresToSqlite().catch(console.error);
}
export { migratePostgresToSqlite };

View file

@ -0,0 +1,273 @@
import { Client } from 'pg';
import Database from 'better-sqlite3';
import { postgresConfig, sqliteConfig, migrationConfig, logConfig } from '../config';
interface TableInfo {
tableName: string;
columns: ColumnInfo[];
}
interface ColumnInfo {
name: string;
type: string;
notNull: boolean;
defaultValue: string | null;
primaryKey: boolean;
}
/**
* Maps SQLite data types back to PostgreSQL data types
* This is a best-effort mapping and may need adjustment based on specific use cases
*/
function mapSqliteTypeToPostgres(sqliteType: string): string {
const typeMap: Record<string, string> = {
'INTEGER': 'INTEGER',
'REAL': 'DOUBLE PRECISION',
'TEXT': 'TEXT',
'BLOB': 'BYTEA',
'NUMERIC': 'NUMERIC',
};
return typeMap[sqliteType.toUpperCase()] || 'TEXT';
}
/**
* Gets all table names from SQLite
*/
function getSqliteTables(db: Database.Database): string[] {
const result = db.prepare(`
SELECT name FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
ORDER BY name
`).all() as { name: string }[];
return result.map(row => row.name);
}
/**
* Gets column information for a specific table from SQLite
*/
function getSqliteTableColumns(db: Database.Database, tableName: string): ColumnInfo[] {
const result = db.prepare(`PRAGMA table_info("${tableName}")`).all() as any[];
return result.map(row => ({
name: row.name,
type: row.type,
notNull: row.notnull === 1,
defaultValue: row.dflt_value,
primaryKey: row.pk === 1,
}));
}
/**
* Creates PostgreSQL table based on SQLite schema
*/
async function createPostgresTable(
pgClient: Client,
tableName: string,
columns: ColumnInfo[]
): Promise<void> {
// Check if table exists
const existsResult = await pgClient.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = $1
AND table_name = $2
)
`, [postgresConfig.schema, tableName]);
const tableExists = existsResult.rows[0].exists;
if (tableExists && migrationConfig.truncateBeforeInsert) {
// Drop existing table to recreate
await pgClient.query(`DROP TABLE IF EXISTS "${postgresConfig.schema}"."${tableName}" CASCADE`);
console.log(` Dropped existing table: ${tableName}`);
} else if (tableExists) {
console.log(` Table already exists, will append data: ${tableName}`);
return;
}
const columnDefs = columns.map(col => {
let def = `"${col.name}" ${mapSqliteTypeToPostgres(col.type)}`;
if (col.notNull) {
def += ' NOT NULL';
}
if (col.primaryKey) {
def += ' PRIMARY KEY';
}
if (col.defaultValue !== null) {
def += ` DEFAULT ${col.defaultValue}`;
}
return def;
}).join(', ');
const createSql = `CREATE TABLE "${postgresConfig.schema}"."${tableName}" (${columnDefs})`;
if (logConfig.verbose) {
console.log(`Creating table: ${tableName}`);
console.log(createSql);
}
await pgClient.query(createSql);
}
/**
* Attempts to parse JSON values that were stored as TEXT in SQLite
*/
function parseValue(value: any, columnName: string): any {
if (value === null) return null;
if (typeof value !== 'string') return value;
// Try to parse as JSON (for arrays/objects that were stringified)
if ((value.startsWith('[') && value.endsWith(']')) ||
(value.startsWith('{') && value.endsWith('}'))) {
try {
return JSON.parse(value);
} catch {
return value;
}
}
return value;
}
/**
* Migrates data from SQLite to PostgreSQL
*/
async function migrateTableData(
sqliteDb: Database.Database,
pgClient: Client,
tableName: string,
columns: ColumnInfo[]
): Promise<number> {
// Check if table should be excluded
if (migrationConfig.excludedTables.includes(tableName)) {
console.log(`Skipping excluded table: ${tableName}`);
return 0;
}
// Check if only specific tables should be included
if (migrationConfig.includedTables.length > 0 && !migrationConfig.includedTables.includes(tableName)) {
console.log(`Skipping table not in include list: ${tableName}`);
return 0;
}
console.log(`Migrating table: ${tableName}`);
// Get total count
const countResult = sqliteDb.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get() as { count: number };
const totalRows = countResult.count;
console.log(` Total rows to migrate: ${totalRows}`);
if (totalRows === 0) {
console.log(` No data to migrate`);
return 0;
}
// Clear existing data if configured (and table wasn't just created)
if (migrationConfig.truncateBeforeInsert) {
await pgClient.query(`DELETE FROM "${postgresConfig.schema}"."${tableName}"`);
console.log(` Cleared existing data`);
}
// Get column names
const columnNames = columns.map(c => `"${c.name}"`).join(', ');
const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
let migratedCount = 0;
let offset = 0;
while (offset < totalRows) {
const rows = sqliteDb.prepare(`
SELECT * FROM "${tableName}"
ORDER BY ROWID
LIMIT ? OFFSET ?
`).all(migrationConfig.batchSize, offset) as any[];
for (const row of rows) {
const values = columns.map(col => parseValue(row[col.name], col.name));
await pgClient.query(
`INSERT INTO "${postgresConfig.schema}"."${tableName}" (${columnNames}) VALUES (${placeholders})`,
values
);
}
migratedCount += rows.length;
offset += migrationConfig.batchSize;
if (logConfig.verbose || offset % 10000 === 0) {
console.log(` Progress: ${migratedCount}/${totalRows} rows`);
}
}
console.log(` Completed: ${migratedCount} rows migrated`);
return migratedCount;
}
/**
* Main migration function
*/
async function migrateSqliteToPostgres(): Promise<void> {
console.log('Starting SQLite to PostgreSQL migration...\n');
// Connect to SQLite
const sqliteDb = new Database(sqliteConfig.filename);
console.log('Connected to SQLite');
// Connect to PostgreSQL
const pgClient = new Client(postgresConfig);
await pgClient.connect();
console.log('Connected to PostgreSQL');
try {
// Get all tables
const tables = getSqliteTables(sqliteDb);
console.log(`\nFound ${tables.length} tables to migrate\n`);
let totalMigrated = 0;
// Migrate each table
for (const tableName of tables) {
try {
const columns = getSqliteTableColumns(sqliteDb, tableName);
// Create table in PostgreSQL
await createPostgresTable(pgClient, tableName, columns);
// Migrate data
const migrated = await migrateTableData(sqliteDb, pgClient, tableName, columns);
totalMigrated += migrated;
console.log('');
} catch (error) {
console.error(`Error migrating table ${tableName}:`, error);
if (logConfig.verbose) {
throw error;
}
}
}
console.log('=================================');
console.log('Migration completed successfully!');
console.log(`Total rows migrated: ${totalMigrated}`);
console.log(`Source: ${sqliteConfig.filename}`);
console.log(`Target: PostgreSQL`);
console.log('=================================');
} catch (error) {
console.error('Migration failed:', error);
throw error;
} finally {
sqliteDb.close();
await pgClient.end();
}
}
// Run migration if called directly
if (require.main === module) {
migrateSqliteToPostgres().catch(console.error);
}
export { migrateSqliteToPostgres };

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": ".",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*", "index.ts"],
"exclude": ["node_modules", "dist"]
}

View file

@ -64,7 +64,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
// const BASE_API_URL = API_URL; // const BASE_API_URL = API_URL;
// const BASE_API_URL = 'http://10.0.2.2:4000'; // const BASE_API_URL = 'http://10.0.2.2:4000';
// const BASE_API_URL = 'http://192.168.100.101:4000'; // const BASE_API_URL = 'http://192.168.100.101:4000';
const BASE_API_URL = 'http://192.168.1.5:4000'; // const BASE_API_URL = 'http://192.168.1.5:4000';
const BASE_API_URL = 'http://192.168.1.5:8787';
// let BASE_API_URL = "https://mf.freshyo.in"; // let BASE_API_URL = "https://mf.freshyo.in";
// let BASE_API_URL = "https://freshyo.technocracy.ovh"; // let BASE_API_URL = "https://freshyo.technocracy.ovh";
// let BASE_API_URL = 'http://192.168.100.107:4000'; // let BASE_API_URL = 'http://192.168.100.107:4000';

View file

@ -1,183 +0,0 @@
# Progress Summary - Meat Farmer Monorepo
## User UI Development Progress
### Profile Image & User Details System
#### Database Schema Updates
- **Added `user_details` table** with fields: bio, dateOfBirth, gender, occupation, profileImage
- **Established one-to-one relationship** between users and user_details tables
- **Updated relations** in schema for proper data fetching
#### Backend API Enhancements
- **Updated auth controller** (`login`, `register`, `getProfile`) to query userDetails and return complete user information
- **Added `updateProfile` API** with comprehensive validation and transaction-based updates
- **Implemented signed URLs** for secure profile image access (3-day expiration)
- **Enhanced tRPC `getSelfData`** to include all user details with signed URLs
#### Frontend Auth System
- **Extended AuthContext** with `userDetails` state and `updateUserDetails` function
- **Modified tRPC queries** for fresh data on every app startup (no caching)
- **Added `useUserDetails` hook** for accessing detailed user information
- **Updated login/register flows** to populate complete user data
### Edit Profile Functionality
#### Page Implementation
- **Created edit-profile page** with pre-populated form values
- **Conditional form rendering** - password fields hidden in edit mode
- **Profile image upload** support with existing image display
- **Form validation** adjusted for edit vs registration modes
#### API Integration
- **Added `useUpdateProfile` hook** using React Query for profile updates
- **Multipart form data handling** for profile image uploads
- **Error handling and loading states** with proper user feedback
- **Context synchronization** after successful profile updates
### UI/UX Improvements
#### Drawer Layout Enhancements
- **Profile image display** in drawer header with fallback to person icon
- **User details integration** showing name and mobile from userDetails
- **Circular avatar styling** for profile images
#### Me Page Redesign
- **2x2 grid layout** replacing vertical button list
- **MaterialIcons integration** with relevant icons for each section:
- Orders: `shopping-bag` (blue)
- Complaints: `report-problem` (green)
- Coupons: `local-offer` (purple)
- Profile: `person` (orange)
- **Enhanced styling** with rounded corners, shadows, and better typography
- **Responsive design** with proper spacing and touch targets
### Registration Form Updates
- **Edit mode support** with `isEdit` prop
- **Conditional field rendering** (passwords/terms hidden in edit mode)
- **Initial values support** for pre-populating form data
- **Profile image handling** for both new uploads and existing images
## Files Modified
### Backend
- `apps/backend/src/db/schema.ts` - Added user_details table, vendor_snippets table, and relations definitions
- `apps/backend/src/uv-apis/auth.controller.ts` - Enhanced auth APIs with userDetails and signed URLs
- `apps/backend/src/uv-apis/auth.router.ts` - Added update profile route
- `apps/backend/src/trpc/user-apis/user.ts` - Updated getSelfData with userDetails
- `apps/backend/src/trpc/admin-apis/vendor-snippets.ts` - Complete CRUD API for vendor snippets management
### Frontend
- `apps/user-ui/src/types/auth.ts` - Added UserDetails interface and updated AuthState
- `apps/user-ui/src/contexts/AuthContext.tsx` - Enhanced with userDetails state and hooks
- `apps/user-ui/src/api-hooks/auth.api.ts` - Added updateProfile API and hook
- `apps/user-ui/components/registration-form.tsx` - Added edit mode support
- `apps/user-ui/app/(drawer)/edit-profile/index.tsx` - New edit profile page
- `apps/user-ui/app/(drawer)/_layout.tsx` - Updated drawer with profile image
- `apps/user-ui/app/(drawer)/me/index.tsx` - Redesigned with 2x2 grid and icons
### Admin UI (New Vendor Snippets Feature)
- `apps/admin-ui/app/(drawer)/vendor-snippets/index.tsx` - Main vendor snippets management page
- `apps/admin-ui/app/(drawer)/_layout.tsx` - Added vendor snippets to drawer navigation
- `apps/admin-ui/components/VendorSnippetForm.tsx` - Create/edit form with validation
- `apps/admin-ui/components/SnippetOrdersView.tsx` - Orders viewing component with matching highlights
- `apps/admin-ui/src/api-hooks/vendor-snippets.api.ts` - tRPC hooks for vendor snippets operations
- `apps/admin-ui/src/trpc-client.ts` - Updated imports for tRPC client usage
## Key Features Implemented
### User UI Features
**Complete user profile system** with detailed information storage
**Secure image handling** with signed URLs and S3 integration
**Edit profile functionality** with pre-populated forms and validation
**Beautiful UI components** with icons and modern design patterns
**Fresh data fetching** on app startup with no caching
**Transaction-safe updates** with proper error handling
**Responsive grid layouts** optimized for mobile experience
### Admin UI Features (Vendor Snippets)
**Complete vendor snippets management system** with full CRUD operations
**Advanced order matching logic** that finds orders by slot and product criteria
**Interactive forms** with slot/product selection and validation
**Orders viewing interface** with product matching highlights and statistics
**Automatic data refresh** using focus callbacks for fresh data
**Proper relations handling** in Drizzle ORM with foreign key relationships
**Error handling and loading states** throughout the user journey
**Navigation integration** with drawer menu and proper routing
## Admin UI Changes
### Vendor Snippets Management System
#### Database Schema Updates
- **Added `vendor_snippets` table** with fields: id, snippetCode, slotId, productIds, validTill, createdAt
- **Established foreign key relationship** between vendorSnippets and deliverySlotInfo tables
- **Added relations definition** (`vendorSnippetsRelations`) for proper Drizzle ORM queries
- **Array field support** for storing multiple product IDs per snippet
#### Backend API Implementation
- **Complete CRUD operations** for vendor snippets:
- `create`: Validates slot/product existence, prevents duplicate codes
- `getAll`: Returns snippets with slot relations, ordered by creation date
- `getById`: Fetches individual snippet with slot details
- `update`: Partial updates with validation and uniqueness checks
- `delete`: Soft delete by setting expiry to current time
- **`getOrdersBySnippet` API**: Advanced order matching logic that:
- Finds orders with matching delivery slots
- Filters orders containing at least one snippet product
- Returns formatted order data with product matching highlights
- Includes customer details, pricing, and delivery information
#### Admin UI Implementation
- **Vendor Snippets List Page**: Complete management interface with:
- Snippet cards showing code, slot info, product count, expiry dates
- Action buttons for View Orders, Edit, and Delete operations
- Empty state with call-to-action for first snippet creation
- Loading states and error handling
- **Create/Edit Forms**: Comprehensive form components using:
- BottomDropdown for slot selection (replacing custom dropdowns)
- MultiSelectDropdown for product selection with search
- DatePicker for expiry date management
- Form validation with real-time error feedback
- Auto-generated snippet codes for new entries
#### Orders Viewing System
- **SnippetOrdersView Component**: Dedicated screen for viewing matched orders:
- Order cards with customer details, amounts, and delivery slots
- Product lists with matching highlights (⭐ indicators)
- Summary statistics (total orders, revenue)
- Responsive design with proper spacing and typography
#### Navigation & UX Enhancements
- **Drawer Integration**: Added "Vendor Snippets" to admin navigation menu
- **Focus-based Refetching**: Implemented `useFocusCallback` for automatic data refresh when returning to the screen
- **Error Handling**: Fixed tRPC client vs hooks usage (`trpcClient` for direct queries)
- **Loading States**: Proper loading indicators and user feedback throughout the flow
#### Technical Improvements
- **Relations Fix**: Resolved Drizzle ORM error by adding missing relations definition
- **API Client Usage**: Corrected tRPC usage patterns (hooks vs direct client)
- **Type Safety**: Proper TypeScript interfaces and error handling
- **Performance**: Efficient queries with proper indexing and filtering
### Previous Admin UI Changes
#### Slot Selection Centralization
- **Moved slot dropdown** from individual pages to Manage Orders hub page
- **Updated navigation** with slotId query parameters
- **Streamlined child pages** with URL param reading
#### UI Cleanup & Improvements
- **Removed redundant elements** from drawer navigation
- **Compacted order displays** for better space utilization
- **Enhanced delivery sequences** layout
## Important Notes
- **Do not run build, compile, or migration commands** - These should be handled manually by developers
- Avoid running `npm run build`, `tsc`, `drizzle-kit generate`, or similar compilation/migration commands
- Schema changes should be committed and migrations generated manually
- **Signed URLs** are used for secure image access with 3-day expiration
- **React Query** handles all API state management with proper loading/error states
- **Vendor Snippets**: Relations definitions are critical for Drizzle ORM queries - always define relations for foreign key relationships
- **tRPC Usage**: Use `trpc` for React hooks and `trpcClient` for direct API calls outside components
- **Focus Callbacks**: Implemented for automatic data refresh when screens regain focus