everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
187 lines (185 loc) • 7.53 kB
JavaScript
const require_core = require('./core.cjs');
let node_fs = require("node:fs");
let node_path = require("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_${require_core.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://") (0, node_fs.mkdirSync)((0, node_path.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
exports.createDatabaseDriver = createDatabaseDriver;
exports.isRailwayPublicProxyUrl = isRailwayPublicProxyUrl;
exports.pluginSchemaName = pluginSchemaName;
exports.resolvePoolSsl = resolvePoolSsl;
//# sourceMappingURL=driver.cjs.map