Commit 29bdd883 authored by ThinhNC's avatar ThinhNC

feat(infra): integrate Cloudflare Hyperdrive with Supabase and Prisma adapter

parent ee57c7af
...@@ -8,3 +8,6 @@ storage/receipts/* ...@@ -8,3 +8,6 @@ storage/receipts/*
!storage/receipts/.gitkeep !storage/receipts/.gitkeep
.DS_Store .DS_Store
.agents/local.md .agents/local.md
.wrangler/
.dev.vars
...@@ -20,11 +20,15 @@ ...@@ -20,11 +20,15 @@
"format": "prettier --write .", "format": "prettier --write .",
"test": "jest --runInBand", "test": "jest --runInBand",
"test:watch": "jest --watch --runInBand", "test:watch": "jest --watch --runInBand",
"test:cov": "jest --coverage --runInBand" "test:cov": "jest --coverage --runInBand",
"wrangler:dev": "wrangler dev",
"wrangler:dev:remote": "wrangler dev --remote",
"wrangler:deploy": "wrangler deploy"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1107.0", "@aws-sdk/client-s3": "^3.1107.0",
"@aws-sdk/s3-request-presigner": "^3.1107.0", "@aws-sdk/s3-request-presigner": "^3.1107.0",
"@prisma/adapter-pg": "^5.22.0",
"@prisma/client": "^5.22.0", "@prisma/client": "^5.22.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
...@@ -37,10 +41,12 @@ ...@@ -37,10 +41,12 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^2.2.0", "multer": "^2.2.0",
"nodemailer": "^9.0.3", "nodemailer": "^9.0.3",
"pg": "^8.23.0",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^5.20260820.1",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10", "@types/cookie-parser": "^1.4.10",
...@@ -52,6 +58,7 @@ ...@@ -52,6 +58,7 @@
"@types/multer": "^2.2.0", "@types/multer": "^2.2.0",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/nodemailer": "^8.0.1", "@types/nodemailer": "^8.0.1",
"@types/pg": "^8.23.1",
"@types/supertest": "^7.2.1", "@types/supertest": "^7.2.1",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"eslint": "^9.17.0", "eslint": "^9.17.0",
...@@ -64,7 +71,8 @@ ...@@ -64,7 +71,8 @@
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.66.0" "typescript-eslint": "^8.66.0",
"wrangler": "^4.124.0"
}, },
"prisma": { "prisma": {
"seed": "tsx prisma/seed.ts" "seed": "tsx prisma/seed.ts"
......
This diff is collapsed.
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
} }
datasource db { datasource db {
......
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { Hyperdrive, WorkerEnv } from '../types/worker-env';
export const prisma = new PrismaClient({ const isDev = process.env.NODE_ENV === 'development';
log: process.env.NODE_ENV === 'development' const logOptions: ('query' | 'error' | 'warn')[] = isDev
? ['query', 'error', 'warn'] ? ['query', 'error', 'warn']
: ['error', 'warn'], : ['error', 'warn'];
/**
* Tạo PrismaClient từ Hyperdrive hoặc PostgreSQL connection string qua Driver Adapter
*/
export function createHyperdrivePrismaClient(
hyperdriveOrConnectionString: Hyperdrive | string,
): PrismaClient {
const connectionString =
typeof hyperdriveOrConnectionString === 'string'
? hyperdriveOrConnectionString
: hyperdriveOrConnectionString.connectionString;
const pool = new Pool({
connectionString,
});
const adapter = new PrismaPg(pool);
return new PrismaClient({
adapter,
log: logOptions,
});
}
// Global cached client instance cho Worker isolates
let cachedWorkerPrisma: PrismaClient | null = null;
let defaultNodePrisma: PrismaClient | null = null;
let currentWorkerEnv: WorkerEnv | null = null;
export function setWorkerEnv(env: WorkerEnv) {
currentWorkerEnv = env;
if (env?.HYPERDRIVE?.connectionString) {
if (!cachedWorkerPrisma) {
cachedWorkerPrisma = createHyperdrivePrismaClient(env.HYPERDRIVE);
}
}
}
function isEdgeRuntime(): boolean {
return (
typeof (globalThis as any).WebSocketPair !== 'undefined' ||
typeof (globalThis as any).EdgeRuntime !== 'undefined' ||
Boolean((globalThis as any).navigator?.userAgent?.includes('Cloudflare-Workers'))
);
}
/**
* Lấy PrismaClient tương thích với Cloudflare Worker Environment hoặc Node.js process
*/
export function getPrismaClient(env?: WorkerEnv): PrismaClient {
const activeEnv = env || currentWorkerEnv;
if (activeEnv?.HYPERDRIVE?.connectionString) {
if (!cachedWorkerPrisma) {
cachedWorkerPrisma = createHyperdrivePrismaClient(activeEnv.HYPERDRIVE);
}
return cachedWorkerPrisma;
}
if (isEdgeRuntime()) {
if (!cachedWorkerPrisma) {
const connStr =
process.env.DATABASE_URL ||
process.env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE ||
'';
cachedWorkerPrisma = createHyperdrivePrismaClient(connStr);
}
return cachedWorkerPrisma;
}
if (!defaultNodePrisma) {
defaultNodePrisma = new PrismaClient({
log: logOptions,
});
}
return defaultNodePrisma;
}
/**
* Lazy proxy Prisma instance cho Node.js server hoặc standard imports
*/
export const prisma = new Proxy({} as PrismaClient, {
get(_target, prop) {
const client = getPrismaClient();
const value = (client as any)[prop];
return typeof value === 'function' ? value.bind(client) : value;
},
}); });
export default prisma;
export interface Hyperdrive {
connectionString: string;
host: string;
port: number;
user: string;
password: string;
database: string;
}
export interface WorkerEnv {
HYPERDRIVE?: Hyperdrive;
NODE_ENV?: string;
PORT?: string;
DATABASE_URL?: string;
JWT_ACCESS_SECRET?: string;
JWT_REFRESH_SECRET?: string;
[key: string]: unknown;
}
import { WorkerEnv } from './types/worker-env';
import { getPrismaClient, setWorkerEnv } from './database/prisma.client';
export default {
async fetch(request: Request, env: WorkerEnv, ctx: any): Promise<Response> {
setWorkerEnv(env);
const url = new URL(request.url);
const pathname = url.pathname;
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
if (pathname === '/' || pathname === '/health' || pathname === '/api/v1/health') {
const startTime = Date.now();
let dbStatus = 'disconnected';
let dbLatencyMs = 0;
let userCount = 0;
let errorDetail: string | null = null;
try {
const prisma = getPrismaClient(env);
const dbStart = Date.now();
userCount = await prisma.user.count();
dbLatencyMs = Date.now() - dbStart;
dbStatus = 'connected';
} catch (err: any) {
dbStatus = 'error';
errorDetail = err.message || String(err);
}
const responsePayload = {
status: dbStatus === 'connected' ? 'ok' : 'degraded',
service: 'finwise-miniapp-be',
timestamp: new Date().toISOString(),
hyperdrive: {
configured: Boolean(env.HYPERDRIVE),
status: dbStatus,
latencyMs: dbLatencyMs,
userCount: dbStatus === 'connected' ? userCount : undefined,
error: errorDetail,
},
uptimeMs: Date.now() - startTime,
};
return new Response(JSON.stringify(responsePayload, null, 2), {
status: dbStatus === 'connected' ? 200 : 503,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
},
});
}
return new Response(
JSON.stringify({
message: 'FinWise Cloudflare Hyperdrive Edge Service',
path: pathname,
hyperdrive: Boolean(env.HYPERDRIVE),
endpoints: ['/health', '/api/v1/health'],
}),
{
status: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
},
},
);
},
};
name = "finwise-miniapp-be"
main = "src/worker.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
# Cloudflare Hyperdrive kết nối Supabase Postgres
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "2e364c7ca4ef490da2e03e40a8b4cb32"
localConnectionString = "postgresql://postgres:Finwise2026Project@db.defnmzktqqqucspyjbvb.supabase.co:5432/postgres"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment