@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.
@@ -1,174 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { IdentityBootstrapService } from '@xemahq/identity-client';
3
- import {
4
- isOpenApiExtraction,
5
- KERNEL_STATE_TOKEN,
6
- } from '@xemahq/service-registry-nest';
7
-
8
- import { bootstrapBiomeDatabases } from '../bootstrap';
9
- import { BiomeDatabaseMigrationMode } from '../migration-mode';
10
-
11
- import type {
12
- BiomeDatabaseDeclaration,
13
- } from '../bootstrap';
14
- import type { DynamicModule } from '@nestjs/common';
15
- import type { KernelState } from '@xemahq/kernel-contracts/kernel-state';
16
-
17
- /**
18
- * DI-resolved sentinel that resolves to `true` once every declared biome
19
- * database has been schema-created, migrated, and exported into the env that
20
- * `createBiomePrismaAdapterFromEnv` reads.
21
- *
22
- * A consuming service MUST list this token in the `inject:[]` of its
23
- * `PrismaService` provider so Nest constructs the database connection ONLY
24
- * after the bootstrap has completed.
25
- */
26
- export const BIOME_DATABASE_READY = Symbol.for(
27
- '@xemahq/biome-database-nest:ready',
28
- );
29
-
30
- interface BiomeDatabaseModuleCommonOptions {
31
- /** The biome whose databases are being bootstrapped. */
32
- readonly biomeId: string;
33
- }
34
-
35
- export interface ApplicationManagedBiomeDatabaseModuleOptions
36
- extends BiomeDatabaseModuleCommonOptions {
37
- readonly migrationMode: BiomeDatabaseMigrationMode.ApplicationManaged;
38
- /**
39
- * Explicit databases to bootstrap. Each declaration pins the credential
40
- * generation independently of migration ownership.
41
- */
42
- readonly databases: readonly BiomeDatabaseDeclaration[];
43
- /**
44
- * Postgres extensions this biome needs (e.g. `['vector']`), ensured in the
45
- * shared `public` schema BEFORE migrations run. Only declare extensions the
46
- * biome actually uses. See `BootstrapBiomeDatabasesOptions.requiredExtensions`.
47
- */
48
- readonly requiredExtensions?: readonly string[];
49
- }
50
-
51
-
52
- /** Options for {@link BiomeDatabaseModule.forRoot}. */
53
- export type BiomeDatabaseModuleOptions =
54
- ApplicationManagedBiomeDatabaseModuleOptions;
55
-
56
- function assertBiomeDatabaseModuleOptions(
57
- opts: unknown,
58
- ): asserts opts is BiomeDatabaseModuleOptions {
59
- if (
60
- opts === null ||
61
- typeof opts !== 'object' ||
62
- !('migrationMode' in opts) ||
63
- opts.migrationMode !== BiomeDatabaseMigrationMode.ApplicationManaged
64
- ) {
65
- throw new Error(
66
- '[biome-database] migrationMode is required and must be ApplicationManaged.',
67
- );
68
- }
69
-
70
- if (
71
- !('databases' in opts) ||
72
- !Array.isArray(opts.databases) ||
73
- opts.databases.length === 0
74
- ) {
75
- throw new Error(
76
- '[biome-database] every migration mode requires at least one explicit database declaration.',
77
- );
78
- }
79
- }
80
-
81
- /**
82
- * Global Nest dynamic module that bootstraps a biome's database using an
83
- * explicitly selected migration ownership mode.
84
- *
85
- * Application-managed mode retains env-first config resolution. Externally
86
- * managed declarations select environment or control plane explicitly, so a
87
- * stale env override can never silently replace the intended runtime
88
- * credential.
89
- *
90
- * `IdentityBootstrapService` (from `@xemahq/identity-client`) is provided
91
- * globally by the app's `XemaServiceModule.forBiome(...)` bootstrap, so it is
92
- * injectable at DI time and mints the service bearer for the control-plane
93
- * call. The control-plane ADDRESS is discovered from the Xema service registry:
94
- * this factory injects the app's `KernelState` backend via `KERNEL_STATE_TOKEN`
95
- * (exposed globally by `ServiceRegistryModule`, wired by
96
- * `XemaServiceModule.forBiome(...)`) and drives a private, short-lived
97
- * `KernelStateServiceRegistry` to `resolve('org-database-pool-api')` with
98
- * bounded retry — see `resolveControlPlaneBaseUrl` in `control-plane-discovery`.
99
- * The app-wide registry is NOT injected: it hydrates only in `onModuleInit`,
100
- * which cannot run until this instantiation-phase factory returns.
101
- *
102
- * REQUIRES the consuming app to provide `KERNEL_STATE_TOKEN` globally — every
103
- * biome service does via `XemaServiceModule.forBiome(...)` /
104
- * `ServiceRegistryModule`. No extra import in this module is needed because that
105
- * provider is global; if it is absent, Nest fails fast at DI resolution.
106
- *
107
- * Fail-fast: an absent/unknown mode or any boot error aborts Nest startup.
108
- *
109
- * @example Consumer wiring
110
- * ```ts
111
- * // app.module.ts (or prisma.module.ts):
112
- * imports: [BiomeDatabaseModule.forRoot({
113
- * biomeId: '<biome-id>',
114
- * })],
115
- *
116
- * // prisma.module.ts — the PrismaService provider waits on the boot:
117
- * {
118
- * provide: PrismaService,
119
- * useFactory: () => new PrismaService(),
120
- * inject: [BIOME_DATABASE_READY],
121
- * }
122
- * ```
123
- */
124
- @Module({})
125
- export class BiomeDatabaseModule {
126
- static forRoot(opts: BiomeDatabaseModuleOptions): DynamicModule;
127
- static forRoot(opts: unknown): DynamicModule {
128
- assertBiomeDatabaseModuleOptions(opts);
129
- const databases: readonly BiomeDatabaseDeclaration[] = opts.databases;
130
-
131
- // Build-time OpenAPI extraction compiles the DI graph for decorator
132
- // metadata only: it never calls `app.init()`, never issues a query, and
133
- // stubs `PrismaService` outright. Running the real bootstrap there would
134
- // connect to — and MIGRATE — a throwaway scratch database, which is both
135
- // pointless and (under parallel extraction) mutually destructive. Resolve
136
- // the sentinel to `true` without injecting anything.
137
- if (isOpenApiExtraction()) {
138
- return {
139
- module: BiomeDatabaseModule,
140
- global: true,
141
- providers: [{ provide: BIOME_DATABASE_READY, useValue: true }],
142
- exports: [BIOME_DATABASE_READY],
143
- };
144
- }
145
-
146
- return {
147
- module: BiomeDatabaseModule,
148
- global: true,
149
- providers: [
150
- {
151
- provide: BIOME_DATABASE_READY,
152
- useFactory: async (
153
- identityBootstrap: IdentityBootstrapService,
154
- kernelState: KernelState,
155
- ): Promise<true> => {
156
- await bootstrapBiomeDatabases({
157
- biomeId: opts.biomeId,
158
- migrationMode: opts.migrationMode,
159
- databases,
160
- ...(opts.requiredExtensions !== undefined
161
- ? { requiredExtensions: opts.requiredExtensions }
162
- : {}),
163
- serviceToken: () => identityBootstrap.getAccessToken(),
164
- kernelState,
165
- });
166
- return true;
167
- },
168
- inject: [IdentityBootstrapService, KERNEL_STATE_TOKEN],
169
- },
170
- ],
171
- exports: [BIOME_DATABASE_READY],
172
- };
173
- }
174
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * LC-1 topology-1 org-erasure ENDPOINT — the DRY adoption surface.
3
- *
4
- * A first-party biome that holds org data by-row opts in with ONE line:
5
- *
6
- * ```ts
7
- * imports: [OrgErasureModule.forRoot({ prismaServiceToken: PrismaService })]
8
- * ```
9
- *
10
- * That exposes the STANDARD internal route `POST /internal/org-erasure/:orgId`,
11
- * which erases every row belonging to the org across the service's org-scoped
12
- * models via {@link eraseOrgData} on the UNSCOPED client (`asSystem()`). The
13
- * org-erasure orchestrator saga fans out to this route on every participant.
14
- *
15
- * Service-only, and ENFORCED by `ServiceActorGuard` — not by the classification
16
- * decorator. `@XemaInternalRoute({ audience: Service })` is metadata the
17
- * fail-closed route scanner reads; it installs NO runtime guard (see
18
- * `.claude/rules/public-and-internal-route-guarding.md`). The global stack only
19
- * proves the JWT signature, and `RolesGuard` enforces nothing unless a route
20
- * declares `@RequireOrgRole`/`@RequireTokenClass`. Without the guard, ANY
21
- * authenticated user token reaching this service — trivially so for any
22
- * participant whose chart enables ingress — could irreversibly erase an
23
- * arbitrary org's data, since `:orgId` is taken verbatim from the path. This
24
- * module is adopted by 17 participants, so the guard belongs HERE, once.
25
- *
26
- * The org filter is applied inside {@link eraseOrgData}; the runner passes the
27
- * unscoped client so scoping is neither doubled nor left to ambient context.
28
- *
29
- * **The module refuses to boot a participant that can erase nothing.** Wiring
30
- * the runner asserts (via {@link assertOrgErasureMatchesModels}) that the
31
- * configured `orgField` matches at least one model of THIS service's schema. A
32
- * participant registered against a column its schema does not spell matches
33
- * zero models, deletes nothing, and returns `{totalDeleted: 0}` — which the
34
- * orchestrator records as a successful erasure. That must fail at deploy, not
35
- * silently succeed during a Data Subject Request.
36
- */
37
- import {
38
- Controller,
39
- Inject,
40
- Logger,
41
- Param,
42
- Post,
43
- UseGuards,
44
- type DynamicModule,
45
- type Type,
46
- } from '@nestjs/common';
47
- import { ApiOperation, ApiTags } from '@nestjs/swagger';
48
- import { ServiceActorGuard } from '@xemahq/platform-common';
49
- import { InternalRouteAudience, XemaInternalRoute } from '@xemahq/xema-decorators';
50
-
51
- import {
52
- assertOrgErasureMatchesModels,
53
- eraseOrgData,
54
- type EraseOrgDataOptions,
55
- type OrgErasureReport,
56
- } from '../tenant-isolation/org-erasure';
57
- import { DEFAULT_ORG_FIELD } from '../tenant-isolation/tenant-isolation.extension';
58
-
59
- import type { TenantScopedApi } from './prisma-service-factory';
60
-
61
- /** DI token for the erasure runner the controller delegates to. */
62
- export const ORG_ERASURE_RUNNER = Symbol.for(
63
- '@xemahq/biome-database-nest/org-erasure-runner',
64
- );
65
-
66
- /**
67
- * The capability a topology-1 participant declares in its service descriptor's
68
- * `exposesCapabilities` so the org-erasure orchestrator can DISCOVER it via the
69
- * service registry (`registry.list()` filtered on this string) and fan out to
70
- * its `POST /internal/org-erasure/:orgId`. Single source of truth shared by the
71
- * declaring service and the orchestrator — discovery is explicit-by-declaration,
72
- * never a probe-and-guess heuristic.
73
- */
74
- export const ORG_DATA_ERASURE_CAPABILITY = 'xema.org-data-erasure';
75
-
76
- /** The internal route path this module exposes (orchestrator fans out to it). */
77
- export const ORG_ERASURE_ROUTE_PATH = 'internal/org-erasure';
78
-
79
- export interface OrgErasureRunner {
80
- erase(orgId: string): Promise<OrgErasureReport>;
81
- }
82
-
83
- @ApiTags('org-erasure')
84
- @UseGuards(ServiceActorGuard)
85
- @Controller('internal/org-erasure')
86
- export class OrgErasureController {
87
- constructor(
88
- @Inject(ORG_ERASURE_RUNNER) private readonly runner: OrgErasureRunner,
89
- ) {}
90
-
91
- @XemaInternalRoute({ audience: InternalRouteAudience.Service })
92
- @Post(':orgId')
93
- @ApiOperation({
94
- summary:
95
- "Erase every row belonging to an org across this service's org-scoped models",
96
- })
97
- erase(@Param('orgId') orgId: string): Promise<OrgErasureReport> {
98
- return this.runner.erase(orgId);
99
- }
100
- }
101
-
102
- export interface OrgErasureModuleOptions extends EraseOrgDataOptions {
103
- /**
104
- * The service's PrismaService DI token — the class it provides as its Prisma
105
- * service (a {@link BiomePrismaServiceInstance}). The runner resolves it and
106
- * calls `.asSystem()` to get the unscoped client.
107
- */
108
- readonly prismaServiceToken: Type<unknown> | string | symbol;
109
- }
110
-
111
- /**
112
- * The subset of the biome Prisma service the runner needs: the unscoped
113
- * `asSystem()` client accessor.
114
- */
115
- type AsSystemProvider = TenantScopedApi<unknown>;
116
-
117
- export class OrgErasureModule {
118
- static forRoot(options: OrgErasureModuleOptions): DynamicModule {
119
- const { prismaServiceToken, ...eraseOptions } = options;
120
- return {
121
- module: OrgErasureModule,
122
- controllers: [OrgErasureController],
123
- providers: [
124
- {
125
- provide: ORG_ERASURE_RUNNER,
126
- useFactory: (prisma: AsSystemProvider): OrgErasureRunner => {
127
- const client = prisma.asSystem();
128
- // BOOT GATE. A participant whose orgField matches no model can
129
- // never delete a row, and `eraseOrgData` would have answered every
130
- // erasure `{totalDeleted: 0}` — which the orchestrator saga records
131
- // as a successful erasure. Discovering that during a live Data
132
- // Subject Request is discovering it too late, so the service
133
- // refuses to START rather than advertise `xema.org-data-erasure`
134
- // it cannot honour. Throwing here aborts Nest's bootstrap.
135
- //
136
- // This is the CONFIGURATION proof; `eraseOrgData` re-asserts per
137
- // request, which is the proof of the RUN being recorded as
138
- // evidence. The assertion is on MODELS matched — an org that
139
- // genuinely holds no rows here still erases successfully.
140
- const models = [
141
- ...assertOrgErasureMatchesModels(client, eraseOptions),
142
- ].sort();
143
- new Logger(OrgErasureModule.name).log(
144
- `org-erasure participant armed on "${
145
- eraseOptions.orgField ?? DEFAULT_ORG_FIELD
146
- }" over ${models.length} model(s): ${models.join(', ')}`,
147
- );
148
- return {
149
- erase: (orgId: string): Promise<OrgErasureReport> =>
150
- eraseOrgData(prisma.asSystem(), orgId, eraseOptions),
151
- };
152
- },
153
- inject: [prismaServiceToken],
154
- },
155
- ],
156
- };
157
- }
158
- }