diff --git a/bun.lock b/bun.lock index d04681d..f65db68 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,7 @@ "@upstash/redis": "^1.35.3", "@upstash/search": "^0.1.5", "@upstash/workflow": "^0.2.16", + "@vercel/functions": "^3.1.4", "autoprefixer": "10.4.14", "caniuse-lite": "^1.0.30001737", "class-variance-authority": "^0.7.1", @@ -1083,6 +1084,10 @@ "@upstash/workflow": ["@upstash/workflow@0.2.16", "", { "dependencies": { "@ai-sdk/openai": "^1.2.1", "@upstash/qstash": "^2.8.1", "ai": "^4.1.54", "zod": "^3.24.1" } }, "sha512-9mqC3p5B9i92gf1KMEZhDcU5YMEut0hkWxmf2gsDcJVCcbLD958MEDT2q2wGZa7MnKAuvEOYNDSefi0x2a5pGQ=="], + "@vercel/functions": ["@vercel/functions@3.1.4", "", { "dependencies": { "@vercel/oidc": "3.0.3" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity"] }, "sha512-1dEfZkb7qxsA+ilo+1uBUCEgr7e90vHcimpDYkUB84DM051wQ5amJDk9x+cnaI29paZb5XukXwGl8yk3Udb/DQ=="], + + "@vercel/oidc": ["@vercel/oidc@3.0.3", "", {}, "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg=="], + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], diff --git a/lib/utils/useDatabase.ts b/lib/utils/useDatabase.ts index f9e2a5a..1bf330a 100644 --- a/lib/utils/useDatabase.ts +++ b/lib/utils/useDatabase.ts @@ -1,11 +1,13 @@ +import { attachDatabasePool } from "@vercel/functions"; import { sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/node-postgres"; import { err, ok, type Result } from "neverthrow"; +import { Pool } from "pg"; import type { Database } from "@/drizzle/types"; import * as schema from "../../drizzle/schema"; import { getOwner } from "./useOwner"; -const dbInstances = new Map(); +const poolInstances = new Map(); export function getDatabaseName(ownerId: string): Result { if (!ownerId.startsWith("org_") && !ownerId.startsWith("user_")) { @@ -31,19 +33,22 @@ export async function getDatabaseForOwner(ownerId: string): Promise { }, ); - const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : ""; - const connectionString = `${process.env.DATABASE_URL}/${databaseName}${sslMode}`; - - if (!dbInstances.has(ownerId)) { - dbInstances.set( - ownerId, - drizzle(connectionString, { - schema, - }), - ); + if (!poolInstances.has(ownerId)) { + const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : ""; + const connectionString = `${process.env.DATABASE_URL}/${databaseName}${sslMode}`; + + const pool = new Pool({ + connectionString, + min: 1, + idleTimeoutMillis: 5000, + connectionTimeoutMillis: 5000, + }); + + attachDatabasePool(pool); + poolInstances.set(ownerId, pool); } - return dbInstances.get(ownerId)!; + return drizzle(poolInstances.get(ownerId)!, { schema }); } export async function deleteDatabase(ownerId: string) { @@ -54,13 +59,22 @@ export async function deleteDatabase(ownerId: string) { }, ); - const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : ""; + const pool = poolInstances.get(ownerId); + if (pool) { + await pool.end(); + poolInstances.delete(ownerId); + } - const ownerDb = drizzle(`${process.env.DATABASE_URL}/manage${sslMode}`, { - schema, + const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : ""; + const managePool = new Pool({ + connectionString: `${process.env.DATABASE_URL}/manage${sslMode}`, + min: 1, + idleTimeoutMillis: 5000, + connectionTimeoutMillis: 5000, }); - // Terminate all connections to the database before dropping + const ownerDb = drizzle(managePool, { schema }); + await ownerDb.execute(sql` SELECT pg_terminate_backend(pid) FROM pg_stat_activity @@ -68,8 +82,9 @@ export async function deleteDatabase(ownerId: string) { AND pid <> pg_backend_pid() `); - // Drop the database with FORCE option (PostgreSQL 13+) await ownerDb.execute( sql`DROP DATABASE ${sql.identifier(databaseName)} WITH (FORCE)`, ); + + await managePool.end(); } diff --git a/ops/useOps.ts b/ops/useOps.ts index bd56e88..1ab6057 100644 --- a/ops/useOps.ts +++ b/ops/useOps.ts @@ -1,11 +1,30 @@ +import { attachDatabasePool } from "@vercel/functions"; import type { UserJSON } from "@clerk/nextjs/server"; import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; import * as schema from "./drizzle/schema"; import type { OpsDatabase } from "./drizzle/types"; +let opsPool: Pool | null = null; +let opsDb: OpsDatabase | null = null; + export async function getOpsDatabase(): Promise { - const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : ""; - return drizzle(`${process.env.DATABASE_URL}/manage${sslMode}`, { schema }); + if (!opsDb) { + const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : ""; + const connectionString = `${process.env.DATABASE_URL}/manage${sslMode}`; + + opsPool = new Pool({ + connectionString, + min: 1, + idleTimeoutMillis: 5000, + connectionTimeoutMillis: 5000, + }); + + attachDatabasePool(opsPool); + opsDb = drizzle(opsPool, { schema }); + } + + return opsDb; } export async function addUserToOpsDb(userData: UserJSON) { diff --git a/package.json b/package.json index f046623..b2949b3 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@upstash/redis": "^1.35.3", "@upstash/search": "^0.1.5", "@upstash/workflow": "^0.2.16", + "@vercel/functions": "^3.1.4", "autoprefixer": "10.4.14", "caniuse-lite": "^1.0.30001737", "class-variance-authority": "^0.7.1",