@xemahq/biome-database-nest 0.12.1 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/biome-database-nest",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Explicit database migration ownership, least-privilege runtime verification, and schema-qualified Prisma wiring for Xema biome services.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -18,8 +18,7 @@
18
18
  "main": "dist/index.js",
19
19
  "types": "dist/index.d.ts",
20
20
  "files": [
21
- "dist",
22
- "src"
21
+ "dist"
23
22
  ],
24
23
  "devDependencies": {
25
24
  "@nestjs/common": "11.1.13",
@@ -35,8 +34,8 @@
35
34
  "ts-jest": "^29.4.9",
36
35
  "typescript": "5.9.3",
37
36
  "@xemahq/platform-common": "^0.7.4",
38
- "@xemahq/xema-decorators": "0.5.3",
39
- "@xemahq/service-registry-nest": "0.7.0"
37
+ "@xemahq/xema-decorators": "0.5.4",
38
+ "@xemahq/service-registry-nest": "0.7.1"
40
39
  },
41
40
  "peerDependencies": {
42
41
  "@nestjs/common": "^10 || ^11",
package/src/index.ts DELETED
@@ -1,115 +0,0 @@
1
- export { DEFAULT_BIOME_DB_KEY, deriveBiomeSchemaName } from './lib/schema-name';
2
- export {
3
- composeIdentifier,
4
- fitIdentifier,
5
- IDENTIFIER_HASH_LENGTH,
6
- POSTGRES_MAX_IDENTIFIER_LENGTH,
7
- sanitizeIdentifierChars,
8
- sanitizeIdentifierSegment,
9
- type ComposeIdentifierInput,
10
- type FitIdentifierInput,
11
- } from './lib/identifier';
12
- export {
13
- rolesForSchema,
14
- schemaRoleBase,
15
- schemaRoleName,
16
- SchemaRoleKind,
17
- SCHEMA_ROLE_BASE_MAX_LENGTH,
18
- SCHEMA_ROLE_SUFFIX_LENGTH,
19
- type SchemaRoleNames,
20
- } from './lib/role-names';
21
- export {
22
- resolveBiomeDatabaseConfigFromEnv,
23
- type BiomeDatabaseConfig,
24
- } from './lib/config';
25
- export {
26
- createBiomePrismaAdapter,
27
- createBiomePrismaAdapterFromEnv,
28
- } from './lib/adapter';
29
- export {
30
- fetchSystemDatabaseConfig,
31
- type SystemDatabaseConfigRequest,
32
- } from './lib/control-plane-client';
33
- export {
34
- resolveControlPlaneBaseUrl,
35
- ORG_DATABASE_POOL_SERVICE_NAME,
36
- type ControlPlaneDiscoveryOptions,
37
- } from './lib/control-plane-discovery';
38
- export {
39
- bootstrapBiomeDatabases,
40
- type BiomeDatabaseDeclaration,
41
- type BootstrapBiomeDatabasesOptions,
42
- } from './lib/bootstrap';
43
- export { BiomeDatabaseMigrationMode } from './lib/migration-mode';
44
- export {
45
- BiomeDatabaseModule,
46
- BIOME_DATABASE_READY,
47
- type ApplicationManagedBiomeDatabaseModuleOptions,
48
- type BiomeDatabaseModuleOptions,
49
- } from './lib/nest/biome-database.module';
50
- export {
51
- createBiomePrismaService,
52
- type BiomePrismaClient,
53
- type BiomePrismaClientCtor,
54
- type BiomePrismaServiceInstance,
55
- type CreateBiomePrismaServiceOptions,
56
- type TenantIsolationServiceOptions,
57
- type TenantScopedApi,
58
- } from './lib/nest/prisma-service-factory';
59
- export {
60
- TenantIsolationMode,
61
- TENANT_ISOLATION_MODE_ENV_VAR,
62
- resolveTenantIsolationMode,
63
- } from './lib/tenant-isolation/tenant-isolation-mode';
64
- export {
65
- TenantIsolationError,
66
- TenantIsolationViolationKind,
67
- formatTenantIsolationViolation,
68
- type TenantIsolationViolation,
69
- } from './lib/tenant-isolation/tenant-isolation-error';
70
- export {
71
- PrismaModelOperation,
72
- scopeArgsToOrg,
73
- type OrgScopeOutcome,
74
- type OrgScopeRequest,
75
- } from './lib/tenant-isolation/org-scope';
76
- export {
77
- DEFAULT_ORG_FIELD,
78
- collectOrgScopedModels,
79
- createTenantIsolationExtension,
80
- readRuntimeModels,
81
- type CreateTenantIsolationExtensionOptions,
82
- type RuntimeDataModelField,
83
- type RuntimeDataModelModel,
84
- type TenantIsolationExtensionDefinition,
85
- type TenantIsolationLogger,
86
- type TenantIsolationQueryContext,
87
- } from './lib/tenant-isolation/tenant-isolation.extension';
88
- export {
89
- assertUnscopedModelsDeclared,
90
- collectUnscopableModels,
91
- formatUnscopedModelReport,
92
- normalizeUnscopedModelDeclarations,
93
- UndeclaredUnscopedModelsError,
94
- type AssertUnscopedModelsOptions,
95
- type NormalizedUnscopedModelDeclaration,
96
- type UnscopedModelDeclaration,
97
- type UnscopedModelDeclarations,
98
- } from './lib/tenant-isolation/unscoped-models';
99
- export {
100
- assertOrgErasureMatchesModels,
101
- eraseOrgData,
102
- OrgErasureBlockedError,
103
- OrgErasureMatchesNothingError,
104
- type EraseOrgDataOptions,
105
- type OrgErasureReport,
106
- } from './lib/tenant-isolation/org-erasure';
107
- export {
108
- OrgErasureModule,
109
- OrgErasureController,
110
- ORG_ERASURE_RUNNER,
111
- ORG_DATA_ERASURE_CAPABILITY,
112
- ORG_ERASURE_ROUTE_PATH,
113
- type OrgErasureModuleOptions,
114
- type OrgErasureRunner,
115
- } from './lib/nest/org-erasure.module';
@@ -1,117 +0,0 @@
1
- import { PrismaPg } from '@prisma/adapter-pg';
2
- import {
3
- composeBiomeDatabaseUrl,
4
- ensureBiomeDbSearchPath,
5
- } from '@xemahq/platform-common';
6
-
7
- import { resolveBiomeDatabaseConfigFromEnv } from './config';
8
-
9
- import type { BiomeDatabaseConfig } from './config';
10
-
11
- const DEFAULT_POOL_MAX = 3;
12
-
13
- /**
14
- * Build a connection string from a {@link BiomeDatabaseConfig}, with `?schema=`
15
- * when a per-biome schema is set and the libpq `search_path` option applied so
16
- * RAW queries also resolve to the biome schema.
17
- *
18
- * @throws if the config is missing a part required to form a URL.
19
- */
20
- function buildBiomeConnectionString(config: BiomeDatabaseConfig): string {
21
- const url = composeBiomeDatabaseUrl({
22
- user: config.username,
23
- password: config.password,
24
- host: config.host,
25
- port: String(config.port),
26
- database: config.database,
27
- ...(config.schema !== undefined ? { schema: config.schema } : {}),
28
- ...(config.sslParams !== undefined ? { sslParams: config.sslParams } : {}),
29
- });
30
- if (url === null) {
31
- throw new Error(
32
- 'createBiomePrismaAdapter: incomplete BiomeDatabaseConfig — ' +
33
- 'host/port/database/username/password are all required.',
34
- );
35
- }
36
- return ensureBiomeDbSearchPath(url);
37
- }
38
-
39
- /**
40
- * Construct a {@link PrismaPg} driver-adapter pool whose Prisma Client qualifies
41
- * ORM table names with the biome schema.
42
- *
43
- * THIS IS THE CENTRALIZED FIX. `@prisma/adapter-pg` (Prisma 7) ignores BOTH the
44
- * URL `?schema=` and the connection `search_path` when qualifying ORM-emitted
45
- * SQL — it falls back to `public.<table>` unless the schema is passed as the
46
- * SECOND constructor argument. Passing `{ schema }` here makes Prisma emit
47
- * `biome_<id>.<table>`. `ensureBiomeDbSearchPath` separately fixes RAW queries.
48
- * Constructing the adapter correctly is the ONLY thing this function does, so
49
- * the correct construction is the only construction a biome can perform.
50
- */
51
- export function createBiomePrismaAdapter(
52
- config: BiomeDatabaseConfig,
53
- ): PrismaPg {
54
- const connectionString = buildBiomeConnectionString(config);
55
- return new PrismaPg(
56
- {
57
- connectionString,
58
- min: 1,
59
- max: config.poolMax ?? DEFAULT_POOL_MAX,
60
- idleTimeoutMillis: 30_000,
61
- },
62
- config.schema !== undefined ? { schema: config.schema } : undefined,
63
- );
64
- }
65
-
66
- /**
67
- * Construct the schema-qualified adapter from the environment — the path the
68
- * unchanged-shape `PrismaService` calls at construction time (post-bootstrap,
69
- * where `DATABASE_URL`/`DB_*` are guaranteed set).
70
- *
71
- * Resolution order:
72
- * 1. If `DATABASE_URL` (or `DATABASE_URL_<KEY>` for a non-default key) is set,
73
- * use it directly as the connection string (still applying
74
- * `ensureBiomeDbSearchPath` + the `{ schema }` second-arg from `DB_SCHEMA`).
75
- * 2. Otherwise compose from the discrete `DB_*` vars.
76
- *
77
- * @throws if neither a `DATABASE_URL` nor a complete `DB_*` set is present.
78
- */
79
- export function createBiomePrismaAdapterFromEnv(
80
- env: NodeJS.ProcessEnv = process.env,
81
- dbKey?: string,
82
- ): PrismaPg {
83
- const isDefault = dbKey === undefined || dbKey === '' || dbKey === 'primary';
84
- const upperSuffix = isDefault
85
- ? ''
86
- : `_${dbKey.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
87
-
88
- const directUrl = env[`DATABASE_URL${upperSuffix}`];
89
- const schema = env[`DB_SCHEMA${upperSuffix}`];
90
- const poolMaxRaw = env[`DB_POOL_MAX${upperSuffix}`] ?? env.DB_POOL_MAX;
91
- const poolMax =
92
- poolMaxRaw !== undefined && poolMaxRaw !== ''
93
- ? Number.parseInt(poolMaxRaw, 10)
94
- : DEFAULT_POOL_MAX;
95
-
96
- if (directUrl !== undefined && directUrl !== '') {
97
- const connectionString = ensureBiomeDbSearchPath(directUrl);
98
- return new PrismaPg(
99
- {
100
- connectionString,
101
- min: 1,
102
- max: poolMax,
103
- idleTimeoutMillis: 30_000,
104
- },
105
- schema !== undefined && schema !== '' ? { schema } : undefined,
106
- );
107
- }
108
-
109
- const config = resolveBiomeDatabaseConfigFromEnv(env, dbKey);
110
- if (config === null) {
111
- throw new Error(
112
- `createBiomePrismaAdapterFromEnv: no DATABASE_URL${upperSuffix} and no ` +
113
- `DB_HOST${upperSuffix}/DB_HOST in the environment — cannot construct an adapter.`,
114
- );
115
- }
116
- return createBiomePrismaAdapter(config);
117
- }
@@ -1,428 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
- import { writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
-
7
- import { Logger } from '@nestjs/common';
8
- import { PrismaMigrationRunner } from '@xemahq/migration-runner-prisma';
9
- import {
10
- composeBiomeDatabaseUrl,
11
- ensureBiomeDbSearchPath,
12
- } from '@xemahq/platform-common';
13
- import { Client } from 'pg';
14
-
15
- import { resolveBiomeDatabaseConfigFromEnv } from './config';
16
- import { fetchSystemDatabaseConfig } from './control-plane-client';
17
- import {
18
- resolveControlPlaneBaseUrl,
19
- type ControlPlaneDiscoveryOptions,
20
- } from './control-plane-discovery';
21
- import { DEFAULT_BIOME_DB_KEY } from './schema-name';
22
-
23
- import type { BiomeDatabaseConfig } from './config';
24
- import type { BiomeDatabaseMigrationMode } from './migration-mode';
25
- import type { KernelState } from '@xemahq/kernel-contracts/kernel-state';
26
-
27
- /** One database whose schema and Prisma migrations the application owns. */
28
- export interface BiomeDatabaseDeclaration {
29
- /** Defaults to `'primary'`. */
30
- readonly key?: string;
31
- /** Directory containing `prisma/` (cwd for `prisma migrate deploy`). */
32
- readonly workspaceDir: string;
33
- }
34
-
35
-
36
- interface BootstrapBiomeDatabasesCommonOptions {
37
- readonly biomeId: string;
38
- /**
39
- * A service bearer, or a factory that mints one. Required whenever the
40
- * selected credential path is the control plane. Its address is discovered
41
- * from the service registry via {@link kernelState}.
42
- */
43
- readonly serviceToken?: string | (() => Promise<string>);
44
- /**
45
- * The app's `KernelState` backend (injected via `KERNEL_STATE_TOKEN`). Used to
46
- * discover the `org-database-pool-api` control-plane address from the service
47
- * registry. Required on the control-plane path; unused when every declared
48
- * database explicitly resolves from the environment.
49
- */
50
- readonly kernelState: KernelState;
51
- /** Optional bounded-backoff tuning for control-plane discovery. */
52
- readonly controlPlaneDiscovery?: ControlPlaneDiscoveryOptions;
53
- }
54
-
55
- export interface ApplicationManagedBootstrapOptions
56
- extends BootstrapBiomeDatabasesCommonOptions {
57
- readonly migrationMode: BiomeDatabaseMigrationMode.ApplicationManaged;
58
- readonly databases: readonly BiomeDatabaseDeclaration[];
59
- /**
60
- * Postgres extensions this biome needs, ensured in the SHARED `public` schema
61
- * BEFORE migrations run (e.g. `['vector']` for pgvector). A Postgres
62
- * extension can exist in only ONE schema per database, so installing it in
63
- * each biome's own schema would make whichever biome migrates first win and
64
- * every later biome fail to resolve the type. Installing in `public` (which
65
- * every biome's search_path includes) keeps it shared. `CREATE EXTENSION IF
66
- * NOT EXISTS` is idempotent; only declare extensions the biome actually uses
67
- * so non-Postgres-extension environments (local dev) are unaffected.
68
- */
69
- readonly requiredExtensions?: readonly string[];
70
- }
71
-
72
-
73
- export type BootstrapBiomeDatabasesOptions =
74
- ApplicationManagedBootstrapOptions;
75
-
76
- const LOG_PREFIX = '[biome-database]';
77
-
78
- /**
79
- * Boot-progress sink. A published SDK must not write to the host's raw
80
- * stdout: this bootstrap runs from a service's `main.ts`, so its output
81
- * belongs on the same Nest logger transport as the rest of that service's
82
- * boot. `Logger`'s static/instance API works before `NestFactory.create`,
83
- * which is exactly when this runs.
84
- */
85
- const bootstrapLogger = new Logger('BiomeDatabaseBootstrap');
86
-
87
- function upperKeySuffix(key: string): string {
88
- return key === DEFAULT_BIOME_DB_KEY
89
- ? ''
90
- : `_${key.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
91
- }
92
-
93
- async function resolveServiceToken(
94
- serviceToken: string | (() => Promise<string>) | undefined,
95
- ): Promise<string> {
96
- if (serviceToken === undefined) {
97
- throw new Error(
98
- `${LOG_PREFIX} no local DB_* override and no serviceToken supplied — ` +
99
- 'cannot authenticate to the control plane.',
100
- );
101
- }
102
- return typeof serviceToken === 'function' ? serviceToken() : serviceToken;
103
- }
104
-
105
- /**
106
- * Resolve config for one database: local `DB_*` override FIRST, control plane
107
- * as the fallback. Throws if neither yields a config (no silent default). The
108
- * control-plane base URL is obtained via {@link getControlPlaneBaseUrl}, which
109
- * discovers `org-database-pool-api` from the service registry (memoized across
110
- * databases so discovery runs at most once per bootstrap).
111
- */
112
- async function resolveDatabaseConfig(
113
- opts: BootstrapBiomeDatabasesCommonOptions,
114
- key: string,
115
- getControlPlaneBaseUrl: () => Promise<string>,
116
- ): Promise<BiomeDatabaseConfig> {
117
- const local = resolveBiomeDatabaseConfigFromEnv(process.env, key);
118
- if (local !== null) {
119
- return local;
120
- }
121
- const baseUrl = await getControlPlaneBaseUrl();
122
- const token = await resolveServiceToken(opts.serviceToken);
123
- return fetchSystemDatabaseConfig(baseUrl, token, {
124
- biomeId: opts.biomeId,
125
- ...(key !== DEFAULT_BIOME_DB_KEY ? { dbKey: key } : {}),
126
- });
127
- }
128
-
129
-
130
- /**
131
- * If the config carries an inline CA PEM, write it to a deterministic temp file
132
- * and rewrite `sslParams` so `sslrootcert` points at it. Returns the config to
133
- * use downstream. When no inline CA is present, the config is returned as-is
134
- * (any already-mounted `sslrootcert` path in `sslParams` is preserved).
135
- */
136
- async function materializeInlineCaCert(
137
- biomeId: string,
138
- key: string,
139
- config: BiomeDatabaseConfig,
140
- ): Promise<BiomeDatabaseConfig> {
141
- if (config.caCertPem === undefined || config.caCertPem === '') {
142
- return config;
143
- }
144
- const hash = createHash('sha256')
145
- .update(`${biomeId}:${key}`)
146
- .digest('hex')
147
- .slice(0, 16);
148
- const certPath = join(tmpdir(), `xema-biome-db-ca-${hash}.crt`);
149
- await writeFile(certPath, config.caCertPem, { mode: 0o600 });
150
-
151
- const params = new URLSearchParams(config.sslParams ?? '');
152
- params.set('sslrootcert', certPath);
153
- if (!params.has('sslmode')) {
154
- params.set('sslmode', 'verify-full');
155
- }
156
- bootstrapLogger.log(
157
- `${LOG_PREFIX} biome=${biomeId} key=${key}: wrote inline CA cert to ${certPath}`,
158
- );
159
- return { ...config, sslParams: params.toString() };
160
- }
161
-
162
- /**
163
- * Ensure `config.schema` exists. No-op when the config has no schema (dedicated
164
- * DB). Uses a per-schema advisory lock inside a txn so concurrent replicas
165
- * serialize the `CREATE SCHEMA IF NOT EXISTS`.
166
- */
167
- /**
168
- * Build the node-postgres `ssl` option from the config's `sslParams` /
169
- * `caCertPem`. Managed Postgres (e.g. DigitalOcean) REQUIRES TLS — a raw
170
- * `Client` with no `ssl` is rejected at connect. We mirror libpq `sslmode`:
171
- * - absent / `disable` → no TLS
172
- * - `require` / `prefer` → encrypt, do NOT verify the chain/host
173
- * - `verify-ca` / `verify-full` → verify against the CA (the CA comes from
174
- * inline `caCertPem` or the `sslrootcert`
175
- * file path in `sslParams`)
176
- */
177
- function buildPgSsl(
178
- config: BiomeDatabaseConfig,
179
- ): false | { ca?: string; rejectUnauthorized: boolean } {
180
- const params = new URLSearchParams(config.sslParams ?? '');
181
- const sslmode = params.get('sslmode');
182
- if (sslmode === null || sslmode === 'disable') {
183
- return false;
184
- }
185
- const verify = sslmode === 'verify-ca' || sslmode === 'verify-full';
186
- const caPath = params.get('sslrootcert');
187
- const ca =
188
- config.caCertPem ?? (caPath ? readFileSync(caPath, 'utf8') : undefined);
189
- return {
190
- ...(ca !== undefined ? { ca } : {}),
191
- rejectUnauthorized: verify,
192
- };
193
- }
194
-
195
- function isValidExtensionName(name: string): boolean {
196
- return /^[a-z0-9_]+$/i.test(name);
197
- }
198
-
199
- async function ensureSchemaExists(
200
- biomeId: string,
201
- key: string,
202
- config: BiomeDatabaseConfig,
203
- requiredExtensions: readonly string[],
204
- ): Promise<void> {
205
- const schema = config.schema;
206
- const hasSchema = schema !== undefined && schema !== '';
207
- if (!hasSchema && requiredExtensions.length === 0) {
208
- return;
209
- }
210
- const client = new Client({
211
- host: config.host,
212
- port: config.port,
213
- database: config.database,
214
- user: config.username,
215
- password: config.password,
216
- ssl: buildPgSsl(config),
217
- });
218
- await client.connect();
219
- try {
220
- // Shared extensions FIRST, in `public` (a Postgres extension can live in
221
- // only ONE schema per DB; `public` is in every biome's search_path so the
222
- // type/ops resolve everywhere). Idempotent; runs before migrate so a
223
- // migration's own `CREATE EXTENSION IF NOT EXISTS` no-ops.
224
- for (const ext of requiredExtensions) {
225
- if (!isValidExtensionName(ext)) {
226
- throw new Error(
227
- `${LOG_PREFIX} biome=${biomeId}: invalid extension name "${ext}".`,
228
- );
229
- }
230
- await client.query(
231
- `CREATE EXTENSION IF NOT EXISTS "${ext}" SCHEMA public`,
232
- );
233
- bootstrapLogger.log(
234
- `${LOG_PREFIX} biome=${biomeId} key=${key}: ensured extension "${ext}" in public`,
235
- );
236
- }
237
- if (hasSchema) {
238
- await client.query('BEGIN');
239
- await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
240
- schema,
241
- ]);
242
- await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
243
- await client.query('COMMIT');
244
- bootstrapLogger.log(
245
- `${LOG_PREFIX} biome=${biomeId} key=${key}: ensured schema "${schema}" exists`,
246
- );
247
- }
248
- } catch (err) {
249
- await client.query('ROLLBACK').catch(() => undefined);
250
- throw err;
251
- } finally {
252
- await client.end();
253
- }
254
- }
255
-
256
- /** Build the direct connection URL for a resolved database config. */
257
- function buildDirectConnectionUrl(
258
- biomeId: string,
259
- key: string,
260
- config: BiomeDatabaseConfig,
261
- ): string {
262
- const directUrl = composeBiomeDatabaseUrl({
263
- user: config.username,
264
- password: config.password,
265
- host: config.host,
266
- port: String(config.port),
267
- database: config.database,
268
- ...(config.schema !== undefined ? { schema: config.schema } : {}),
269
- ...(config.sslParams !== undefined ? { sslParams: config.sslParams } : {}),
270
- });
271
- if (directUrl === null) {
272
- throw new Error(
273
- `${LOG_PREFIX} biome=${biomeId} key=${key}: incomplete config — cannot form a database URL.`,
274
- );
275
- }
276
- return ensureBiomeDbSearchPath(directUrl);
277
- }
278
-
279
- async function runMigrations(
280
- biomeId: string,
281
- key: string,
282
- workspaceDir: string,
283
- migrateUrl: string,
284
- ): Promise<void> {
285
- bootstrapLogger.log(
286
- `${LOG_PREFIX} biome=${biomeId} key=${key}: running prisma migrate deploy (cwd=${workspaceDir})`,
287
- );
288
- // The migrate connection MUST carry `public` in its search_path (not just the
289
- // biome schema) so migrations resolve SHARED objects that live in `public` —
290
- // notably the pgvector `vector`/`halfvec` types and their operator classes.
291
- // pgvector can only be installed in ONE schema per database; the shared
292
- // xema_biomes DB keeps it in `public`. Without `,public` here, a biome whose
293
- // schema is the migrate target cannot see `public.vector` and CREATE TABLE
294
- // with a `vector` column fails (`type "vector" does not exist`). The runtime
295
- // URL already gets this via `ensureBiomeDbSearchPath`; the migrate URL needs
296
- // it too. `ensureBiomeDbSearchPath` is idempotent.
297
- const result = await new PrismaMigrationRunner().run({
298
- workspaceDir,
299
- connectionUrl: migrateUrl,
300
- env: {},
301
- });
302
- if (result.exitCode !== 0) {
303
- throw new Error(
304
- `${LOG_PREFIX} biome=${biomeId} key=${key}: migrations failed (exit ${result.exitCode}):\n${result.output}`,
305
- );
306
- }
307
- }
308
-
309
- /**
310
- * Build the STEADY-STATE runtime URL. When the config declares a transaction
311
- * pooler (`poolerHost`), the runtime connection is routed through it — the fix
312
- * for connection fan-out across many replicas × biomes — while `migrateUrl`
313
- * (direct) is what schema-create and `prisma migrate deploy` already used.
314
- *
315
- * The app→pooler leg is normally in-cluster plaintext (`sslmode=disable` by
316
- * default); the pooler terminates the managed-Postgres TLS on its own upstream
317
- * leg. Both URLs carry the libpq `search_path` option (via
318
- * `ensureBiomeDbSearchPath`) so RAW queries resolve to the biome schema — the
319
- * pooler must be configured to track `search_path` per-client (PgBouncer ≥1.23
320
- * `track_extra_parameters = search_path`) so transaction pooling stays correct.
321
- *
322
- * No pooler declared → returns the direct runtime URL (current behavior).
323
- */
324
- function buildRuntimeUrl(
325
- biomeId: string,
326
- key: string,
327
- config: BiomeDatabaseConfig,
328
- directUrl: string,
329
- ): string {
330
- if (config.poolerHost === undefined || config.poolerHost === '') {
331
- return ensureBiomeDbSearchPath(directUrl);
332
- }
333
- const pooledUrl = composeBiomeDatabaseUrl({
334
- user: config.username,
335
- password: config.password,
336
- host: config.poolerHost,
337
- port: String(config.poolerPort ?? config.port),
338
- database: config.database,
339
- ...(config.schema !== undefined ? { schema: config.schema } : {}),
340
- sslParams: config.poolerSslParams ?? 'sslmode=disable',
341
- });
342
- if (pooledUrl === null) {
343
- throw new Error(
344
- `${LOG_PREFIX} biome=${biomeId} key=${key}: incomplete config — cannot form a pooled runtime URL.`,
345
- );
346
- }
347
- bootstrapLogger.log(
348
- `${LOG_PREFIX} biome=${biomeId} key=${key}: runtime routed through pooler ${config.poolerHost}:${config.poolerPort ?? config.port}`,
349
- );
350
- return ensureBiomeDbSearchPath(pooledUrl);
351
- }
352
-
353
-
354
-
355
-
356
- /**
357
- * Export the resolved connection into the env the unchanged-shape
358
- * `PrismaService` reads. The primary DB lands on `DATABASE_URL`/`DB_SCHEMA`;
359
- * additional keys land on `DATABASE_URL_<KEY>`/`DB_SCHEMA_<KEY>`.
360
- */
361
- function exportResolvedEnv(
362
- key: string,
363
- config: BiomeDatabaseConfig,
364
- runtimeUrl: string,
365
- ): void {
366
- const suffix = upperKeySuffix(key);
367
- process.env[`DATABASE_URL${suffix}`] = runtimeUrl;
368
- if (config.schema !== undefined && config.schema !== '') {
369
- process.env[`DB_SCHEMA${suffix}`] = config.schema;
370
- }
371
- }
372
-
373
- /**
374
- * Bootstrap every declared biome database according to its explicit migration
375
- * ownership mode.
376
- *
377
- * Application-managed mode retains schema/extension creation and Prisma
378
- * migration execution. Externally-managed mode performs no DDL and never
379
- * invokes the migration runner: it resolves only the explicitly selected
380
- * runtime credential, proves its least-privilege contract and migration
381
- * compatibility, then exports the verified runtime URL.
382
- */
383
- export async function bootstrapBiomeDatabases(
384
- opts: BootstrapBiomeDatabasesOptions,
385
- ): Promise<void> {
386
- // Discover the control-plane address at most once, and only when a database
387
- // actually needs it (i.e. has no local DB_* override).
388
- let cachedBaseUrl: string | undefined;
389
- const getControlPlaneBaseUrl = async (): Promise<string> => {
390
- if (cachedBaseUrl === undefined) {
391
- cachedBaseUrl = await resolveControlPlaneBaseUrl(
392
- opts.kernelState,
393
- opts.controlPlaneDiscovery,
394
- );
395
- }
396
- return cachedBaseUrl;
397
- };
398
-
399
- for (const decl of opts.databases) {
400
- const key = decl.key ?? DEFAULT_BIOME_DB_KEY;
401
- bootstrapLogger.log(
402
- `${LOG_PREFIX} biome=${opts.biomeId} key=${key}: resolving config for mode=${opts.migrationMode}`,
403
- );
404
- const rawConfig = await resolveDatabaseConfig(
405
- opts,
406
- key,
407
- getControlPlaneBaseUrl,
408
- );
409
- const config = await materializeInlineCaCert(
410
- opts.biomeId,
411
- key,
412
- rawConfig,
413
- );
414
- const directUrl = buildDirectConnectionUrl(opts.biomeId, key, config);
415
- await ensureSchemaExists(
416
- opts.biomeId,
417
- key,
418
- config,
419
- opts.requiredExtensions ?? [],
420
- );
421
- await runMigrations(opts.biomeId, key, decl.workspaceDir, directUrl);
422
- const runtimeUrl = buildRuntimeUrl(opts.biomeId, key, config, directUrl);
423
- exportResolvedEnv(key, config, runtimeUrl);
424
- bootstrapLogger.log(
425
- `${LOG_PREFIX} biome=${opts.biomeId} key=${key}: bootstrap complete`,
426
- );
427
- }
428
- }