UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

184 lines (182 loc) • 7.39 kB
import { pluginMigrationSlug } from "./core.mjs"; import { mkdirSync } from "node:fs"; import { dirname } from "node:path"; //#region src/db/driver.ts /** * Resolve a plugin id to its data-schema name on a shared database. * * Schema names are unconditional `plugin_<slug>` for every workspace on the * shared API database. The `pluginId === "api" ? undefined` special case found * in older plugin copies is dead code (a plugin is never `api`); api's own * tables already live in `plugin_api`. */ function pluginSchemaName(pluginId) { return `plugin_${pluginMigrationSlug(pluginId)}`; } /** * Connection-level guardrails. `ALTER DATABASE ... SET` only reaches sessions * opened after it runs — pooled connections that already exist never inherit * it, so any bound must ride on every connection's startup options. Defaults: * a lock wait fails after 10s (unbounded waits wedge the whole pool silently), * a session idle inside a transaction is reaped after 30s, and statement * timeout stays off unless DB_STATEMENT_TIMEOUT_MS is set (long migrations and * analytical queries must not break). */ /** * Parses a millisecond env value for a Postgres `-c` timeout setting. * A explicit `0` disables the timeout (Postgres semantics); garbage throws — * a typo must fail the boot loudly, never silently become the default. */ function timeoutMsEnv(name, fallbackMs) { const raw = process.env[name]; if (raw === void 0 || raw.trim() === "") return fallbackMs; const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed < 0) throw new Error(`[db] Invalid ${name}="${raw}" — must be a non-negative number of milliseconds (0 disables the timeout).`); return parsed; } function connectionOptions(namespace) { return [ ...namespace ? [`-c search_path="${namespace}",public`] : [], `-c lock_timeout=${timeoutMsEnv("DB_LOCK_TIMEOUT_MS", 1e4)}`, `-c idle_in_transaction_session_timeout=${timeoutMsEnv("DB_IDLE_TX_TIMEOUT_MS", 3e4)}`, ...process.env.DB_STATEMENT_TIMEOUT_MS ? [`-c statement_timeout=${timeoutMsEnv("DB_STATEMENT_TIMEOUT_MS", 0)}`] : [] ].join(" "); } const isLocalDbUrl = (url) => url.includes("localhost") || url.includes("127.0.0.1") || url.includes("host.docker.internal") || url.includes(".railway.internal"); /** * Railway's public proxy (`*.rlwy.net`) fronts the same Postgres as the * internal hostname and presents the same self-signed certificate chain no * client CA bundle can verify — but unlike `*.railway.internal` it crosses * the public internet, so TLS must stay on (encrypted, unverified) rather * than being dropped entirely. */ function isRailwayPublicProxyUrl(url) { try { return new URL(url).hostname.toLowerCase().endsWith(".rlwy.net"); } catch { return false; } } /** * TLS behavior for a non-local Postgres URL, following libpq `sslmode` * semantics from the connection string itself: * * - `disable` → no TLS * - `prefer`/`allow`/`require` → TLS, but the server's certificate is NOT * verified (libpq: `require` encrypts, it does not trust). Managed * providers (Railway, Neon, Supabase poolers…) hand out `sslmode=require` * URLs backed by certificates no client CA bundle can verify. * - `verify-ca`/`verify-full` → TLS + certificate verification. * - absent → verification ON (secure default for bare URLs), EXCEPT Railway's * public proxy (`*.rlwy.net`), which fronts the same self-signed Postgres * as `*.railway.internal` — those URLs default to encrypted-unverified * TLS. `DB_SSL_REJECT_UNAUTHORIZED` still overrides. * * `DB_SSL_REJECT_UNAUTHORIZED` stays as an explicit operator override: it * wins over whatever the URL says (`"false"` disables verification, `"true"` * forces it). */ function resolvePoolSsl(url) { if (isLocalDbUrl(url)) return false; let mode = null; try { mode = new URL(url).searchParams.get("sslmode"); } catch { mode = null; } if (process.env.DB_SSL_REJECT_UNAUTHORIZED === "false") return { rejectUnauthorized: false }; if (process.env.DB_SSL_REJECT_UNAUTHORIZED === "true") return { rejectUnauthorized: true }; switch (mode) { case "disable": return false; case "verify-ca": case "verify-full": return { rejectUnauthorized: true }; case "prefer": case "allow": case "require": return { rejectUnauthorized: false }; default: if (isRailwayPublicProxyUrl(url)) return { rejectUnauthorized: false }; return { rejectUnauthorized: true }; } } function buildPoolConfig(url, namespace) { return { connectionString: url, ssl: resolvePoolSsl(url), max: Number(process.env.DB_POOL_MAX) || 10, connectionTimeoutMillis: Number(process.env.DB_CONNECTION_TIMEOUT_MS) || 3e4, idleTimeoutMillis: Number(process.env.DB_IDLE_TIMEOUT_MS) || 3e4, options: connectionOptions(namespace) }; } async function ensureNamespaceExists(pool, namespace) { const client = await pool.connect(); try { await client.query("BEGIN"); try { await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", ["drizzle.__drizzle_migrations"]); await client.query(`CREATE SCHEMA IF NOT EXISTS "${namespace}"`); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } } finally { client.release(); } } function createCloseHandler(pool) { let closed = false; return async () => { if (closed) return; closed = true; pool.removeAllListeners("error"); pool.removeAllListeners("connect"); await pool.end(); }; } /** * Create a drizzle database driver for a plugin, choosing the engine by URL * scheme: `pglite:` / `:memory:` → embedded PGlite, anything else → postgres * (`pg.Pool`). * * Drivers stay externalized (never statically imported, never in the Module * Federation singleton share set) — they are loaded here via dynamic `import()` * only, and `drizzle-orm` appears as a runtime import for the `drizzle` entry * points. The `namespace` option expresses every topology the platform needs: * `plugin_<slug>` on a shared database, undefined for public-schema (api) or * dedicated-database (auth) workloads. */ async function createDatabaseDriver(url, schema, namespace) { if (url.startsWith("pglite:") || url === ":memory:") { const { drizzle } = await import("drizzle-orm/pglite"); const { PGlite } = await import("@electric-sql/pglite"); const rawDir = url === ":memory:" ? ":memory:" : url.replace("pglite:", ""); const dataDir = rawDir.endsWith("/:memory:") || rawDir === ":memory:" ? "memory://" : rawDir; if (dataDir !== "memory://") mkdirSync(dirname(dataDir), { recursive: true }); const pglite = new PGlite(dataDir); if (namespace) { await pglite.exec(`CREATE SCHEMA IF NOT EXISTS "${namespace}"`); await pglite.exec(`SET search_path TO "${namespace}", public`); } return { db: drizzle(pglite, { schema }), close: async () => { await pglite.close(); } }; } const { Pool } = await import("pg"); const { drizzle } = await import("drizzle-orm/node-postgres"); const pool = new Pool(buildPoolConfig(url, namespace)); pool.on("error", (err) => { console.error("[Database] Unexpected pool error:", err.message); }); if (namespace) await ensureNamespaceExists(pool, namespace); return { db: drizzle(pool, { schema }), close: createCloseHandler(pool) }; } //#endregion export { createDatabaseDriver, isRailwayPublicProxyUrl, pluginSchemaName, resolvePoolSsl }; //# sourceMappingURL=driver.mjs.map