UNPKG

everything-dev

Version:

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

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