@xemahq/biome-database-nest 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -1,120 +1,16 @@
1
- <p align="center">
2
- <svg width="680" height="120" viewBox="0 0 680 120" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="@xemahq/biome-database-nest">
3
- <rect width="680" height="120" rx="14" fill="#0B1020"/>
4
- <g transform="translate(28,34)">
5
- <path d="M26 0 L52 15 L52 45 L26 60 L0 45 L0 15 Z" fill="#8B5CF6" opacity="0.18"/>
6
- <path d="M26 12 L41 21 L41 39 L26 48 L11 39 L11 21 Z" fill="#8B5CF6"/>
7
- </g>
8
- <text x="92" y="52" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="22" fill="#F8FAFC" font-weight="700">@xemahq/biome-database-nest</text>
9
- <text x="92" y="80" font-family="ui-sans-serif,system-ui,sans-serif" font-size="15" fill="#94A3B8">Explicit database ownership and runtime wiring for Xema biomes</text>
10
- <text x="652" y="105" text-anchor="end" font-family="ui-sans-serif,system-ui,sans-serif" font-size="12" fill="#475569">xema.dev</text>
11
- </svg>
12
- </p>
13
-
14
- <p align="center">
15
- <a href="https://xema.dev">Website</a> &middot;
16
- <a href="https://www.npmjs.com/package/@xemahq/biome-database-nest">npm</a>
17
- </p>
18
-
19
- <p align="center">
20
- <img alt="npm" src="https://img.shields.io/npm/v/%40xemahq%2Fbiome-database-nest?color=2563eb&label=npm">
21
- <img alt="license" src="https://img.shields.io/npm/l/%40xemahq%2Fbiome-database-nest?color=10b981">
22
- <img alt="types" src="https://img.shields.io/npm/types/%40xemahq%2Fbiome-database-nest?color=3178c6">
23
- </p>
24
-
25
1
  # @xemahq/biome-database-nest
26
2
 
27
3
  > Explicit database ownership and runtime wiring for Xema biomes
28
4
 
29
5
  ## Overview
30
6
 
31
- Gives a Xema biome service explicit database ownership without coupling it to a
32
- deployment environment. It supplies schema-qualified Prisma wiring, validates
33
- image/schema compatibility, and provides a governed lifecycle for data
34
- cutovers that require an operator between expansion and contraction.
7
+ Gives a biome service its own Postgres schema with schema-qualified Prisma wiring. It runs the service's migrations at boot, verifies the schema matches the image, and renews the short-lived runtime credential before it expires. It also provides an operator lifecycle for data cutovers where a destructive contraction must wait for a verified backfill.
35
8
 
36
9
  ## When to use it
37
10
 
38
11
  - Use it when a NestJS biome service stores data or owns database migrations.
39
- The service names only its resource; the database pool issues a short-lived
40
- migration credential (schema owner, direct connection) and a separate
41
- DML-only runtime credential that the module renews before it expires.
42
- An expired runtime credential refuses new connections and fails readiness.
43
- - A local `DB_*` override is a development path and is refused when
44
- `NODE_ENV=production`.
45
- - Every Postgres session must run in UTC. The database or role default must be
46
- `ALTER DATABASE … SET timezone TO 'UTC'`; the package only verifies it
47
- (`SHOW TimeZone`) on the app pool, the bootstrap and operator migrations, and
48
- refuses to start with `DatabaseSessionTimeZoneError` otherwise. It never sets
49
- the zone itself.
50
- - Use its operator lifecycle when a destructive contraction must wait for a
51
- verified backfill instead of running automatically at application startup.
52
- - Wrap the complete operator command with `withOperatorMigrationLock` so apply,
53
- verification, and contraction cannot overlap across replicas.
54
- - Use `ExpiringLeaseStore` to elect one replica for a recurring sweep whose work
55
- makes network calls. It coordinates through a lease row with an expiry rather
56
- than a session advisory lock, so nothing depends on which pooled connection
57
- served a call and no transaction is held open across the work.
58
- - Declare the Postgres extensions a service needs (`requiredExtensions`) and
59
- the bootstrap VERIFIES they are installed in `public` before migrating. It
60
- never creates one: an extension is a database-level prerequisite created by
61
- the deployment's privileged bootstrap, and an absent one refuses the boot
62
- with a message naming the extension, the database and the remedy.
63
-
64
- ## Tables this SDK owns
65
-
66
- This SDK owns `xema_operator_migrations`, the ledger of the migration runner
67
- (the way Prisma owns `_prisma_migrations`). It is the table's only creator, and
68
- the ledger records its own creation. Consumers never declare it in
69
- `schema.prisma` or in a migration.
70
-
71
- Other SDKs' persistent tables (for example the authorization-facts outbox) are
72
- created by **SDK-owned versioned migrations**. The owning SDK declares a
73
- `SdkOwnedMigrationSet` (`owner`, versioned `migrations`, each naming the tables
74
- it `creates`, and the `tables` shapes), and its Nest module lists
75
- `contributeSdkOwnedMigrations(set)` in its `providers`. `BiomeDatabaseModule`
76
- collects every contribution in the graph (one set per owner, ordered by owner)
77
- and applies them in the migration phase. That is the only way a set reaches the
78
- phase: a biome wires nothing by hand.
79
-
80
- `runBiomeMigrations` is the one migration phase (the bootstrap runs it, and so
81
- does `pg-test-harness`). Under a per-schema lock it:
82
-
83
- 1. reads the service's own migrations and refuses one that names an SDK-owned
84
- table outside a PL/pgSQL function body;
85
- 2. runs `prisma migrate deploy` (first, so a fresh schema is still empty for
86
- Prisma);
87
- 3. creates the ledger, or verifies that this SDK created it;
88
- 4. applies each SDK-owned migration once, records it in the ledger with the
89
- sha256 of its SQL, and verifies every owned table's shape.
90
-
91
- A table is never adopted. Every refusal below fails the boot with a coded error:
92
-
93
- | Code | When |
94
- | --- | --- |
95
- | `SDK_OWNED_TABLE_NOT_CREATED_BY_OWNER` | An owned table exists and the ledger has no record of its owner creating it (a service migration, a `schema.prisma` model or manual DDL created it). |
96
- | `SDK_OWNED_TABLE_SHAPE_MISMATCH` | An owned table differs from the shape its SDK declares. |
97
- | `SDK_OWNED_MIGRATION_CHANGED` | An applied migration's SQL is not the SQL this image ships. |
98
- | `SDK_OWNED_MIGRATIONS_UNKNOWN_TO_IMAGE` | The ledger records an SDK-owned migration this image does not ship (the database is newer, or the image dropped an SDK). |
99
- | `SERVICE_MIGRATION_TOUCHES_SDK_OWNED_TABLE` | A service migration names an SDK-owned table at migration time. |
100
- | `SDK_OWNED_MIGRATION_SET_INVALID` | A set is malformed (for example its SQL uses `IF NOT EXISTS`, or two owners claim one table). |
101
- | `SDK_OWNED_MIGRATIONS_NOT_APPLIED` | A module that owns tables is mounted in a graph with no migration phase. |
102
- | `SDK_OWNED_MIGRATIONS_CONFLICT` | Two different sets are contributed for one owner. |
103
-
104
- The remedy for the first three is a reset of the database schema, so the owning
105
- SDK creates and records its tables. The ledger is never edited by hand.
106
-
107
- The shape check compares each column's name, type, nullability and default, the
108
- primary key, every CHECK constraint (name and expression) and every index (name,
109
- uniqueness, key columns, predicate), and refuses a foreign key, exclusion
110
- constraint or constraint trigger on an owned table. It does not compare an
111
- index's access method, operator classes, collations or INCLUDE columns, ordinary
112
- triggers, row-security policies, privileges, the table owner or storage
113
- parameters.
114
-
115
- A released migration is immutable; a change is a new version. The package that
116
- owns a set pins `sdkOwnedMigrationDigests(set)` in a spec, so an edit fails
117
- there rather than at the boot of every deployed schema.
12
+ - Use `withOperatorMigrationLock` around an operator migration command so replicas cannot overlap.
13
+ - Use `ExpiringLeaseStore` to elect one replica for a recurring sweep without holding a transaction open.
118
14
 
119
15
  ## Installation
120
16
 
@@ -130,14 +26,6 @@ import {
130
26
  BiomeDatabaseModule,
131
27
  } from '@xemahq/biome-database-nest';
132
28
 
133
- // Mounted beside `XemaServiceModule.forBiome(<SERVICE>_BOOTSTRAP)` (a service)
134
- // or `XemaServiceModule.forWorker(<WORKER_KEY>_DESCRIPTOR)` (a worker, e.g.
135
- // `ORG_DATABASE_POOL_WORKER_DESCRIPTOR`),
136
- // which publish the manifest's `maximumPerInstance` the pool is sized from.
137
- // Without one the boot is refused; the number is never typed here.
138
- // The pool is exactly requests + activities + nexus + 2 (the SDK's own connections).
139
- // `DB_POOL_MAX_<KEY>` / `DB_POOL_MAX` is a feasibility ceiling only: below that
140
- // size the boot is refused with a `BiomeDatabasePoolOptionsError`, never shrunk.
141
29
  BiomeDatabaseModule.forRoot({
142
30
  biomeId: 'example',
143
31
  migrationMode: BiomeDatabaseMigrationMode.ApplicationManaged,
@@ -148,10 +36,48 @@ BiomeDatabaseModule.forRoot({
148
36
  });
149
37
  ```
150
38
 
39
+ Mount it beside `XemaServiceModule.forBiome(...)`, which publishes the connection count the pool is sized from.
40
+
41
+ ## Configuration
42
+
43
+ | Option | Purpose |
44
+ | --- | --- |
45
+ | `biomeId` | The biome the database belongs to. |
46
+ | `migrationMode` | `BiomeDatabaseMigrationMode.ApplicationManaged`. |
47
+ | `databases` | The databases the service owns. |
48
+ | `requiredExtensions` | Postgres extensions verified as installed in `public` before migrating; never created. |
49
+
50
+ | Environment | Purpose |
51
+ | --- | --- |
52
+ | `DB_POOL_MAX`, `DB_POOL_MAX_<KEY>` | Feasibility ceiling for the pool. Below the derived size the boot is refused. |
53
+ | `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `DB_SCHEMA`, `DB_SSL_PARAMS`, `DB_POOLER_HOST`, `DB_POOLER_PORT`, `DB_POOLER_SSL_PARAMS` | Local development connection override; refused when `NODE_ENV=production`. |
54
+ | `DATABASE_URL` | Connection string for operator migrations when none is passed. |
55
+
56
+ Every Postgres session must run in UTC. The package verifies this and refuses to start otherwise; it never sets the zone.
57
+
58
+ ## Testing
59
+
60
+ `@xemahq/pg-test-harness` (0.5 and later) provisions a scratch database but does not know how to migrate it. `createTestMigrator` from the `/testing` subpath returns the `migrate` callback it needs: the same migration phase a service runs at boot (`runBiomeMigrations`). It applies the service's Prisma migrations from `<rootDir>/prisma`, then the operator-migration ledger and the SDK-owned migrations you list, and verifies every owned table's shape. Running it again on the same schema changes nothing.
61
+
62
+ ```ts
63
+ // tests/global-setup.ts
64
+ import { createTestMigrator } from '@xemahq/biome-database-nest/testing';
65
+ import { provisionTestDatabase } from '@xemahq/pg-test-harness';
66
+
67
+ export default async function globalSetup(): Promise<void> {
68
+ await provisionTestDatabase({
69
+ serviceName: 'example',
70
+ rootDir: process.cwd(),
71
+ migrate: createTestMigrator({ sdkOwnedMigrations: [/* each SDK's migration set */] }),
72
+ });
73
+ }
74
+ ```
75
+
76
+ `createTestMigrator(options?)` takes `sdkOwnedMigrations?: readonly SdkOwnedMigrationSet[]` and returns `(target: { serviceName, rootDir, url, schema }) => Promise<void>`. It reads no environment variables.
77
+
151
78
  ## Peer requirements
152
79
 
153
- - NestJS 10 or 11 and Prisma 7.
154
- - Compatible Xema identity, kernel-contract, service-registry, and decorator packages.
80
+ NestJS 10 or 11, Prisma 7 (`@prisma/client`, `@prisma/adapter-pg`), `@xemahq/identity-client`, `@xemahq/contracts`, `@xemahq/service-registry-nest`, `@xemahq/platform-common`.
155
81
 
156
82
  ## License
157
83
 
@@ -0,0 +1,12 @@
1
+ import type { SdkOwnedMigrationSet } from '../lib/sdk-owned-migration-set';
2
+ export interface TestMigrationTarget {
3
+ readonly serviceName: string;
4
+ readonly rootDir: string;
5
+ readonly url: string;
6
+ readonly schema: string;
7
+ }
8
+ export interface CreateTestMigratorOptions {
9
+ readonly sdkOwnedMigrations?: readonly SdkOwnedMigrationSet[];
10
+ }
11
+ export declare function createTestMigrator(options?: CreateTestMigratorOptions): (target: TestMigrationTarget) => Promise<void>;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAM3E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IAExC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC/D;AAWD,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,yBAA8B,GACtC,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CA6BhD"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTestMigrator = createTestMigrator;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const biome_migrations_1 = require("../lib/biome-migrations");
7
+ const operator_migration_1 = require("../lib/operator-migration");
8
+ const SQL_IDENTIFIER = /^[a-z_][a-z0-9_]*$/iu;
9
+ function createTestMigrator(options = {}) {
10
+ return async (target) => {
11
+ const schemaPath = (0, node_path_1.join)(target.rootDir, 'prisma', 'schema.prisma');
12
+ if (!(0, node_fs_1.existsSync)(schemaPath)) {
13
+ throw new Error(`${target.serviceName}: no Prisma schema at ${schemaPath}. Pass the service root as \`rootDir\` to provisionTestDatabase.`);
14
+ }
15
+ if (!SQL_IDENTIFIER.test(target.schema)) {
16
+ throw new Error(`${target.serviceName}: refusing scratch schema ${JSON.stringify(target.schema)}; it must match ${SQL_IDENTIFIER.source} before it is interpolated into CREATE SCHEMA.`);
17
+ }
18
+ const database = await (0, operator_migration_1.openOperatorMigrationClient)({
19
+ connectionString: target.url,
20
+ });
21
+ try {
22
+ await database.query(`CREATE SCHEMA IF NOT EXISTS "${target.schema}"`);
23
+ await (0, biome_migrations_1.runBiomeMigrations)({
24
+ database,
25
+ workspaceDir: target.rootDir,
26
+ connectionUrl: target.url,
27
+ label: target.serviceName,
28
+ sdkOwnedMigrations: options.sdkOwnedMigrations ?? [],
29
+ });
30
+ }
31
+ finally {
32
+ await database.end();
33
+ }
34
+ };
35
+ }
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":";;AAmCA,gDA+BC;AAlED,qCAAqC;AACrC,yCAAiC;AAEjC,8DAA6D;AAC7D,kEAAwE;AAsBxE,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAS9C,SAAgB,kBAAkB,CAChC,UAAqC,EAAE;IAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE;QACtB,MAAM,UAAU,GAAG,IAAA,gBAAI,EAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;QACnE,IAAI,CAAC,IAAA,oBAAU,EAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,CAAC,WAAW,yBAAyB,UAAU,kEAAkE,CAC3H,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,CAAC,WAAW,6BAA6B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,mBAAmB,cAAc,CAAC,MAAM,gDAAgD,CACxK,CAAC;QACJ,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAA,gDAA2B,EAAC;YACjD,gBAAgB,EAAE,MAAM,CAAC,GAAG;SAC7B,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,KAAK,CAAC,gCAAgC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YACvE,MAAM,IAAA,qCAAkB,EAAC;gBACvB,QAAQ;gBACR,YAAY,EAAE,MAAM,CAAC,OAAO;gBAC5B,aAAa,EAAE,MAAM,CAAC,GAAG;gBACzB,KAAK,EAAE,MAAM,CAAC,WAAW;gBACzB,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,IAAI,EAAE;aACrD,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC;QACvB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/biome-database-nest",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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)",
@@ -30,7 +30,7 @@
30
30
  "@types/node": "25.2.3",
31
31
  "@types/pg": "^8.16.0",
32
32
  "@xemahq/identity-client": "^0.15.5",
33
- "@xemahq/contracts": "^0.78.0",
33
+ "@xemahq/contracts": "^0.79.0",
34
34
  "@xemahq/pg-test-harness": "^0.3.1",
35
35
  "@xemahq/platform-common": ">=0.77.0",
36
36
  "eslint": "^9.39.4",
@@ -40,8 +40,8 @@
40
40
  "ts-jest": "^29.4.9",
41
41
  "typescript": "5.9.3",
42
42
  "@xemahq/eslint-config": "0.14.0",
43
- "@xemahq/service-registry-nest": "1.0.0",
44
- "@xemahq/xema-decorators": "1.0.0"
43
+ "@xemahq/xema-decorators": "1.0.1",
44
+ "@xemahq/service-registry-nest": "1.1.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@nestjs/common": "^10 || ^11",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@xemahq/migration-runner-prisma": "^0.3.1",
58
- "@xemahq/org-erasure-nest": "^2.0.0",
58
+ "@xemahq/org-erasure-nest": "^2.0.1",
59
59
  "pg": "^8.16.3"
60
60
  },
61
61
  "exports": {
@@ -64,6 +64,11 @@
64
64
  "xema-source": "./src/index.ts",
65
65
  "default": "./dist/index.js"
66
66
  },
67
+ "./testing": {
68
+ "types": "./dist/testing/index.d.ts",
69
+ "xema-source": "./src/testing/index.ts",
70
+ "default": "./dist/testing/index.js"
71
+ },
67
72
  "./dist/lib/*": {
68
73
  "types": "./dist/lib/*.d.ts",
69
74
  "default": "./dist/lib/*.js"
@@ -78,6 +83,6 @@
78
83
  "typecheck": "tsc -p tsconfig.json --noEmit"
79
84
  },
80
85
  "xemaRelease": {
81
- "sourceDigest": "sha256:b40aff384ba4436e8abd374a3ed50e2a666892722608e2b27633890b91da374e"
86
+ "sourceDigest": "sha256:6a5dd5acb427f3601ffafd9f7f634024b4504d3968dd6508b0b4794998130f71"
82
87
  }
83
88
  }