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 LAST_TRIGGER_KEY = 'lastTrigger'
const ALARM_DELAY_MINUTES = 0.5 const ALARM_DELAY_MINUTES = 0.5
// const ALARM_DELAY_MINUTES = 3 // const ALARM_DELAY_MINUTES = 0.1
export class CacheCreator { export class CacheCreator {
private state: any private state: any
@ -53,13 +53,16 @@ export class CacheCreator {
async alarm(): Promise<void> { async alarm(): Promise<void> {
ensureWorkerInit(this.env) ensureWorkerInit(this.env)
console.log('from the shceduler')
const lastTrigger = await this.state.storage.get(LAST_TRIGGER_KEY) const lastTrigger = await this.state.storage.get(LAST_TRIGGER_KEY)
if (!lastTrigger) { if (!lastTrigger) {
return return
} }
const threshold = dayjs().subtract(ALARM_DELAY_MINUTES, 'minute') 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() await initializeAllStores()
} }
} }

View file

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

View file

@ -1,79 +1,79 @@
import fs from "fs"; // import fs from "fs";
import path from "path"; // import path from "path";
//
export class DiskPersistedSet { // export class DiskPersistedSet {
private set: Set<string>; // private set: Set<string>;
private readonly filePath: string; // private readonly filePath: string;
private dirty = false; // private dirty = false;
//
constructor(filePath: string = "./persister") { // constructor(filePath: string = "./persister") {
this.filePath = path.resolve(filePath); // this.filePath = path.resolve(filePath);
//
// ✅ Ensure file exists // // ✅ Ensure file exists
if (!fs.existsSync(this.filePath)) { // if (!fs.existsSync(this.filePath)) {
fs.writeFileSync(this.filePath, "", "utf8"); // fs.writeFileSync(this.filePath, "", "utf8");
} // }
//
// ✅ Load existing values from file // // ✅ Load existing values from file
const contents = fs.readFileSync(this.filePath, "utf8"); // const contents = fs.readFileSync(this.filePath, "utf8");
this.set = new Set( // this.set = new Set(
contents.split("\n").map(x => x.trim()).filter(x => x.length > 0) // contents.split("\n").map(x => x.trim()).filter(x => x.length > 0)
); // );
//
this.registerExitHandlers(); // this.registerExitHandlers();
} // }
//
private persist() { // private persist() {
if (!this.dirty) return; // if (!this.dirty) return;
fs.writeFileSync(this.filePath, Array.from(this.set).join("\n"), "utf8"); // fs.writeFileSync(this.filePath, Array.from(this.set).join("\n"), "utf8");
this.dirty = false; // this.dirty = false;
} // }
//
private markDirty() { // private markDirty() {
this.dirty = true; // this.dirty = true;
} // }
//
add(value: string): void { // add(value: string): void {
if (!this.set.has(value)) { // if (!this.set.has(value)) {
this.set.add(value); // this.set.add(value);
this.markDirty(); // this.markDirty();
this.persist(); // this.persist();
} // }
} // }
//
delete(value: string): void { // delete(value: string): void {
if (this.set.delete(value)) { // if (this.set.delete(value)) {
this.markDirty(); // this.markDirty();
this.persist(); // this.persist();
} // }
} // }
//
has(value: string): boolean { // has(value: string): boolean {
return this.set.has(value); // return this.set.has(value);
} // }
//
values(): string[] { // values(): string[] {
return Array.from(this.set); // return Array.from(this.set);
} // }
//
clear(): void { // clear(): void {
if (this.set.size > 0) { // if (this.set.size > 0) {
this.set.clear(); // this.set.clear();
this.markDirty(); // this.markDirty();
this.persist(); // this.persist();
} // }
} // }
//
private registerExitHandlers() { // private registerExitHandlers() {
const flush = () => this.persist(); // const flush = () => this.persist();
//
process.on("exit", flush); // process.on("exit", flush);
process.on("SIGINT", () => { flush(); process.exit(); }); // process.on("SIGINT", () => { flush(); process.exit(); });
process.on("SIGTERM", () => { flush(); process.exit(); }); // process.on("SIGTERM", () => { flush(); process.exit(); });
process.on("uncaughtException", (err) => { // process.on("uncaughtException", (err) => {
console.error("Uncaught exception. Flushing DiskPersistedSet:", err); // console.error("Uncaught exception. Flushing DiskPersistedSet:", err);
flush(); // flush();
process.exit(1); // 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 type { Buffer } from 'buffer'
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3" import { AwsClient } from 'aws4fetch'
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 { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
import { import {
getS3AccessKeyId, getS3AccessKeyId,
@ -11,74 +8,81 @@ import {
getS3SecretAccessKey, getS3SecretAccessKey,
getS3BucketName, getS3BucketName,
getAssetsDomain, getAssetsDomain,
} from "@/src/lib/env-exporter" } from '@/src/lib/env-exporter'
let s3Client: S3Client | null = null let awsClient: AwsClient | null = null
let s3ClientKey = '' let awsClientKey = ''
const getS3Client = () => { const getAwsClient = () => {
const region = getS3Region() const region = getS3Region()
const endpoint = getS3Url() const endpoint = getS3Url()
const accessKeyId = getS3AccessKeyId() const accessKeyId = getS3AccessKeyId()
const secretAccessKey = getS3SecretAccessKey() const secretAccessKey = getS3SecretAccessKey()
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}` const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
if (!s3Client || nextKey !== s3ClientKey) { if (!awsClient || nextKey !== awsClientKey) {
s3ClientKey = nextKey awsClientKey = nextKey
s3Client = new S3Client({ awsClient = new AwsClient({
accessKeyId,
secretAccessKey,
region, region,
endpoint, service: 's3',
forcePathStyle: true,
credentials: {
accessKeyId,
secretAccessKey,
},
}) })
} }
return s3Client return awsClient
} }
export const imageUploadS3 = async(body: Buffer, type: string, key:string) => { const buildObjectUrl = (bucket: string, key: string) => {
// const key = `${category}/${Date.now()}` const endpoint = getS3Url()
const s3BucketName = getS3BucketName() const normalizedEndpoint = endpoint.endsWith('/')
const s3Client = getS3Client() ? endpoint.slice(0, -1)
const command = new PutObjectCommand({ : endpoint
Bucket: s3BucketName, const normalizedKey = key.replace(/^\/+/, '')
Key: key, return `${normalizedEndpoint}/${bucket}/${normalizedKey}`
Body: body, }
ContentType: type,
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}` const imageUrl = `${key}`
return imageUrl; return imageUrl
} }
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>; // 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) { if (keys.length === 0) {
return true; return true
} }
try { try {
const s3Client = getS3Client() const client = getAwsClient()
await Promise.all( await Promise.all(
keys.map((key) => { keys.map(async (key) => {
const deleteCommand = new DeleteObjectCommand({ const url = buildObjectUrl(bucket, key)
Bucket: bucket, const resp = await client.fetch(url, { method: 'DELETE' })
Key: key, if (!resp.ok && resp.status !== 404) {
}) const body = await resp.text().catch(() => '')
return s3Client.send(deleteCommand) throw new Error(`Failed to delete image: ${resp.status} ${body}`)
}
}) })
) )
return true return true
} }
catch (error) { catch (error) {
console.error("Error deleting image:", error) console.error('Error deleting image:', error)
throw new Error("Failed to delete image") 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[] { export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
const assetsDomain = getAssetsDomain() const assetsDomain = getAssetsDomain()
if (Array.isArray(input)) { if (Array.isArray(input)) {
return input.map(key => scaffoldAssetUrl(key) as string); return input.map(key => scaffoldAssetUrl(key) as string)
} }
if (!input) { if (!input) {
return ''; return ''
} }
const normalizedKey = input.replace(/^\/+/, ''); const normalizedKey = input.replace(/^\/+/, '')
const domain = assetsDomain.endsWith('/') const domain = assetsDomain.endsWith('/')
? assetsDomain.slice(0, -1) ? assetsDomain.slice(0, -1)
: assetsDomain; : assetsDomain
return `${domain}/${normalizedKey}`; return `${domain}/${normalizedKey}`
} }
/** /**
* Generate a signed URL from an S3 URL * 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 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> { export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
if (!s3UrlRaw) { if (!s3UrlRaw) {
return ''; return ''
} }
const s3Url = s3UrlRaw const s3Url = s3UrlRaw
try { try {
// Cache disabled for Workers compatibility const client = getAwsClient()
// const cachedUrl = signedUrlCache.get(s3Url); const url = buildObjectUrl(getS3BucketName(), s3Url)
// if (cachedUrl) { const signedRequest = await client.sign(url, {
// return cachedUrl; method: 'GET',
// } signQuery: true,
expires: expiresIn,
// Create the command to get the object })
const command = new GetObjectCommand({ return signedRequest.url
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) { } catch (error) {
console.error("Error generating signed URL:", error); console.error('Error generating signed URL:', error)
throw new Error("Failed to generate signed URL"); 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 { export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
// Cache disabled for Workers compatibility - cannot retrieve original URL without cache // Cache disabled for Workers compatibility - cannot retrieve original URL without cache
// To re-enable, migrate signed-url-cache to object storage (R2/S3) // 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[]> { export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
if (!s3Urls || !s3Urls.length) { if (!s3Urls || !s3Urls.length) {
return []; return []
} }
try { try {
// Process URLs in parallel for better performance
const signedUrls = await Promise.all( const signedUrls = await Promise.all(
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => '')) s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
); )
return signedUrls; return signedUrls
} catch (error) { } catch (error) {
console.error("Error generating multiple signed URLs:", error); console.error('Error generating multiple signed URLs:', error)
// Return an array of empty strings with the same length as input return s3Urls.map(() => '')
return s3Urls.map(() => '');
} }
} }
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> { export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
try { try {
// Insert record into upload_url_status
await createUploadUrlStatus(key) await createUploadUrlStatus(key)
// Generate signed upload URL const client = getAwsClient()
const command = new PutObjectCommand({ const url = buildObjectUrl(getS3BucketName(), key)
Bucket: getS3BucketName(), const signedRequest = await client.sign(url, {
Key: key, method: 'PUT',
ContentType: mimeType, signQuery: true,
}); expires: expiresIn,
headers: {
'Content-Type': mimeType,
},
})
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn }); return signedRequest.url
return signedUrl;
} catch (error) { } catch (error) {
console.error('Error generating upload URL:', error); console.error('Error generating upload URL:', error)
throw new Error('Failed to generate upload URL'); throw new Error('Failed to generate upload URL')
} }
} }
// export function extractKeyFromPresignedUrl(url:string) { // export function extractKeyFromPresignedUrl(url:string) {
// const u = new URL(url); // const u = new URL(url);
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash // 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) // New function (excludes bucket name)
export function extractKeyFromPresignedUrl(url: string): string { export function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url); const u = new URL(url)
const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash const rawKey = u.pathname.replace(/^\/+/, '') // remove leading slash
const decodedKey = decodeURIComponent(rawKey); const decodedKey = decodeURIComponent(rawKey)
// Remove bucket prefix // Remove bucket prefix
const parts = decodedKey.split('/'); const parts = decodedKey.split('/')
parts.shift(); // Remove bucket name parts.shift() // Remove bucket name
return parts.join('/'); return parts.join('/')
} }
export async function claimUploadUrl(url: string): Promise<void> { export async function claimUploadUrl(url: string): Promise<void> {
try { try {
const semiKey = extractKeyFromPresignedUrl(url); const semiKey = extractKeyFromPresignedUrl(url)
// Update status to 'claimed' if currently 'pending' // Update status to 'claimed' if currently 'pending'
const updated = await claimUploadUrlStatus(semiKey) const updated = await claimUploadUrlStatus(semiKey)
if (!updated) { if (!updated) {
throw new Error('Upload URL not found or already claimed'); throw new Error('Upload URL not found or already claimed')
} }
} catch (error) { } catch (error) {
console.error('Error claiming upload URL:', error); console.error('Error claiming upload URL:', error)
throw new Error('Failed to claim upload URL'); 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'), price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional(), marketPrice: z.number().optional(),
incrementStep: z.number().optional().default(1), 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), isSuspended: z.boolean().optional().default(false),
isFlashAvailable: z.boolean().optional().default(false), isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().optional(), flashPrice: z.number().optional(),
@ -262,7 +262,7 @@ export const productRouter = router({
price: price.toString(), price: price.toString(),
marketPrice: marketPrice?.toString(), marketPrice: marketPrice?.toString(),
incrementStep, incrementStep,
productQuantity, productQuantity: productQuantity as any,
isSuspended, isSuspended,
isFlashAvailable, isFlashAvailable,
flashPrice: flashPrice?.toString(), flashPrice: flashPrice?.toString(),
@ -302,7 +302,7 @@ export const productRouter = router({
price: z.number().positive('Price must be positive'), price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional(), marketPrice: z.number().optional(),
incrementStep: z.number().optional().default(1), 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), isSuspended: z.boolean().optional().default(false),
isFlashAvailable: z.boolean().optional().default(false), isFlashAvailable: z.boolean().optional().default(false),
flashPrice: z.number().nullable().optional(), flashPrice: z.number().nullable().optional(),
@ -347,7 +347,7 @@ export const productRouter = router({
price: price.toString(), price: price.toString(),
marketPrice: marketPrice?.toString(), marketPrice: marketPrice?.toString(),
incrementStep, incrementStep,
productQuantity, productQuantity: productQuantity as any,
isSuspended, isSuspended,
isFlashAvailable, isFlashAvailable,
flashPrice: flashPrice?.toString() ?? null, flashPrice: flashPrice?.toString() ?? null,

View file

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

View file

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

View file

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

View file

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

View file

@ -163,13 +163,38 @@ type ProductInfoInsert = InferInsertModel<typeof productInfo>
type ProductInfoUpdate = Partial<ProductInfoInsert> type ProductInfoUpdate = Partial<ProductInfoInsert>
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> { 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) return mapProduct(product)
} }
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> { 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) const [product] = await db.update(productInfo)
.set(updates) .set(safeUpdates)
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
.returning() .returning()
if (!product) { if (!product) {

View file

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