This commit is contained in:
shafi54 2026-04-27 21:21:11 +05:30
parent 8ea26f5705
commit 4199ff7d9b
17 changed files with 605 additions and 196 deletions

83
apps/admin-ui/.detoxrc.js Normal file
View file

@ -0,0 +1,83 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
'$0': 'jest',
config: 'e2e/jest.config.js'
},
jest: {
setupTimeout: 120000
}
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YOUR_APP.app',
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
},
'ios.release': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/YOUR_APP.app',
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
reversePorts: [
8081
]
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 15'
}
},
attached: {
type: 'android.attached',
device: {
adbName: '.*'
}
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_3a_API_30_x86'
}
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'ios.sim.release': {
device: 'simulator',
app: 'ios.release'
},
'android.att.debug': {
device: 'attached',
app: 'android.debug'
},
'android.att.release': {
device: 'attached',
app: 'android.release'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
},
'android.emu.release': {
device: 'emulator',
app: 'android.release'
}
}
};

View file

@ -0,0 +1,12 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
rootDir: '..',
testMatch: ['<rootDir>/e2e/**/*.test.js'],
testTimeout: 120000,
maxWorkers: 1,
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
reporters: ['detox/runners/jest/reporter'],
testEnvironment: 'detox/runners/jest/testEnvironment',
verbose: true,
};

View file

@ -0,0 +1,23 @@
describe('Example', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should have welcome screen', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
});
it('should show hello screen after tap', async () => {
await element(by.id('hello_button')).tap();
await expect(element(by.text('Hello!!!'))).toBeVisible();
});
it('should show world screen after tap', async () => {
await element(by.id('world_button')).tap();
await expect(element(by.text('World!!!'))).toBeVisible();
});
});

View file

@ -4,7 +4,7 @@ import { ensureWorkerInit } from '@/src/lib/worker-init'
const LAST_TRIGGER_KEY = 'lastTrigger'
const ALARM_DELAY_MINUTES = 0.5
// const ALARM_DELAY_MINUTES = 3
// const ALARM_DELAY_MINUTES = 0.1
export class CacheCreator {
private state: any
@ -53,13 +53,16 @@ export class CacheCreator {
async alarm(): Promise<void> {
ensureWorkerInit(this.env)
console.log('from the shceduler')
const lastTrigger = await this.state.storage.get(LAST_TRIGGER_KEY)
if (!lastTrigger) {
return
}
const threshold = dayjs().subtract(ALARM_DELAY_MINUTES, 'minute')
if (dayjs(lastTrigger).isBefore(threshold)) {
const isQualify = dayjs(lastTrigger).isBefore(threshold);
console.log({isQualify, threshold, curr: dayjs(lastTrigger)})
if (isQualify) {
await initializeAllStores()
}
}

View file

@ -88,11 +88,15 @@ async function createProductsFileInternal(version: number): Promise<string> {
const productsData = await scaffoldProducts()
const jsonContent = JSON.stringify(productsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const filePath = `${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.products, version)}`
console.log(filePath)
return await imageUploadS3(
buffer,
'application/json',
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.products, version)}`
)
filePath
)
}
async function createEssentialConstsFileInternal(version: number): Promise<string> {

View file

@ -1,79 +1,79 @@
import fs from "fs";
import path from "path";
export class DiskPersistedSet {
private set: Set<string>;
private readonly filePath: string;
private dirty = false;
constructor(filePath: string = "./persister") {
this.filePath = path.resolve(filePath);
// ✅ Ensure file exists
if (!fs.existsSync(this.filePath)) {
fs.writeFileSync(this.filePath, "", "utf8");
}
// ✅ Load existing values from file
const contents = fs.readFileSync(this.filePath, "utf8");
this.set = new Set(
contents.split("\n").map(x => x.trim()).filter(x => x.length > 0)
);
this.registerExitHandlers();
}
private persist() {
if (!this.dirty) return;
fs.writeFileSync(this.filePath, Array.from(this.set).join("\n"), "utf8");
this.dirty = false;
}
private markDirty() {
this.dirty = true;
}
add(value: string): void {
if (!this.set.has(value)) {
this.set.add(value);
this.markDirty();
this.persist();
}
}
delete(value: string): void {
if (this.set.delete(value)) {
this.markDirty();
this.persist();
}
}
has(value: string): boolean {
return this.set.has(value);
}
values(): string[] {
return Array.from(this.set);
}
clear(): void {
if (this.set.size > 0) {
this.set.clear();
this.markDirty();
this.persist();
}
}
private registerExitHandlers() {
const flush = () => this.persist();
process.on("exit", flush);
process.on("SIGINT", () => { flush(); process.exit(); });
process.on("SIGTERM", () => { flush(); process.exit(); });
process.on("uncaughtException", (err) => {
console.error("Uncaught exception. Flushing DiskPersistedSet:", err);
flush();
process.exit(1);
});
}
}
// import fs from "fs";
// import path from "path";
//
// export class DiskPersistedSet {
// private set: Set<string>;
// private readonly filePath: string;
// private dirty = false;
//
// constructor(filePath: string = "./persister") {
// this.filePath = path.resolve(filePath);
//
// // ✅ Ensure file exists
// if (!fs.existsSync(this.filePath)) {
// fs.writeFileSync(this.filePath, "", "utf8");
// }
//
// // ✅ Load existing values from file
// const contents = fs.readFileSync(this.filePath, "utf8");
// this.set = new Set(
// contents.split("\n").map(x => x.trim()).filter(x => x.length > 0)
// );
//
// this.registerExitHandlers();
// }
//
// private persist() {
// if (!this.dirty) return;
// fs.writeFileSync(this.filePath, Array.from(this.set).join("\n"), "utf8");
// this.dirty = false;
// }
//
// private markDirty() {
// this.dirty = true;
// }
//
// add(value: string): void {
// if (!this.set.has(value)) {
// this.set.add(value);
// this.markDirty();
// this.persist();
// }
// }
//
// delete(value: string): void {
// if (this.set.delete(value)) {
// this.markDirty();
// this.persist();
// }
// }
//
// has(value: string): boolean {
// return this.set.has(value);
// }
//
// values(): string[] {
// return Array.from(this.set);
// }
//
// clear(): void {
// if (this.set.size > 0) {
// this.set.clear();
// this.markDirty();
// this.persist();
// }
// }
//
// private registerExitHandlers() {
// const flush = () => this.persist();
//
// process.on("exit", flush);
// process.on("SIGINT", () => { flush(); process.exit(); });
// process.on("SIGTERM", () => { flush(); process.exit(); });
// process.on("uncaughtException", (err) => {
// console.error("Uncaught exception. Flushing DiskPersistedSet:", err);
// flush();
// process.exit(1);
// });
// }
// }

193
apps/backend/src/lib/s3-client.ts Executable file → Normal file
View file

@ -1,8 +1,5 @@
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
import type { Buffer } from 'buffer'
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 { AwsClient } from 'aws4fetch'
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
import {
getS3AccessKeyId,
@ -11,74 +8,81 @@ import {
getS3SecretAccessKey,
getS3BucketName,
getAssetsDomain,
} from "@/src/lib/env-exporter"
} from '@/src/lib/env-exporter'
let s3Client: S3Client | null = null
let s3ClientKey = ''
let awsClient: AwsClient | null = null
let awsClientKey = ''
const getS3Client = () => {
const getAwsClient = () => {
const region = getS3Region()
const endpoint = getS3Url()
const accessKeyId = getS3AccessKeyId()
const secretAccessKey = getS3SecretAccessKey()
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
if (!s3Client || nextKey !== s3ClientKey) {
s3ClientKey = nextKey
s3Client = new S3Client({
if (!awsClient || nextKey !== awsClientKey) {
awsClientKey = nextKey
awsClient = new AwsClient({
accessKeyId,
secretAccessKey,
region,
endpoint,
forcePathStyle: true,
credentials: {
accessKeyId,
secretAccessKey,
},
service: 's3',
})
}
return s3Client
return awsClient
}
export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
// const key = `${category}/${Date.now()}`
const s3BucketName = getS3BucketName()
const s3Client = getS3Client()
const command = new PutObjectCommand({
Bucket: s3BucketName,
Key: key,
Body: body,
ContentType: type,
const buildObjectUrl = (bucket: string, key: string) => {
const endpoint = getS3Url()
const normalizedEndpoint = endpoint.endsWith('/')
? endpoint.slice(0, -1)
: endpoint
const normalizedKey = key.replace(/^\/+/, '')
return `${normalizedEndpoint}/${bucket}/${normalizedKey}`
}
export const imageUploadS3 = async(body: Buffer, type: string, key: string) => {
const client = getAwsClient()
const url = buildObjectUrl(getS3BucketName(), key)
const resp = await client.fetch(url, {
method: 'PUT',
headers: {
'Content-Type': type,
},
body,
})
const resp = await s3Client.send(command)
if (!resp.ok) {
const responseBody = await resp.text().catch(() => '')
throw new Error(`Failed to upload image: ${resp.status} ${responseBody}`)
}
const imageUrl = `${key}`
return imageUrl;
return imageUrl
}
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?:string, keys: string[]}) {
export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?: string, keys: string[]}) {
if (keys.length === 0) {
return true;
return true
}
try {
const s3Client = getS3Client()
const client = getAwsClient()
await Promise.all(
keys.map((key) => {
const deleteCommand = new DeleteObjectCommand({
Bucket: bucket,
Key: key,
})
return s3Client.send(deleteCommand)
keys.map(async (key) => {
const url = buildObjectUrl(bucket, key)
const resp = await client.fetch(url, { method: 'DELETE' })
if (!resp.ok && resp.status !== 404) {
const body = await resp.text().catch(() => '')
throw new Error(`Failed to delete image: ${resp.status} ${body}`)
}
})
)
return true
}
catch (error) {
console.error("Error deleting image:", error)
throw new Error("Failed to delete image")
console.error('Error deleting image:', error)
throw new Error('Failed to delete image')
}
}
@ -87,19 +91,18 @@ export function scaffoldAssetUrl(input: (string | null)[]): string[]
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
const assetsDomain = getAssetsDomain()
if (Array.isArray(input)) {
return input.map(key => scaffoldAssetUrl(key) as string);
return input.map(key => scaffoldAssetUrl(key) as string)
}
if (!input) {
return '';
return ''
}
const normalizedKey = input.replace(/^\/+/, '');
const normalizedKey = input.replace(/^\/+/, '')
const domain = assetsDomain.endsWith('/')
? assetsDomain.slice(0, -1)
: assetsDomain;
return `${domain}/${normalizedKey}`;
: assetsDomain
return `${domain}/${normalizedKey}`
}
/**
* Generate a signed URL from an S3 URL
* @param s3Url The full S3 URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/object)
@ -108,34 +111,23 @@ export function scaffoldAssetUrl(input: string | null | (string | null)[]): stri
*/
export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
if (!s3UrlRaw) {
return '';
return ''
}
const s3Url = s3UrlRaw
try {
// Cache disabled for Workers compatibility
// const cachedUrl = signedUrlCache.get(s3Url);
// if (cachedUrl) {
// return cachedUrl;
// }
// Create the command to get the object
const command = new GetObjectCommand({
Bucket: getS3BucketName(),
Key: s3Url,
});
// Generate the signed URL
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
// Cache disabled for Workers compatibility
// signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
return signedUrl;
const client = getAwsClient()
const url = buildObjectUrl(getS3BucketName(), s3Url)
const signedRequest = await client.sign(url, {
method: 'GET',
signQuery: true,
expires: expiresIn,
})
return signedRequest.url
} catch (error) {
console.error("Error generating signed URL:", error);
throw new Error("Failed to generate signed URL");
console.error('Error generating signed URL:', error)
throw new Error('Failed to generate signed URL')
}
}
@ -147,7 +139,7 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
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;
return null
}
/**
@ -158,44 +150,43 @@ export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null
*/
export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
if (!s3Urls || !s3Urls.length) {
return [];
return []
}
try {
// Process URLs in parallel for better performance
const signedUrls = await Promise.all(
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
);
)
return signedUrls;
return signedUrls
} catch (error) {
console.error("Error generating multiple signed URLs:", error);
// Return an array of empty strings with the same length as input
return s3Urls.map(() => '');
console.error('Error generating multiple signed URLs:', error)
return s3Urls.map(() => '')
}
}
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
try {
// Insert record into upload_url_status
await createUploadUrlStatus(key)
// Generate signed upload URL
const command = new PutObjectCommand({
Bucket: getS3BucketName(),
Key: key,
ContentType: mimeType,
});
const client = getAwsClient()
const url = buildObjectUrl(getS3BucketName(), key)
const signedRequest = await client.sign(url, {
method: 'PUT',
signQuery: true,
expires: expiresIn,
headers: {
'Content-Type': mimeType,
},
})
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
return signedUrl;
return signedRequest.url
} catch (error) {
console.error('Error generating upload URL:', error);
throw new Error('Failed to generate upload URL');
console.error('Error generating upload URL:', error)
throw new Error('Failed to generate upload URL')
}
}
// export function extractKeyFromPresignedUrl(url:string) {
// const u = new URL(url);
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
@ -204,27 +195,27 @@ export async function generateUploadUrl(key: string, mimeType: string, expiresIn
// New function (excludes bucket name)
export function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
const decodedKey = decodeURIComponent(rawKey);
const u = new URL(url)
const rawKey = u.pathname.replace(/^\/+/, '') // remove leading slash
const decodedKey = decodeURIComponent(rawKey)
// Remove bucket prefix
const parts = decodedKey.split('/');
parts.shift(); // Remove bucket name
return parts.join('/');
const parts = decodedKey.split('/')
parts.shift() // Remove bucket name
return parts.join('/')
}
export async function claimUploadUrl(url: string): Promise<void> {
try {
const semiKey = extractKeyFromPresignedUrl(url);
const semiKey = extractKeyFromPresignedUrl(url)
// Update status to 'claimed' if currently 'pending'
const updated = await claimUploadUrlStatus(semiKey)
if (!updated) {
throw new Error('Upload URL not found or already claimed');
throw new Error('Upload URL not found or already claimed')
}
} catch (error) {
console.error('Error claiming upload URL:', error);
throw new Error('Failed to claim upload URL');
console.error('Error claiming upload URL:', error)
throw new Error('Failed to claim upload URL')
}
}

View file

@ -0,0 +1,244 @@
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
import type { Buffer } from 'buffer'
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 { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
import {
getS3AccessKeyId,
getS3Region,
getS3Url,
getS3SecretAccessKey,
getS3BucketName,
getAssetsDomain,
} from "@/src/lib/env-exporter"
let s3Client: S3Client | null = null
let s3ClientKey = ''
const getS3Client = () => {
const region = getS3Region()
const endpoint = getS3Url()
const accessKeyId = getS3AccessKeyId()
const secretAccessKey = getS3SecretAccessKey()
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
if (!s3Client || nextKey !== s3ClientKey) {
s3ClientKey = nextKey
s3Client = new S3Client({
region,
endpoint,
forcePathStyle: true,
credentials: {
accessKeyId,
secretAccessKey,
},
})
}
return s3Client
}
// export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
// // const key = `${category}/${Date.now()}`
// const s3BucketName = getS3BucketName()
// const s3Client = getS3Client()
// const command = new PutObjectCommand({
// Bucket: s3BucketName,
// Key: key,
// Body: body,
// ContentType: type,
// })
// const resp = await s3Client.send(command)
//
// const imageUrl = `${key}`
// return imageUrl;
// }
export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
const env = (globalThis as any).ENV || {}
if (!env.MY_BUCKET) {
throw new Error('MY_BUCKET binding not found in runtime env')
}
await env.MY_BUCKET.put(key, body, {
httpMetadata: {
contentType: type,
},
})
const imageUrl = `${key}`
return imageUrl
}
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?:string, keys: string[]}) {
if (keys.length === 0) {
return true;
}
try {
const s3Client = getS3Client()
await Promise.all(
keys.map((key) => {
const deleteCommand = new DeleteObjectCommand({
Bucket: bucket,
Key: key,
})
return s3Client.send(deleteCommand)
})
)
return true
}
catch (error) {
console.error("Error deleting image:", error)
throw new Error("Failed to delete image")
}
}
export function scaffoldAssetUrl(input: string | null): string
export function scaffoldAssetUrl(input: (string | null)[]): string[]
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
const assetsDomain = getAssetsDomain()
if (Array.isArray(input)) {
return input.map(key => scaffoldAssetUrl(key) as string);
}
if (!input) {
return '';
}
const normalizedKey = input.replace(/^\/+/, '');
const domain = assetsDomain.endsWith('/')
? assetsDomain.slice(0, -1)
: assetsDomain;
return `${domain}/${normalizedKey}`;
}
/**
* Generate a signed URL from an S3 URL
* @param s3Url The full S3 URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/object)
* @param expiresIn Expiration time in seconds (default: 259200 seconds = 3 days)
* @returns A pre-signed URL that provides temporary access to the object
*/
export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
if (!s3UrlRaw) {
return '';
}
const s3Url = s3UrlRaw
try {
// Cache disabled for Workers compatibility
// const cachedUrl = signedUrlCache.get(s3Url);
// if (cachedUrl) {
// return cachedUrl;
// }
// Create the command to get the object
const command = new GetObjectCommand({
Bucket: getS3BucketName(),
Key: s3Url,
});
// Generate the signed URL
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
// Cache disabled for Workers compatibility
// signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
return signedUrl;
} catch (error) {
console.error("Error generating signed URL:", error);
throw new Error("Failed to generate signed URL");
}
}
/**
* Get the original S3 URL from a signed URL
* @param signedUrl The signed URL
* @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;
}
/**
* Generate signed URLs for multiple S3 URLs
* @param s3Urls Array of S3 URLs or null values
* @param expiresIn Expiration time in seconds (default: 259200 seconds = 3 days)
* @returns Array of signed URLs (empty strings for null/invalid inputs)
*/
export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
if (!s3Urls || !s3Urls.length) {
return [];
}
try {
// Process URLs in parallel for better performance
const signedUrls = await Promise.all(
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
);
return signedUrls;
} catch (error) {
console.error("Error generating multiple signed URLs:", error);
// Return an array of empty strings with the same length as input
return s3Urls.map(() => '');
}
}
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
try {
// Insert record into upload_url_status
await createUploadUrlStatus(key)
// Generate signed upload URL
const command = new PutObjectCommand({
Bucket: getS3BucketName(),
Key: key,
ContentType: mimeType,
});
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
return signedUrl;
} catch (error) {
console.error('Error generating upload URL:', error);
throw new Error('Failed to generate upload URL');
}
}
// export function extractKeyFromPresignedUrl(url:string) {
// const u = new URL(url);
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
// return decodeURIComponent(rawKey);
// }
// New function (excludes bucket name)
export function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
const decodedKey = decodeURIComponent(rawKey);
// Remove bucket prefix
const parts = decodedKey.split('/');
parts.shift(); // Remove bucket name
return parts.join('/');
}
export async function claimUploadUrl(url: string): Promise<void> {
try {
const semiKey = extractKeyFromPresignedUrl(url);
// Update status to 'claimed' if currently 'pending'
const updated = await claimUploadUrlStatus(semiKey)
if (!updated) {
throw new Error('Upload URL not found or already claimed');
}
} catch (error) {
console.error('Error claiming upload URL:', error);
throw new Error('Failed to claim upload URL');
}
}

View file

@ -226,7 +226,7 @@ export const productRouter = router({
price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional(),
incrementStep: z.number().optional().default(1),
productQuantity: z.number().optional().default(1),
productQuantity: z.union([z.number(), z.string()]).optional().default(1),
isSuspended: z.boolean().optional().default(false),
isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().optional(),
@ -262,7 +262,7 @@ export const productRouter = router({
price: price.toString(),
marketPrice: marketPrice?.toString(),
incrementStep,
productQuantity,
productQuantity: productQuantity as any,
isSuspended,
isFlashAvailable,
flashPrice: flashPrice?.toString(),
@ -302,7 +302,7 @@ export const productRouter = router({
price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional(),
incrementStep: z.number().optional().default(1),
productQuantity: z.number().optional().default(1),
productQuantity: z.union([z.number(), z.string()]).optional().default(1),
isSuspended: z.boolean().optional().default(false),
isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().nullable().optional(),
@ -347,7 +347,7 @@ export const productRouter = router({
price: price.toString(),
marketPrice: marketPrice?.toString(),
incrementStep,
productQuantity,
productQuantity: productQuantity as any,
isSuspended,
isFlashAvailable,
flashPrice: flashPrice?.toString() ?? null,

View file

@ -40,6 +40,10 @@ queue = "order-placed-queue-dev"
[[queues.consumers]]
queue = "order-cancelled-queue-dev"
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "meatfarmer-dev"
[observability]
enabled = false
head_sampling_rate = 1

View file

@ -40,6 +40,10 @@ queue = "order-placed-queue"
[[queues.consumers]]
queue = "order-cancelled-queue"
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "meatfarmer"
[observability]
enabled = false
head_sampling_rate = 1

View file

@ -6,6 +6,7 @@ import AddressForm from '@/src/components/AddressForm';
import LocationAttacher from '@/src/components/LocationAttacher';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '@/src/trpc-client';
import { useAuth } from '@/src/contexts/AuthContext';
import * as Location from 'expo-location';
interface AddressSelectorProps {
@ -22,7 +23,11 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
const [locationLoading, setLocationLoading] = useState(false);
const queryClient = useQueryClient();
const scrollViewRef = useRef<ScrollView>(null);
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery();
const { isAuthenticated } = useAuth();
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery(
undefined,
{ enabled: isAuthenticated }
);
const updateAddressMutation = trpc.user.address.updateAddress.useMutation({
onSuccess: () => {

View file

@ -80,7 +80,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
return desc;
};
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery();
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery(
undefined,
{ enabled: isAuthenticated }
);
const { data: constsData } = useGetEssentialConsts();
const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);

View file

@ -6,6 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useQueryClient } from '@tanstack/react-query';
import AddressForm from '@/src/components/AddressForm';
import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
import { useAuth } from '@/src/contexts/AuthContext';
import { trpc } from '@/src/trpc-client';
import { useCentralProductStore } from '@/src/store/centralProductStore';
@ -24,6 +25,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const params = useLocalSearchParams();
const queryClient = useQueryClient();
const router = useRouter();
const { isAuthenticated } = useAuth();
// Protect checkout route and preserve query params
useAuthenticatedRoute({
@ -34,7 +36,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const cartType: "regular" | "flash" = isFlashDelivery ? "flash" : "regular";
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery();
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery(
undefined,
{ enabled: isAuthenticated }
);
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery();
const { data: constsData } = useGetEssentialConsts();
const products = useCentralProductStore((state) => state.products);
@ -137,7 +142,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
0
);
const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery();
const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery(
undefined,
{ enabled: isAuthenticated }
);
const eligibleCoupons = useMemo(() => {
if (!couponsRaw?.data) return [];

View file

@ -163,13 +163,38 @@ type ProductInfoInsert = InferInsertModel<typeof productInfo>
type ProductInfoUpdate = Partial<ProductInfoInsert>
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
const [product] = await db.insert(productInfo).values(input).returning()
const productQuantityRaw = (input as any).productQuantity
const productQuantity = typeof productQuantityRaw === 'string'
? Number(productQuantityRaw)
: productQuantityRaw
const safeProductQuantity = typeof productQuantity === 'number' && Number.isFinite(productQuantity)
? productQuantity
: 1
const [product] = await db.insert(productInfo).values({
...input,
productQuantity: safeProductQuantity,
}).returning()
return mapProduct(product)
}
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
const productQuantityRaw = (updates as any).productQuantity
const productQuantity = typeof productQuantityRaw === 'string'
? Number(productQuantityRaw)
: productQuantityRaw
const safeUpdates = typeof productQuantityRaw === 'undefined'
? updates
: {
...updates,
productQuantity: typeof productQuantity === 'number' && Number.isFinite(productQuantity)
? productQuantity
: 1,
}
const [product] = await db.update(productInfo)
.set(updates)
.set(safeUpdates)
.where(eq(productInfo.id, id))
.returning()
if (!product) {

View file

@ -64,8 +64,8 @@ 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.100.109:8787';
const BASE_API_URL = 'http://192.168.1.5:8787';
// const BASE_API_URL = 'http://192.168.100.109:8787';
// let BASE_API_URL = "https://raw.freshyo.in";
// let BASE_API_URL = "https://worker.freshyo.in";
// let BASE_API_URL = "https://freshyo.technocracy.ovh";