Compare commits
No commits in common. "7432f8dfd536b58bb99510ff2f706055802c29de" and "1b042819af371b8d0f080da20323bb0c6dea2d4f" have entirely different histories.
7432f8dfd5
...
1b042819af
67 changed files with 1237 additions and 401065 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# Agent Instructions for Meat Farmer Monorepo
|
||||
|
||||
## 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.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
|
@ -47,4 +48,6 @@ react-native. They are available in the common-ui as MyText, MyTextInput, MyTouc
|
|||
- Database: Drizzle ORM with PostgreSQL
|
||||
|
||||
## 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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
// 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;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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
|
|
@ -1,48 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
import 'dotenv/config';
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
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 { createApp } from '@/src/app'
|
||||
// import signedUrlCache from '@/src/lib/signed-url-cache';
|
||||
import { appRouter } from '@/src/trpc/router';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
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 '@/src/jobs/jobs-index';
|
||||
import { startAutomatedJobs } from '@/src/lib/automatedJobs';
|
||||
|
|
@ -11,9 +20,120 @@ seed()
|
|||
initFunc()
|
||||
startAutomatedJobs()
|
||||
|
||||
// signedUrlCache.loadFromDisk(); // Disabled for Workers compatibility
|
||||
const app = new Hono();
|
||||
|
||||
const app = createApp()
|
||||
// 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,
|
||||
}));
|
||||
|
||||
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({
|
||||
fetch: app.fetch,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -10,8 +10,6 @@
|
|||
"dev2": "tsx watch index.ts",
|
||||
"dev_node": "tsx 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:push": "docker push mohdshafiuddin54/health_petal:latest"
|
||||
},
|
||||
|
|
@ -38,13 +36,11 @@
|
|||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20260304.0",
|
||||
"@types/node": "^24.5.2",
|
||||
"rimraf": "^6.1.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^3.114.0"
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
236
apps/backend/src/apis/admin-apis/apis/product.controller.ts
Normal file
236
apps/backend/src/apis/admin-apis/apis/product.controller.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
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);
|
||||
};
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -2,15 +2,10 @@
|
|||
// This file re-exports everything from postgresImporter to provide a clean abstraction layer
|
||||
|
||||
import type { AdminOrderDetails } from '@packages/shared'
|
||||
// import { getOrderDetails } from '@/src/postgresImporter'
|
||||
import { getOrderDetails, initDb } from '@/src/sqliteImporter'
|
||||
import { getOrderDetails } from '@/src/postgresImporter'
|
||||
|
||||
// Re-export everything from postgresImporter
|
||||
// export * from '@/src/postgresImporter'
|
||||
|
||||
export * from '@/src/sqliteImporter'
|
||||
|
||||
export { initDb }
|
||||
export * from '@/src/postgresImporter'
|
||||
|
||||
// Re-export getOrderDetails with the correct signature
|
||||
export async function getOrderDetailsWrapper(orderId: number): Promise<AdminOrderDetails | null> {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ export class ApiError extends Error {
|
|||
this.name = 'ApiError';
|
||||
this.statusCode = statusCode;
|
||||
this.details = details;
|
||||
// Error.captureStackTrace?.(this, ApiError);
|
||||
Error.captureStackTrace?.(this, ApiError);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +1,57 @@
|
|||
|
||||
// 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 appUrl = process.env.APP_URL 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 jwtSecret: string = process.env.JWT_SECRET as string
|
||||
|
||||
export const defaultRoleName = 'gen_user';
|
||||
|
||||
export const getEncodedJwtSecret = () => {
|
||||
const env = getRuntimeEnv()
|
||||
const secret = (env.JWT_SECRET as string) || ''
|
||||
return new TextEncoder().encode(secret)
|
||||
}
|
||||
export const encodedJwtSecret = new TextEncoder().encode(jwtSecret)
|
||||
|
||||
export const s3AccessKeyId = runtimeEnv.S3_ACCESS_KEY_ID as string
|
||||
export const s3AccessKeyId = process.env.S3_ACCESS_KEY_ID as string
|
||||
|
||||
export const s3SecretAccessKey = runtimeEnv.S3_SECRET_ACCESS_KEY as string
|
||||
export const s3SecretAccessKey = process.env.S3_SECRET_ACCESS_KEY as string
|
||||
|
||||
export const s3BucketName = runtimeEnv.S3_BUCKET_NAME as string
|
||||
export const s3BucketName = process.env.S3_BUCKET_NAME as string
|
||||
|
||||
export const s3Region = runtimeEnv.S3_REGION as string
|
||||
export const s3Region = process.env.S3_REGION as string
|
||||
|
||||
export const assetsDomain = runtimeEnv.ASSETS_DOMAIN as string
|
||||
export const assetsDomain = process.env.ASSETS_DOMAIN as string;
|
||||
|
||||
export const apiCacheKey = runtimeEnv.API_CACHE_KEY as string
|
||||
export const apiCacheKey = process.env.API_CACHE_KEY as string;
|
||||
|
||||
export const cloudflareApiToken = runtimeEnv.CLOUDFLARE_API_TOKEN as string
|
||||
export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string;
|
||||
|
||||
export const cloudflareZoneId = runtimeEnv.CLOUDFLARE_ZONE_ID as string
|
||||
export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string;
|
||||
|
||||
export const s3Url = runtimeEnv.S3_URL as string
|
||||
export const s3Url = process.env.S3_URL as string
|
||||
|
||||
export const redisUrl = runtimeEnv.REDIS_URL as string
|
||||
export const redisUrl = process.env.REDIS_URL as string
|
||||
|
||||
export const expoAccessToken = runtimeEnv.EXPO_ACCESS_TOKEN as string
|
||||
|
||||
export const phonePeBaseUrl = runtimeEnv.PHONE_PE_BASE_URL as string
|
||||
export const expoAccessToken = process.env.EXPO_ACCESS_TOKEN as string;
|
||||
|
||||
export const phonePeClientId = runtimeEnv.PHONE_PE_CLIENT_ID as string
|
||||
export const phonePeBaseUrl = process.env.PHONE_PE_BASE_URL as string;
|
||||
|
||||
export const phonePeClientVersion = Number(runtimeEnv.PHONE_PE_CLIENT_VERSION as string)
|
||||
export const phonePeClientId = process.env.PHONE_PE_CLIENT_ID as string;
|
||||
|
||||
export const phonePeClientSecret = runtimeEnv.PHONE_PE_CLIENT_SECRET as string
|
||||
export const phonePeClientVersion = Number(process.env.PHONE_PE_CLIENT_VERSION as string);
|
||||
|
||||
export const phonePeMerchantId = runtimeEnv.PHONE_PE_MERCHANT_ID as string
|
||||
export const phonePeClientSecret = process.env.PHONE_PE_CLIENT_SECRET as string;
|
||||
|
||||
export const razorpayId = runtimeEnv.RAZORPAY_KEY as string
|
||||
export const phonePeMerchantId = process.env.PHONE_PE_MERCHANT_ID as string;
|
||||
|
||||
export const razorpaySecret = runtimeEnv.RAZORPAY_SECRET as string
|
||||
export const razorpayId = process.env.RAZORPAY_KEY as string;
|
||||
|
||||
export const otpSenderAuthToken = runtimeEnv.OTP_SENDER_AUTH_TOKEN as string
|
||||
export const razorpaySecret = process.env.RAZORPAY_SECRET as string;
|
||||
|
||||
export const minOrderValue = Number(runtimeEnv.MIN_ORDER_VALUE as string)
|
||||
export const otpSenderAuthToken = process.env.OTP_SENDER_AUTH_TOKEN as string;
|
||||
|
||||
export const deliveryCharge = Number(runtimeEnv.DELIVERY_CHARGE as string)
|
||||
export const minOrderValue = Number(process.env.MIN_ORDER_VALUE as string);
|
||||
|
||||
export const telegramBotToken = runtimeEnv.TELEGRAM_BOT_TOKEN as string
|
||||
export const deliveryCharge = Number(process.env.DELIVERY_CHARGE as string);
|
||||
|
||||
export const telegramChatIds = (runtimeEnv.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || []
|
||||
export const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN as string;
|
||||
|
||||
export const isDevMode = (runtimeEnv.ENV_MODE as string) === 'dev'
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// import { Queue, Worker } from 'bullmq';
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import { Expo } from 'expo-server-sdk';
|
||||
import { redisUrl } from '@/src/lib/env-exporter'
|
||||
// import { db } from '@/src/db/db_index'
|
||||
|
|
@ -14,35 +14,31 @@ import {
|
|||
REFUND_INITIATED_MESSAGE
|
||||
} from '@/src/lib/const-strings';
|
||||
|
||||
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
||||
connection: { url: redisUrl },
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 10,
|
||||
attempts: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export const notificationQueue:any = {};
|
||||
export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
|
||||
if (!job) return;
|
||||
|
||||
// export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
||||
// connection: { url: redisUrl },
|
||||
// defaultJobOptions: {
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: 10,
|
||||
// attempts: 3,
|
||||
// },
|
||||
// });
|
||||
const { name, data } = job;
|
||||
console.log(`Processing notification job ${job.id} - ${name}`);
|
||||
|
||||
export const notificationWorker:any = {};
|
||||
// export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
|
||||
// if (!job) return;
|
||||
//
|
||||
// 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,
|
||||
// });
|
||||
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: {
|
||||
token: string;
|
||||
|
|
@ -88,12 +84,12 @@ async function sendAdminNotification(data: {
|
|||
}
|
||||
}
|
||||
|
||||
// notificationWorker.on('completed', (job) => {
|
||||
// if (job) console.log(`Notification job ${job.id} completed`);
|
||||
// });
|
||||
// notificationWorker.on('failed', (job, err) => {
|
||||
// if (job) console.error(`Notification job ${job.id} failed:`, err);
|
||||
// });
|
||||
notificationWorker.on('completed', (job) => {
|
||||
if (job) console.log(`Notification job ${job.id} completed`);
|
||||
});
|
||||
notificationWorker.on('failed', (job, err) => {
|
||||
if (job) console.error(`Notification job ${job.id} failed:`, err);
|
||||
});
|
||||
|
||||
export async function scheduleNotification(userId: number, payload: any, options?: { delay?: number; priority?: number }) {
|
||||
const jobData = { userId, ...payload };
|
||||
|
|
@ -163,8 +159,8 @@ export async function sendRefundInitiatedNotification(userId: number, orderId?:
|
|||
orderId
|
||||
});
|
||||
}
|
||||
//
|
||||
// process.on('SIGTERM', async () => {
|
||||
// await notificationQueue.close();
|
||||
// await notificationWorker.close();
|
||||
// });
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await notificationQueue.close();
|
||||
await notificationWorker.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,23 +21,23 @@ class RedisClient {
|
|||
// console.error('Redis Client Error:', err);
|
||||
// });
|
||||
//
|
||||
// this.client.on('connect', () => {
|
||||
// console.log('Redis Client Connected');
|
||||
// this.isConnected = true;
|
||||
// });
|
||||
//
|
||||
// this.client.on('disconnect', () => {
|
||||
// console.log('Redis Client Disconnected');
|
||||
// this.isConnected = false;
|
||||
// });
|
||||
//
|
||||
// this.client.on('ready', () => {
|
||||
// console.log('Redis Client Ready');
|
||||
// });
|
||||
//
|
||||
// this.client.on('reconnecting', () => {
|
||||
// console.log('Redis Client Reconnecting');
|
||||
// });
|
||||
this.client.on('connect', () => {
|
||||
console.log('Redis Client Connected');
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.client.on('disconnect', () => {
|
||||
console.log('Redis Client Disconnected');
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.client.on('ready', () => {
|
||||
console.log('Redis Client Ready');
|
||||
});
|
||||
|
||||
this.client.on('reconnecting', () => {
|
||||
console.log('Redis Client Reconnecting');
|
||||
});
|
||||
|
||||
// Connect immediately (fire and forget)
|
||||
// this.client.connect().catch((err) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
|
||||
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||
// import signedUrlCache from "@/src/lib/signed-url-cache" // Disabled for Workers compatibility
|
||||
import signedUrlCache from "@/src/lib/signed-url-cache"
|
||||
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
|
||||
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter"
|
||||
|
||||
|
|
@ -89,11 +89,12 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
|
|||
const s3Url = s3UrlRaw
|
||||
|
||||
try {
|
||||
// Cache disabled for Workers compatibility
|
||||
// const cachedUrl = signedUrlCache.get(s3Url);
|
||||
// if (cachedUrl) {
|
||||
// return cachedUrl;
|
||||
// }
|
||||
// Check if we have a cached signed URL
|
||||
const cachedUrl = signedUrlCache.get(s3Url);
|
||||
if (cachedUrl) {
|
||||
// Found in cache, return it
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
// Create the command to get the object
|
||||
const command = new GetObjectCommand({
|
||||
|
|
@ -104,8 +105,8 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
|
|||
// Generate the signed URL
|
||||
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
|
||||
|
||||
// Cache disabled for Workers compatibility
|
||||
// signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
|
||||
// Cache the signed URL with TTL matching the expiration time (convert seconds to milliseconds)
|
||||
signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000); // Subtract 1 minute to ensure it doesn't expire before use
|
||||
|
||||
return signedUrl;
|
||||
} catch (error) {
|
||||
|
|
@ -120,9 +121,14 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
|
|||
* @returns The original S3 URL if found in cache, otherwise null
|
||||
*/
|
||||
export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
|
||||
// Cache disabled for Workers compatibility - cannot retrieve original URL without cache
|
||||
// To re-enable, migrate signed-url-cache to object storage (R2/S3)
|
||||
return null;
|
||||
if (!signedUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find the original URL in our cache
|
||||
const originalUrl = signedUrlCache.getOriginalUrl(signedUrl);
|
||||
|
||||
return originalUrl || null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,263 +0,0 @@
|
|||
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
Normal file → Executable file
285
apps/backend/src/lib/signed-url-cache.ts
Normal file → Executable file
|
|
@ -1,24 +1,263 @@
|
|||
// SIGNED URL CACHE - DISABLED
|
||||
// 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)
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export default {
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
getOriginalUrl: () => undefined,
|
||||
has: () => false,
|
||||
hasSignedUrl: () => false,
|
||||
clear: () => {},
|
||||
clearExpired: () => 0,
|
||||
saveToDisk: () => {},
|
||||
loadFromDisk: () => {},
|
||||
};
|
||||
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;
|
||||
|
|
@ -2,7 +2,7 @@ import { Context, Next } from 'hono';
|
|||
import { jwtVerify } from 'jose';
|
||||
import { getStaffUserById, isUserSuspended } from '@/src/dbService';
|
||||
import { ApiError } from '@/src/lib/api-error';
|
||||
import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
import { encodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
|
||||
interface UserContext {
|
||||
userId: number;
|
||||
|
|
@ -27,7 +27,7 @@ export const authenticateUser = async (c: Context, next: Next) => {
|
|||
const token = authHeader.substring(7);
|
||||
console.log(c.req.header)
|
||||
|
||||
const { payload } = await jwtVerify(token, getEncodedJwtSecret());
|
||||
const { payload } = await jwtVerify(token, encodedJwtSecret);
|
||||
const decoded = payload as any;
|
||||
|
||||
// Check if this is a staff token (has staffId)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Context, Next } from 'hono';
|
||||
import { jwtVerify, errors } from 'jose';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
import { encodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
|
||||
export const verifyToken = async (c: Context, next: Next) => {
|
||||
try {
|
||||
|
|
@ -20,7 +20,7 @@ export const verifyToken = async (c: Context, next: Next) => {
|
|||
}
|
||||
|
||||
// Verify token
|
||||
const { payload } = await jwtVerify(token, getEncodedJwtSecret());
|
||||
const { payload } = await jwtVerify(token, encodedJwtSecret);
|
||||
|
||||
|
||||
// Add user info to context
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { Context, Next } from 'hono';
|
|||
import { jwtVerify } from 'jose';
|
||||
import { getStaffUserById } from '@/src/dbService';
|
||||
import { ApiError } from '@/src/lib/api-error';
|
||||
import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
import { encodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
|
||||
/**
|
||||
* Verify JWT token and extract payload
|
||||
*/
|
||||
const verifyStaffToken = async (token: string) => {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, getEncodedJwtSecret());
|
||||
const { payload } = await jwtVerify(token, encodedJwtSecret);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throw new ApiError('Access denied. Invalid auth credentials', 401);
|
||||
|
|
|
|||
|
|
@ -1,294 +0,0 @@
|
|||
// 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'
|
||||
|
|
@ -661,7 +661,7 @@ export const productRouter = router({
|
|||
return {
|
||||
groups: groups.map(group => ({
|
||||
...group,
|
||||
products: group.memberships.map((m: any) => ({
|
||||
products: group.memberships.map(m => ({
|
||||
...(m.product as AdminProduct),
|
||||
images: (m.product.images as string[]) || null,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-ind
|
|||
import { z } from 'zod';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { SignJWT } from 'jose';
|
||||
import { getEncodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
import { encodedJwtSecret } from '@/src/lib/env-exporter';
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import {
|
||||
getStaffUserByName,
|
||||
|
|
@ -44,7 +44,7 @@ export const staffUserRouter = router({
|
|||
const token = await new SignJWT({ staffId: staff.id, name: staff.name })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setExpirationTime('30d')
|
||||
.sign(getEncodedJwtSecret());
|
||||
.sign(encodedJwtSecret);
|
||||
|
||||
return {
|
||||
message: 'Login successful',
|
||||
|
|
|
|||
|
|
@ -420,9 +420,9 @@ export const vendorSnippetsRouter = router({
|
|||
productName: item.product.name,
|
||||
quantity: parseFloat(item.quantity),
|
||||
productSize: item.product.productQuantity,
|
||||
price: parseFloat((item.price ?? 0).toString()),
|
||||
price: parseFloat(item.price.toString()),
|
||||
unit: item.product.unit?.shortNotation || 'unit',
|
||||
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
|
||||
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||
is_packaged: item.is_packaged,
|
||||
is_package_verified: item.is_package_verified,
|
||||
}));
|
||||
|
|
@ -604,9 +604,9 @@ export const vendorSnippetsRouter = router({
|
|||
productId: item.productId,
|
||||
productName: item.product.name,
|
||||
quantity: parseFloat(item.quantity),
|
||||
price: parseFloat((item.price ?? 0).toString()),
|
||||
price: parseFloat(item.price.toString()),
|
||||
unit: item.product.unit?.shortNotation || 'unit',
|
||||
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
|
||||
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||
productSize: item.product.productQuantity,
|
||||
is_packaged: item.is_packaged,
|
||||
is_package_verified: item.is_package_verified,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs'
|
|||
import { SignJWT } from 'jose';
|
||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { getEncodedJwtSecret } from '@/src/lib/env-exporter'
|
||||
import { encodedJwtSecret } from '@/src/lib/env-exporter'
|
||||
import { sendOtp, verifyOtpUtil, getOtpCreds } from '@/src/lib/otp-utils'
|
||||
import {
|
||||
getUserAuthByEmail as getUserAuthByEmailInDb,
|
||||
|
|
@ -45,7 +45,7 @@ const generateToken = async (userId: number): Promise<string> => {
|
|||
return await new SignJWT({ userId })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setExpirationTime('7d')
|
||||
.sign(getEncodedJwtSecret());
|
||||
.sign(encodedJwtSecret);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -110,9 +110,7 @@ export const authRouter = router({
|
|||
createdAt: foundUser.createdAt.toISOString(),
|
||||
profileImage: profileImageSignedUrl,
|
||||
bio: userDetail?.bio || null,
|
||||
dateOfBirth: userDetail?.dateOfBirth
|
||||
? new Date(userDetail.dateOfBirth as any).toISOString()
|
||||
: null,
|
||||
dateOfBirth: userDetail?.dateOfBirth || null,
|
||||
gender: userDetail?.gender || null,
|
||||
occupation: userDetail?.occupation || null,
|
||||
},
|
||||
|
|
@ -372,9 +370,7 @@ export const authRouter = router({
|
|||
createdAt: updatedUser.createdAt?.toISOString?.() || new Date().toISOString(),
|
||||
profileImage: profileImageSignedUrl,
|
||||
bio: userDetail?.bio || null,
|
||||
dateOfBirth: userDetail?.dateOfBirth
|
||||
? new Date(userDetail.dateOfBirth as any).toISOString()
|
||||
: null,
|
||||
dateOfBirth: userDetail?.dateOfBirth || null,
|
||||
gender: userDetail?.gender || null,
|
||||
occupation: userDetail?.occupation || null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,29 +1,25 @@
|
|||
import { router, protectedProcedure } from "@/src/trpc/trpc-index";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
validateAndGetUserCoupon,
|
||||
applyDiscountToUserOrder,
|
||||
cancelUserOrderTransaction,
|
||||
checkUserSuspended,
|
||||
db,
|
||||
deleteUserCartItemsForOrder,
|
||||
getOrderProductById,
|
||||
getUserAddressByIdAndUser,
|
||||
getOrderProductById,
|
||||
checkUserSuspended,
|
||||
getUserSlotCapacityStatus,
|
||||
placeUserOrderTransaction,
|
||||
deleteUserCartItemsForOrder,
|
||||
recordUserCouponUsage,
|
||||
getUserOrdersWithRelations,
|
||||
getUserOrderCount,
|
||||
getUserOrderByIdWithRelations,
|
||||
getUserCouponUsageForOrder,
|
||||
getUserOrderBasic,
|
||||
getUserOrderByIdWithRelations,
|
||||
getUserOrderCount,
|
||||
getUserOrdersWithRelations,
|
||||
cancelUserOrderTransaction,
|
||||
updateUserOrderNotes,
|
||||
getUserRecentlyDeliveredOrderIds,
|
||||
getUserProductIdsFromOrders,
|
||||
getUserProductsForRecentOrders,
|
||||
getUserRecentlyDeliveredOrderIds,
|
||||
getUserSlotCapacityStatus,
|
||||
orders,
|
||||
orderItems,
|
||||
orderStatus,
|
||||
placeUserOrderTransaction,
|
||||
recordUserCouponUsage,
|
||||
updateUserOrderNotes,
|
||||
validateAndGetUserCoupon,
|
||||
} from "@/src/dbService";
|
||||
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
|
||||
import { scaffoldAssetUrl } from "@/src/lib/s3-client";
|
||||
|
|
@ -119,10 +115,9 @@ const placeOrderUtil = async (params: {
|
|||
const orderTotal = items.reduce(
|
||||
(sum, item) => {
|
||||
if (!item.product) return sum
|
||||
const basePrice = params.isFlash
|
||||
? (item.product.flashPrice ?? item.product.price)
|
||||
: item.product.price
|
||||
const itemPrice = parseFloat((basePrice ?? 0).toString())
|
||||
const itemPrice = params.isFlash
|
||||
? parseFloat((item.product.flashPrice || item.product.price).toString())
|
||||
: parseFloat(item.product.price.toString());
|
||||
return sum + itemPrice * item.quantity;
|
||||
},
|
||||
0
|
||||
|
|
@ -137,6 +132,9 @@ const placeOrderUtil = async (params: {
|
|||
|
||||
const totalWithDelivery = totalAmount + expectedDeliveryCharge;
|
||||
|
||||
const { db } = await import("postgresService");
|
||||
const { orders, orderItems, orderStatus } = await import("postgresService");
|
||||
|
||||
type OrderData = {
|
||||
order: Omit<typeof orders.$inferInsert, "id">;
|
||||
orderItems: Omit<typeof orderItems.$inferInsert, "id">[];
|
||||
|
|
@ -150,10 +148,9 @@ const placeOrderUtil = async (params: {
|
|||
const subOrderTotal = items.reduce(
|
||||
(sum, item) => {
|
||||
if (!item.product) return sum
|
||||
const basePrice = params.isFlash
|
||||
? (item.product.flashPrice ?? item.product.price)
|
||||
: item.product.price
|
||||
const itemPrice = parseFloat((basePrice ?? 0).toString())
|
||||
const itemPrice = params.isFlash
|
||||
? parseFloat((item.product.flashPrice || item.product.price).toString())
|
||||
: parseFloat(item.product.price.toString());
|
||||
return sum + itemPrice * item.quantity;
|
||||
},
|
||||
0
|
||||
|
|
@ -185,26 +182,23 @@ const placeOrderUtil = async (params: {
|
|||
isFlashDelivery: params.isFlash,
|
||||
};
|
||||
|
||||
const validItems = items.filter(
|
||||
(item): item is typeof item & { product: NonNullable<typeof item.product> } =>
|
||||
item.product !== null && item.product !== undefined
|
||||
)
|
||||
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 {
|
||||
const orderItemsData: Omit<typeof orderItems.$inferInsert, "id">[] = items
|
||||
.filter((item) => item.product !== null && item.product !== undefined)
|
||||
.map(
|
||||
(item) => ({
|
||||
orderId: 0,
|
||||
productId: item.productId,
|
||||
quantity: item.quantity.toString(),
|
||||
price: priceString,
|
||||
discountedPrice: priceString,
|
||||
}
|
||||
}
|
||||
);
|
||||
price: params.isFlash
|
||||
? item.product!.flashPrice || item.product!.price
|
||||
: item.product!.price,
|
||||
discountedPrice: (
|
||||
params.isFlash
|
||||
? item.product!.flashPrice || item.product!.price
|
||||
: item.product!.price
|
||||
).toString(),
|
||||
})
|
||||
);
|
||||
|
||||
const orderStatusData: Omit<typeof orderStatus.$inferInsert, "id"> = {
|
||||
userId,
|
||||
|
|
|
|||
|
|
@ -65,12 +65,9 @@ export const paymentRouter = router({
|
|||
};
|
||||
}
|
||||
|
||||
// Create Razorpay order and insert payment record
|
||||
if (order.totalAmount === null) {
|
||||
throw new ApiError('Order total is missing', 400)
|
||||
}
|
||||
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
|
||||
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
|
||||
// Create Razorpay order and insert payment record
|
||||
const razorpayOrder = await RazorpayPaymentService.createOrder(parseInt(orderId), order.totalAmount);
|
||||
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
|
||||
|
||||
return {
|
||||
razorpayOrderId: 0,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-ind
|
|||
import { SignJWT } from 'jose'
|
||||
import { z } from 'zod'
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { getEncodedJwtSecret } from '@/src/lib/env-exporter'
|
||||
import { encodedJwtSecret } from '@/src/lib/env-exporter'
|
||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import {
|
||||
getUserProfileById as getUserProfileByIdInDb,
|
||||
|
|
@ -23,7 +23,7 @@ const generateToken = async (userId: number): Promise<string> => {
|
|||
return await new SignJWT({ userId })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setExpirationTime('7d')
|
||||
.sign(getEncodedJwtSecret());
|
||||
.sign(encodedJwtSecret);
|
||||
};
|
||||
|
||||
export const userRouter = router({
|
||||
|
|
@ -59,9 +59,7 @@ export const userRouter = router({
|
|||
mobile: user.mobile,
|
||||
profileImage: profileImageSignedUrl,
|
||||
bio: userDetail?.bio || null,
|
||||
dateOfBirth: userDetail?.dateOfBirth
|
||||
? new Date(userDetail.dateOfBirth as any).toISOString()
|
||||
: null,
|
||||
dateOfBirth: userDetail?.dateOfBirth || null,
|
||||
gender: userDetail?.gender || null,
|
||||
occupation: userDetail?.occupation || null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,10 +35,8 @@
|
|||
"@commonTypes/*": ["../../packages/ui/shared-types/*"],
|
||||
"@packages/shared": ["../../packages/shared"],
|
||||
"@packages/shared/*": ["../../packages/shared/*"],
|
||||
// "postgresService": ["../../packages/db_helper_postgres"],
|
||||
// "postgresService/*": ["../../packages/db_helper_postgres/*"],
|
||||
"sqliteService": ["../../packages/db_helper_sqlite"],
|
||||
"sqliteService/*": ["../../packages/db_helper_sqlite/*"],
|
||||
"postgresService": ["../../packages/db_helper_postgres"],
|
||||
"postgresService/*": ["../../packages/db_helper_postgres/*"],
|
||||
"global-shared": ["../../packages/shared"],
|
||||
"global-shared/*": ["../../packages/shared/*"]
|
||||
},
|
||||
|
|
@ -124,5 +122,6 @@
|
|||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src", "types", "index.ts", "worker.ts", "../shared-types", "../../packages/shared"]
|
||||
"include": ["src", "types", "index.ts", "../shared-types", "../../packages/shared"]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
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)
|
||||
},
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
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
|
||||
|
|
@ -21,7 +21,6 @@ type StoreWithProductsResponse = StoreWithProductsApiType;
|
|||
|
||||
function useCacheUrl(filename: string): string | null {
|
||||
const { data: essentialConsts } = useGetEssentialConsts()
|
||||
console.log(essentialConsts)
|
||||
|
||||
const assetsDomain = essentialConsts?.assetsDomain
|
||||
const apiCacheKey = essentialConsts?.apiCacheKey
|
||||
|
|
|
|||
|
|
@ -1,501 +0,0 @@
|
|||
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
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1774588140474,
|
||||
"tag": "0000_nifty_sauron",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
// Re-export database connection
|
||||
export { db, initDb } from './src/db/db_index'
|
||||
|
||||
// Re-export schema
|
||||
export * from './src/db/schema'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { homeBanners, staffUsers } from '../db/schema'
|
||||
import { homeBanners } from '../db/schema'
|
||||
import { eq, desc } from 'drizzle-orm'
|
||||
|
||||
|
||||
export interface Banner {
|
||||
id: number
|
||||
name: string
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ export async function getUsersForCoupon(
|
|||
})
|
||||
|
||||
return {
|
||||
users: userList.map((user) => ({
|
||||
users: userList.map((user: typeof users.$inferSelect) => ({
|
||||
id: user.id,
|
||||
name: user.name || 'Unknown',
|
||||
mobile: user.mobile,
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export async function getOrderDetails(orderId: number): Promise<AdminOrderDetail
|
|||
let couponData = null
|
||||
if (couponUsageData.length > 0) {
|
||||
let totalDiscountAmount = 0
|
||||
const orderTotal = parseFloat((orderData.totalAmount ?? '0').toString())
|
||||
const orderTotal = parseFloat(orderData.totalAmount.toString())
|
||||
|
||||
for (const usage of couponUsageData) {
|
||||
let discountAmount = 0
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import type {
|
|||
UserOrderDetail,
|
||||
UserRecentProduct,
|
||||
} from '@packages/shared'
|
||||
import { coerceDate } from '../lib/date'
|
||||
|
||||
export interface OrderItemInput {
|
||||
productId: number
|
||||
|
|
@ -367,7 +366,7 @@ export async function getOrdersWithRelations(
|
|||
offset: number,
|
||||
pageSize: number
|
||||
): Promise<OrderWithRelations[]> {
|
||||
const ordersWithRelations = await db.query.orders.findMany({
|
||||
return db.query.orders.findMany({
|
||||
where: eq(orders.userId, userId),
|
||||
with: {
|
||||
orderItems: {
|
||||
|
|
@ -408,26 +407,10 @@ export async function getOrdersWithRelations(
|
|||
},
|
||||
},
|
||||
},
|
||||
orderBy: [desc(orders.createdAt)],
|
||||
orderBy: (ordersTable: typeof orders) => [desc(ordersTable.createdAt)],
|
||||
limit: pageSize,
|
||||
offset: offset,
|
||||
})
|
||||
|
||||
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[]
|
||||
}) as Promise<OrderWithRelations[]>
|
||||
}
|
||||
|
||||
export async function getOrderCount(userId: number): Promise<number> {
|
||||
|
|
@ -494,23 +477,7 @@ export async function getOrderByIdWithRelations(
|
|||
},
|
||||
})
|
||||
|
||||
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
|
||||
return order as OrderDetailWithRelations | null
|
||||
}
|
||||
|
||||
export async function getCouponUsageForOrder(
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
# @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
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
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`)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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`)
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* 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',
|
||||
};
|
||||
|
|
@ -1,336 +0,0 @@
|
|||
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 };
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
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 };
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
||||
|
|
@ -64,8 +64,7 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
|
|||
// const BASE_API_URL = API_URL;
|
||||
// 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.1.5:4000';
|
||||
const BASE_API_URL = 'http://192.168.1.5:8787';
|
||||
const BASE_API_URL = 'http://192.168.1.5:4000';
|
||||
// let BASE_API_URL = "https://mf.freshyo.in";
|
||||
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
||||
// let BASE_API_URL = 'http://192.168.100.107:4000';
|
||||
|
|
|
|||
183
progress.md
Normal file
183
progress.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# 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
|
||||
Loading…
Add table
Reference in a new issue