@xemahq/biome-database-nest 0.11.0 → 0.12.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
@@ -28,19 +28,33 @@
28
28
 
29
29
  ## Overview
30
30
 
31
- This package centralizes schema-qualified Prisma construction and database
32
- bootstrap for Xema biome services. Every consumer explicitly selects whether
33
- the application or an external process owns migrations, and every database
34
- declaration pins an exact credential generation. Externally managed services
35
- must prove connection identity, zero role membership and object ownership,
36
- exact relation/type/function privileges, a read-only migration ledger, and
37
- migration compatibility before the application may start. Cross-schema access
38
- is denied except for exact, versioned, read-only PostgreSQL extension objects.
31
+ Gives a Xema biome service its database without the service knowing anything
32
+ about databases. It resolves the connection from `org-database-pool-api` at
33
+ boot, builds a schema-qualified Prisma client for it, and applies the service's
34
+ own Prisma migrations before the app serves traffic.
35
+
36
+ A biome carries **no** database configuration: no host, no credential, no URL,
37
+ no schema name, nothing to put in a Helm chart or a secret. Those are facts the
38
+ platform owns, and a biome that had to know them would be a biome that breaks
39
+ when they change.
40
+
41
+ ## What a biome gets
42
+
43
+ - **A database of its own, on first boot.** The pool binds the service to it and
44
+ holds it there, so the name can never silently move to an empty schema.
45
+ - **A schema-qualified Prisma client**, already pointed at that database.
46
+ - **Migrations applied at boot** — `prisma migrate deploy`, which THROWS on
47
+ failure, so a bad migration stops the pod becoming ready instead of half-
48
+ applying under live traffic.
49
+ - **More than one database when it needs one.** A biome that ships several API
50
+ services gives each its own; a single service that needs a second declares a
51
+ `dbKey`. Physically these are schemas in the shared `xema_biomes` database —
52
+ `biome_<biomeId>` for the primary, `biome_<biomeId>__<dbKey>` for the rest.
39
53
 
40
54
  ## When to use it
41
55
 
42
- - Use it when a NestJS biome service needs a schema-qualified Prisma connection.
43
- - Select externally managed migrations when deployment operations own all DDL.
56
+ Use it in any NestJS biome service that stores data. There is no second way to
57
+ reach a biome database, and nothing for the service to configure.
44
58
 
45
59
  ## Installation
46
60
 
@@ -50,6 +64,8 @@ pnpm add @xemahq/biome-database-nest
50
64
 
51
65
  ## Usage
52
66
 
67
+ The whole integration:
68
+
53
69
  ```ts
54
70
  import {
55
71
  BiomeDatabaseMigrationMode,
@@ -58,26 +74,25 @@ import {
58
74
 
59
75
  BiomeDatabaseModule.forRoot({
60
76
  biomeId: 'example',
61
- migrationMode: BiomeDatabaseMigrationMode.ExternallyManaged,
62
- databases: [externallyManagedDatabase],
77
+ migrationMode: BiomeDatabaseMigrationMode.ApplicationManaged,
78
+ databases: [{ workspaceDir: process.cwd() }],
63
79
  });
64
80
  ```
65
81
 
66
- Application-managed declarations also require a credential generation:
82
+ `biomeId` names the database this service owns; it is pinned by the pool on
83
+ first resolve, so changing it later is refused rather than silently pointing the
84
+ service at a fresh empty schema. `workspaceDir` is where the Prisma schema and
85
+ `migrations/` live.
86
+
87
+ A second database for the same service:
67
88
 
68
89
  ```ts
69
- {
70
- key: 'primary',
71
- workspaceDir: process.cwd(),
72
- credentialGeneration: 2,
73
- }
90
+ databases: [
91
+ { workspaceDir: process.cwd() },
92
+ { key: 'analytics', workspaceDir: `${process.cwd()}/analytics` },
93
+ ],
74
94
  ```
75
95
 
76
- Runtime declarations use closed privilege enums. Views, materialized views,
77
- and foreign tables are SELECT-only unless a writable view or foreign table is
78
- explicitly named. Runtime-callable functions are exact-overload allowlists;
79
- `SECURITY DEFINER` functions are never accepted.
80
-
81
96
  ## Peer requirements
82
97
 
83
98
  - NestJS 10 or 11 and Prisma 7.
package/dist/index.d.ts CHANGED
@@ -14,6 +14,6 @@ export { TenantIsolationError, TenantIsolationViolationKind, formatTenantIsolati
14
14
  export { PrismaModelOperation, scopeArgsToOrg, type OrgScopeOutcome, type OrgScopeRequest, } from './lib/tenant-isolation/org-scope';
15
15
  export { DEFAULT_ORG_FIELD, collectOrgScopedModels, createTenantIsolationExtension, readRuntimeModels, type CreateTenantIsolationExtensionOptions, type RuntimeDataModelField, type RuntimeDataModelModel, type TenantIsolationExtensionDefinition, type TenantIsolationLogger, type TenantIsolationQueryContext, } from './lib/tenant-isolation/tenant-isolation.extension';
16
16
  export { assertUnscopedModelsDeclared, collectUnscopableModels, formatUnscopedModelReport, normalizeUnscopedModelDeclarations, UndeclaredUnscopedModelsError, type AssertUnscopedModelsOptions, type NormalizedUnscopedModelDeclaration, type UnscopedModelDeclaration, type UnscopedModelDeclarations, } from './lib/tenant-isolation/unscoped-models';
17
- export { eraseOrgData, OrgErasureBlockedError, type EraseOrgDataOptions, type OrgErasureReport, } from './lib/tenant-isolation/org-erasure';
17
+ export { assertOrgErasureMatchesModels, eraseOrgData, OrgErasureBlockedError, OrgErasureMatchesNothingError, type EraseOrgDataOptions, type OrgErasureReport, } from './lib/tenant-isolation/org-erasure';
18
18
  export { OrgErasureModule, OrgErasureController, ORG_ERASURE_RUNNER, ORG_DATA_ERASURE_CAPABILITY, ORG_ERASURE_ROUTE_PATH, type OrgErasureModuleOptions, type OrgErasureRunner, } from './lib/nest/org-erasure.module';
19
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,8BAA8B,EAC9B,uBAAuB,EACvB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,yBAAyB,EACzB,KAAK,eAAe,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,wBAAwB,EACxB,+BAA+B,GAChC,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,yBAAyB,EACzB,KAAK,2BAA2B,GACjC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,0BAA0B,EAC1B,8BAA8B,EAC9B,KAAK,4BAA4B,GAClC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,GACpC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,4CAA4C,EACjD,KAAK,0BAA0B,GAChC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,wBAAwB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,+BAA+B,EACpC,KAAK,6BAA6B,EAClC,KAAK,eAAe,GACrB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,8BAA8B,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,+CAA+C,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,8BAA8B,EAC9B,iBAAiB,EACjB,KAAK,qCAAqC,EAC1C,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,kCAAkC,EACvC,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,GACjC,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,yBAAyB,EACzB,kCAAkC,EAClC,6BAA6B,EAC7B,KAAK,2BAA2B,EAChC,KAAK,kCAAkC,EACvC,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,GACtB,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,8BAA8B,EAC9B,uBAAuB,EACvB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,cAAc,EACd,2BAA2B,EAC3B,yBAAyB,EACzB,KAAK,eAAe,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,wBAAwB,EACxB,+BAA+B,GAChC,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,yBAAyB,EACzB,KAAK,2BAA2B,GACjC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,0BAA0B,EAC1B,8BAA8B,EAC9B,KAAK,4BAA4B,GAClC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,GACpC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,4CAA4C,EACjD,KAAK,0BAA0B,GAChC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,wBAAwB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,+BAA+B,EACpC,KAAK,6BAA6B,EAClC,KAAK,eAAe,GACrB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,8BAA8B,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,+CAA+C,CAAC;AACvD,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,8BAA8B,EAC9B,iBAAiB,EACjB,KAAK,qCAAqC,EAC1C,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,kCAAkC,EACvC,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,GACjC,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,yBAAyB,EACzB,kCAAkC,EAClC,6BAA6B,EAC7B,KAAK,2BAA2B,EAChC,KAAK,kCAAkC,EACvC,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,6BAA6B,EAC7B,YAAY,EACZ,sBAAsB,EACtB,6BAA6B,EAC7B,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,GACtB,MAAM,+BAA+B,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ORG_ERASURE_ROUTE_PATH = exports.ORG_DATA_ERASURE_CAPABILITY = exports.ORG_ERASURE_RUNNER = exports.OrgErasureController = exports.OrgErasureModule = exports.OrgErasureBlockedError = exports.eraseOrgData = exports.UndeclaredUnscopedModelsError = exports.normalizeUnscopedModelDeclarations = exports.formatUnscopedModelReport = exports.collectUnscopableModels = exports.assertUnscopedModelsDeclared = exports.readRuntimeModels = exports.createTenantIsolationExtension = exports.collectOrgScopedModels = exports.DEFAULT_ORG_FIELD = exports.scopeArgsToOrg = exports.PrismaModelOperation = exports.formatTenantIsolationViolation = exports.TenantIsolationViolationKind = exports.TenantIsolationError = exports.resolveTenantIsolationMode = exports.TENANT_ISOLATION_MODE_ENV_VAR = exports.TenantIsolationMode = exports.createBiomePrismaService = exports.BIOME_DATABASE_READY = exports.BiomeDatabaseModule = exports.BiomeDatabaseMigrationMode = exports.bootstrapBiomeDatabases = exports.ORG_DATABASE_POOL_SERVICE_NAME = exports.resolveControlPlaneBaseUrl = exports.fetchSystemDatabaseConfig = exports.createBiomePrismaAdapterFromEnv = exports.createBiomePrismaAdapter = exports.resolveBiomeDatabaseConfigFromEnv = exports.SCHEMA_ROLE_SUFFIX_LENGTH = exports.SCHEMA_ROLE_BASE_MAX_LENGTH = exports.SchemaRoleKind = exports.schemaRoleName = exports.schemaRoleBase = exports.rolesForSchema = exports.sanitizeIdentifierSegment = exports.sanitizeIdentifierChars = exports.POSTGRES_MAX_IDENTIFIER_LENGTH = exports.IDENTIFIER_HASH_LENGTH = exports.fitIdentifier = exports.composeIdentifier = exports.deriveBiomeSchemaName = exports.DEFAULT_BIOME_DB_KEY = void 0;
3
+ exports.ORG_DATA_ERASURE_CAPABILITY = exports.ORG_ERASURE_RUNNER = exports.OrgErasureController = exports.OrgErasureModule = exports.OrgErasureMatchesNothingError = exports.OrgErasureBlockedError = exports.eraseOrgData = exports.assertOrgErasureMatchesModels = exports.UndeclaredUnscopedModelsError = exports.normalizeUnscopedModelDeclarations = exports.formatUnscopedModelReport = exports.collectUnscopableModels = exports.assertUnscopedModelsDeclared = exports.readRuntimeModels = exports.createTenantIsolationExtension = exports.collectOrgScopedModels = exports.DEFAULT_ORG_FIELD = exports.scopeArgsToOrg = exports.PrismaModelOperation = exports.formatTenantIsolationViolation = exports.TenantIsolationViolationKind = exports.TenantIsolationError = exports.resolveTenantIsolationMode = exports.TENANT_ISOLATION_MODE_ENV_VAR = exports.TenantIsolationMode = exports.createBiomePrismaService = exports.BIOME_DATABASE_READY = exports.BiomeDatabaseModule = exports.BiomeDatabaseMigrationMode = exports.bootstrapBiomeDatabases = exports.ORG_DATABASE_POOL_SERVICE_NAME = exports.resolveControlPlaneBaseUrl = exports.fetchSystemDatabaseConfig = exports.createBiomePrismaAdapterFromEnv = exports.createBiomePrismaAdapter = exports.resolveBiomeDatabaseConfigFromEnv = exports.SCHEMA_ROLE_SUFFIX_LENGTH = exports.SCHEMA_ROLE_BASE_MAX_LENGTH = exports.SchemaRoleKind = exports.schemaRoleName = exports.schemaRoleBase = exports.rolesForSchema = exports.sanitizeIdentifierSegment = exports.sanitizeIdentifierChars = exports.POSTGRES_MAX_IDENTIFIER_LENGTH = exports.IDENTIFIER_HASH_LENGTH = exports.fitIdentifier = exports.composeIdentifier = exports.deriveBiomeSchemaName = exports.DEFAULT_BIOME_DB_KEY = void 0;
4
+ exports.ORG_ERASURE_ROUTE_PATH = void 0;
4
5
  var schema_name_1 = require("./lib/schema-name");
5
6
  Object.defineProperty(exports, "DEFAULT_BIOME_DB_KEY", { enumerable: true, get: function () { return schema_name_1.DEFAULT_BIOME_DB_KEY; } });
6
7
  Object.defineProperty(exports, "deriveBiomeSchemaName", { enumerable: true, get: function () { return schema_name_1.deriveBiomeSchemaName; } });
@@ -60,8 +61,10 @@ Object.defineProperty(exports, "formatUnscopedModelReport", { enumerable: true,
60
61
  Object.defineProperty(exports, "normalizeUnscopedModelDeclarations", { enumerable: true, get: function () { return unscoped_models_1.normalizeUnscopedModelDeclarations; } });
61
62
  Object.defineProperty(exports, "UndeclaredUnscopedModelsError", { enumerable: true, get: function () { return unscoped_models_1.UndeclaredUnscopedModelsError; } });
62
63
  var org_erasure_1 = require("./lib/tenant-isolation/org-erasure");
64
+ Object.defineProperty(exports, "assertOrgErasureMatchesModels", { enumerable: true, get: function () { return org_erasure_1.assertOrgErasureMatchesModels; } });
63
65
  Object.defineProperty(exports, "eraseOrgData", { enumerable: true, get: function () { return org_erasure_1.eraseOrgData; } });
64
66
  Object.defineProperty(exports, "OrgErasureBlockedError", { enumerable: true, get: function () { return org_erasure_1.OrgErasureBlockedError; } });
67
+ Object.defineProperty(exports, "OrgErasureMatchesNothingError", { enumerable: true, get: function () { return org_erasure_1.OrgErasureMatchesNothingError; } });
65
68
  var org_erasure_module_1 = require("./lib/nest/org-erasure.module");
66
69
  Object.defineProperty(exports, "OrgErasureModule", { enumerable: true, get: function () { return org_erasure_module_1.OrgErasureModule; } });
67
70
  Object.defineProperty(exports, "OrgErasureController", { enumerable: true, get: function () { return org_erasure_module_1.OrgErasureController; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAAgF;AAAvE,mHAAA,oBAAoB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACpD,+CAS0B;AARxB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,oHAAA,sBAAsB,OAAA;AACtB,4HAAA,8BAA8B,OAAA;AAC9B,qHAAA,uBAAuB,OAAA;AACvB,uHAAA,yBAAyB,OAAA;AAI3B,+CAQ0B;AAPxB,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,yHAAA,2BAA2B,OAAA;AAC3B,uHAAA,yBAAyB,OAAA;AAG3B,uCAGsB;AAFpB,2HAAA,iCAAiC,OAAA;AAGnC,yCAGuB;AAFrB,mHAAA,wBAAwB,OAAA;AACxB,0HAAA,+BAA+B,OAAA;AAEjC,mEAGoC;AAFlC,iIAAA,yBAAyB,OAAA;AAG3B,yEAIuC;AAHrC,qIAAA,0BAA0B,OAAA;AAC1B,yIAAA,8BAA8B,OAAA;AAGhC,6CAIyB;AAHvB,oHAAA,uBAAuB,OAAA;AAIzB,uDAAkE;AAAzD,4HAAA,0BAA0B,OAAA;AACnC,0EAK0C;AAJxC,4HAAA,mBAAmB,OAAA;AACnB,6HAAA,oBAAoB,OAAA;AAItB,4EAQ2C;AAPzC,kIAAA,wBAAwB,OAAA;AAQ1B,sFAIsD;AAHpD,4HAAA,mBAAmB,OAAA;AACnB,sIAAA,6BAA6B,OAAA;AAC7B,mIAAA,0BAA0B,OAAA;AAE5B,wFAKuD;AAJrD,8HAAA,oBAAoB,OAAA;AACpB,sIAAA,4BAA4B,OAAA;AAC5B,wIAAA,8BAA8B,OAAA;AAGhC,8DAK0C;AAJxC,iHAAA,oBAAoB,OAAA;AACpB,2GAAA,cAAc,OAAA;AAIhB,gGAW2D;AAVzD,+HAAA,iBAAiB,OAAA;AACjB,oIAAA,sBAAsB,OAAA;AACtB,4IAAA,8BAA8B,OAAA;AAC9B,+HAAA,iBAAiB,OAAA;AAQnB,0EAUgD;AAT9C,+HAAA,4BAA4B,OAAA;AAC5B,0HAAA,uBAAuB,OAAA;AACvB,4HAAA,yBAAyB,OAAA;AACzB,qIAAA,kCAAkC,OAAA;AAClC,gIAAA,6BAA6B,OAAA;AAM/B,kEAK4C;AAJ1C,2GAAA,YAAY,OAAA;AACZ,qHAAA,sBAAsB,OAAA;AAIxB,oEAQuC;AAPrC,sHAAA,gBAAgB,OAAA;AAChB,0HAAA,oBAAoB,OAAA;AACpB,wHAAA,kBAAkB,OAAA;AAClB,iIAAA,2BAA2B,OAAA;AAC3B,4HAAA,sBAAsB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;AAAA,iDAAgF;AAAvE,mHAAA,oBAAoB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACpD,+CAS0B;AARxB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,oHAAA,sBAAsB,OAAA;AACtB,4HAAA,8BAA8B,OAAA;AAC9B,qHAAA,uBAAuB,OAAA;AACvB,uHAAA,yBAAyB,OAAA;AAI3B,+CAQ0B;AAPxB,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,yHAAA,2BAA2B,OAAA;AAC3B,uHAAA,yBAAyB,OAAA;AAG3B,uCAGsB;AAFpB,2HAAA,iCAAiC,OAAA;AAGnC,yCAGuB;AAFrB,mHAAA,wBAAwB,OAAA;AACxB,0HAAA,+BAA+B,OAAA;AAEjC,mEAGoC;AAFlC,iIAAA,yBAAyB,OAAA;AAG3B,yEAIuC;AAHrC,qIAAA,0BAA0B,OAAA;AAC1B,yIAAA,8BAA8B,OAAA;AAGhC,6CAIyB;AAHvB,oHAAA,uBAAuB,OAAA;AAIzB,uDAAkE;AAAzD,4HAAA,0BAA0B,OAAA;AACnC,0EAK0C;AAJxC,4HAAA,mBAAmB,OAAA;AACnB,6HAAA,oBAAoB,OAAA;AAItB,4EAQ2C;AAPzC,kIAAA,wBAAwB,OAAA;AAQ1B,sFAIsD;AAHpD,4HAAA,mBAAmB,OAAA;AACnB,sIAAA,6BAA6B,OAAA;AAC7B,mIAAA,0BAA0B,OAAA;AAE5B,wFAKuD;AAJrD,8HAAA,oBAAoB,OAAA;AACpB,sIAAA,4BAA4B,OAAA;AAC5B,wIAAA,8BAA8B,OAAA;AAGhC,8DAK0C;AAJxC,iHAAA,oBAAoB,OAAA;AACpB,2GAAA,cAAc,OAAA;AAIhB,gGAW2D;AAVzD,+HAAA,iBAAiB,OAAA;AACjB,oIAAA,sBAAsB,OAAA;AACtB,4IAAA,8BAA8B,OAAA;AAC9B,+HAAA,iBAAiB,OAAA;AAQnB,0EAUgD;AAT9C,+HAAA,4BAA4B,OAAA;AAC5B,0HAAA,uBAAuB,OAAA;AACvB,4HAAA,yBAAyB,OAAA;AACzB,qIAAA,kCAAkC,OAAA;AAClC,gIAAA,6BAA6B,OAAA;AAM/B,kEAO4C;AAN1C,4HAAA,6BAA6B,OAAA;AAC7B,2GAAA,YAAY,OAAA;AACZ,qHAAA,sBAAsB,OAAA;AACtB,4HAAA,6BAA6B,OAAA;AAI/B,oEAQuC;AAPrC,sHAAA,gBAAgB,OAAA;AAChB,0HAAA,oBAAoB,OAAA;AACpB,wHAAA,kBAAkB,OAAA;AAClB,iIAAA,2BAA2B,OAAA;AAC3B,4HAAA,sBAAsB,OAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"org-erasure.module.d.ts","sourceRoot":"","sources":["../../../src/lib/nest/org-erasure.module.ts"],"names":[],"mappings":"AA4BA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACtB,MAAM,iCAAiC,CAAC;AAKzC,eAAO,MAAM,kBAAkB,eAE9B,CAAC;AAUF,eAAO,MAAM,2BAA2B,0BAA0B,CAAC;AAGnE,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACjD;AAED,qBAGa,oBAAoB;IAED,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IASvE,KAAK,CAAiB,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAGhE;AAED,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAMlE,QAAQ,CAAC,kBAAkB,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;CAC9D;AAQD,qBAAa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,aAAa;CAiBhE"}
1
+ {"version":3,"file":"org-erasure.module.d.ts","sourceRoot":"","sources":["../../../src/lib/nest/org-erasure.module.ts"],"names":[],"mappings":"AAoCA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACtB,MAAM,iCAAiC,CAAC;AAMzC,eAAO,MAAM,kBAAkB,eAE9B,CAAC;AAUF,eAAO,MAAM,2BAA2B,0BAA0B,CAAC;AAGnE,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACjD;AAED,qBAGa,oBAAoB;IAED,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IASvE,KAAK,CAAiB,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAGhE;AAED,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAMlE,QAAQ,CAAC,kBAAkB,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;CAC9D;AAQD,qBAAa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,aAAa;CAwChE"}
@@ -18,6 +18,7 @@ const swagger_1 = require("@nestjs/swagger");
18
18
  const platform_common_1 = require("@xemahq/platform-common");
19
19
  const xema_decorators_1 = require("@xemahq/xema-decorators");
20
20
  const org_erasure_1 = require("../tenant-isolation/org-erasure");
21
+ const tenant_isolation_extension_1 = require("../tenant-isolation/tenant-isolation.extension");
21
22
  exports.ORG_ERASURE_RUNNER = Symbol.for('@xemahq/biome-database-nest/org-erasure-runner');
22
23
  exports.ORG_DATA_ERASURE_CAPABILITY = 'xema.org-data-erasure';
23
24
  exports.ORG_ERASURE_ROUTE_PATH = 'internal/org-erasure';
@@ -58,9 +59,16 @@ class OrgErasureModule {
58
59
  providers: [
59
60
  {
60
61
  provide: exports.ORG_ERASURE_RUNNER,
61
- useFactory: (prisma) => ({
62
- erase: (orgId) => (0, org_erasure_1.eraseOrgData)(prisma.asSystem(), orgId, eraseOptions),
63
- }),
62
+ useFactory: (prisma) => {
63
+ const client = prisma.asSystem();
64
+ const models = [
65
+ ...(0, org_erasure_1.assertOrgErasureMatchesModels)(client, eraseOptions),
66
+ ].sort();
67
+ new common_1.Logger(OrgErasureModule.name).log(`org-erasure participant armed on "${eraseOptions.orgField ?? tenant_isolation_extension_1.DEFAULT_ORG_FIELD}" over ${models.length} model(s): ${models.join(', ')}`);
68
+ return {
69
+ erase: (orgId) => (0, org_erasure_1.eraseOrgData)(prisma.asSystem(), orgId, eraseOptions),
70
+ };
71
+ },
64
72
  inject: [prismaServiceToken],
65
73
  },
66
74
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"org-erasure.module.js","sourceRoot":"","sources":["../../../src/lib/nest/org-erasure.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AA4BA,2CAQwB;AACxB,6CAAwD;AACxD,6DAA4D;AAC5D,6DAAmF;AAEnF,iEAIyC;AAK5B,QAAA,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAC1C,gDAAgD,CACjD,CAAC;AAUW,QAAA,2BAA2B,GAAG,uBAAuB,CAAC;AAGtD,QAAA,sBAAsB,GAAG,sBAAsB,CAAC;AAStD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAEgB;IAD/C,YAC+C,MAAwB;QAAxB,WAAM,GAAN,MAAM,CAAkB;IACpE,CAAC;IAQJ,KAAK,CAAiB,KAAa;QACjC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;CACF,CAAA;AAdY,oDAAoB;AAW/B;IANC,IAAA,mCAAiB,EAAC,EAAE,QAAQ,EAAE,uCAAqB,CAAC,OAAO,EAAE,CAAC;IAC9D,IAAA,aAAI,EAAC,QAAQ,CAAC;IACd,IAAA,sBAAY,EAAC;QACZ,OAAO,EACL,6EAA6E;KAChF,CAAC;IACK,WAAA,IAAA,cAAK,EAAC,OAAO,CAAC,CAAA;;;;iDAEpB;+BAbU,oBAAoB;IAHhC,IAAA,iBAAO,EAAC,aAAa,CAAC;IACtB,IAAA,kBAAS,EAAC,mCAAiB,CAAC;IAC5B,IAAA,mBAAU,EAAC,sBAAsB,CAAC;IAG9B,WAAA,IAAA,eAAM,EAAC,0BAAkB,CAAC,CAAA;;GAFlB,oBAAoB,CAchC;AAiBD,MAAa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAgC;QAC7C,MAAM,EAAE,kBAAkB,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC;QACxD,OAAO;YACL,MAAM,EAAE,gBAAgB;YACxB,WAAW,EAAE,CAAC,oBAAoB,CAAC;YACnC,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,0BAAkB;oBAC3B,UAAU,EAAE,CAAC,MAAwB,EAAoB,EAAE,CAAC,CAAC;wBAC3D,KAAK,EAAE,CAAC,KAAa,EAA6B,EAAE,CAClD,IAAA,0BAAY,EAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,YAAY,CAAC;qBACvD,CAAC;oBACF,MAAM,EAAE,CAAC,kBAAkB,CAAC;iBAC7B;aACF;SACF,CAAC;IACJ,CAAC;CACF;AAlBD,4CAkBC"}
1
+ {"version":3,"file":"org-erasure.module.js","sourceRoot":"","sources":["../../../src/lib/nest/org-erasure.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAoCA,2CASwB;AACxB,6CAAwD;AACxD,6DAA4D;AAC5D,6DAAmF;AAEnF,iEAKyC;AACzC,+FAAmF;AAKtE,QAAA,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAC1C,gDAAgD,CACjD,CAAC;AAUW,QAAA,2BAA2B,GAAG,uBAAuB,CAAC;AAGtD,QAAA,sBAAsB,GAAG,sBAAsB,CAAC;AAStD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAEgB;IAD/C,YAC+C,MAAwB;QAAxB,WAAM,GAAN,MAAM,CAAkB;IACpE,CAAC;IAQJ,KAAK,CAAiB,KAAa;QACjC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;CACF,CAAA;AAdY,oDAAoB;AAW/B;IANC,IAAA,mCAAiB,EAAC,EAAE,QAAQ,EAAE,uCAAqB,CAAC,OAAO,EAAE,CAAC;IAC9D,IAAA,aAAI,EAAC,QAAQ,CAAC;IACd,IAAA,sBAAY,EAAC;QACZ,OAAO,EACL,6EAA6E;KAChF,CAAC;IACK,WAAA,IAAA,cAAK,EAAC,OAAO,CAAC,CAAA;;;;iDAEpB;+BAbU,oBAAoB;IAHhC,IAAA,iBAAO,EAAC,aAAa,CAAC;IACtB,IAAA,kBAAS,EAAC,mCAAiB,CAAC;IAC5B,IAAA,mBAAU,EAAC,sBAAsB,CAAC;IAG9B,WAAA,IAAA,eAAM,EAAC,0BAAkB,CAAC,CAAA;;GAFlB,oBAAoB,CAchC;AAiBD,MAAa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAgC;QAC7C,MAAM,EAAE,kBAAkB,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC;QACxD,OAAO;YACL,MAAM,EAAE,gBAAgB;YACxB,WAAW,EAAE,CAAC,oBAAoB,CAAC;YACnC,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,0BAAkB;oBAC3B,UAAU,EAAE,CAAC,MAAwB,EAAoB,EAAE;wBACzD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;wBAajC,MAAM,MAAM,GAAG;4BACb,GAAG,IAAA,2CAA6B,EAAC,MAAM,EAAE,YAAY,CAAC;yBACvD,CAAC,IAAI,EAAE,CAAC;wBACT,IAAI,eAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,GAAG,CACnC,qCACE,YAAY,CAAC,QAAQ,IAAI,8CAC3B,UAAU,MAAM,CAAC,MAAM,cAAc,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACzD,CAAC;wBACF,OAAO;4BACL,KAAK,EAAE,CAAC,KAAa,EAA6B,EAAE,CAClD,IAAA,0BAAY,EAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,YAAY,CAAC;yBACvD,CAAC;oBACJ,CAAC;oBACD,MAAM,EAAE,CAAC,kBAAkB,CAAC;iBAC7B;aACF;SACF,CAAC;IACJ,CAAC;CACF;AAzCD,4CAyCC"}
@@ -13,5 +13,12 @@ export declare class OrgErasureBlockedError extends Error {
13
13
  readonly blockedModels: readonly string[];
14
14
  constructor(orgId: string, blockedModels: readonly string[]);
15
15
  }
16
+ export declare class OrgErasureMatchesNothingError extends Error {
17
+ readonly orgField: string;
18
+ readonly knownModels: readonly string[];
19
+ readonly exemptModels: readonly string[];
20
+ constructor(orgField: string, knownModels: readonly string[], exemptModels: readonly string[]);
21
+ }
22
+ export declare function assertOrgErasureMatchesModels(client: unknown, options?: EraseOrgDataOptions): ReadonlySet<string>;
16
23
  export declare function eraseOrgData(client: unknown, orgId: string, options?: EraseOrgDataOptions): Promise<OrgErasureReport>;
17
24
  //# sourceMappingURL=org-erasure.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"org-erasure.d.ts","sourceRoot":"","sources":["../../../src/lib/tenant-isolation/org-erasure.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,mBAAmB;IAElC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAQ3B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAGD,qBAAa,sBAAuB,SAAQ,KAAK;IAE7C,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE;gBADhC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,SAAS,MAAM,EAAE;CAY5C;AA2BD,wBAAsB,YAAY,CAChC,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,gBAAgB,CAAC,CA0D3B"}
1
+ {"version":3,"file":"org-erasure.d.ts","sourceRoot":"","sources":["../../../src/lib/tenant-isolation/org-erasure.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,mBAAmB;IAElC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAQ3B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAGD,qBAAa,sBAAuB,SAAQ,KAAK;IAE7C,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE;gBADhC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,SAAS,MAAM,EAAE;CAY5C;AAiBD,qBAAa,6BAA8B,SAAQ,KAAK;IAEpD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE;IACvC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE;gBAF/B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,YAAY,EAAE,SAAS,MAAM,EAAE;CAkB3C;AAoBD,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,mBAAwB,GAChC,WAAW,CAAC,MAAM,CAAC,CAYrB;AA2BD,wBAAsB,YAAY,CAChC,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAwD3B"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OrgErasureBlockedError = void 0;
3
+ exports.OrgErasureMatchesNothingError = exports.OrgErasureBlockedError = void 0;
4
+ exports.assertOrgErasureMatchesModels = assertOrgErasureMatchesModels;
4
5
  exports.eraseOrgData = eraseOrgData;
5
6
  const tenant_isolation_extension_1 = require("./tenant-isolation.extension");
6
7
  const PRISMA_FK_VIOLATION_CODE = 'P2003';
@@ -18,6 +19,37 @@ class OrgErasureBlockedError extends Error {
18
19
  }
19
20
  }
20
21
  exports.OrgErasureBlockedError = OrgErasureBlockedError;
22
+ class OrgErasureMatchesNothingError extends Error {
23
+ orgField;
24
+ knownModels;
25
+ exemptModels;
26
+ constructor(orgField, knownModels, exemptModels) {
27
+ super(`[org-erasure] this erasure participant matched ZERO models on org ` +
28
+ `field "${orgField}" — it cannot delete anything, so it MUST NOT ` +
29
+ `report a successful erasure (the orchestrator would record proof of ` +
30
+ `an erasure that never happened). Known models: [${knownModels.join(', ')}]` +
31
+ (exemptModels.length > 0
32
+ ? `; exemptModels: [${exemptModels.join(', ')}]` +
33
+ ` — exempting every org-scoped model leaves nothing to erase`
34
+ : '') +
35
+ `. Fix the participant's orgField (or stop registering the service as ` +
36
+ `an org-erasure participant) rather than relaxing this check.`);
37
+ this.orgField = orgField;
38
+ this.knownModels = knownModels;
39
+ this.exemptModels = exemptModels;
40
+ this.name = 'OrgErasureMatchesNothingError';
41
+ }
42
+ }
43
+ exports.OrgErasureMatchesNothingError = OrgErasureMatchesNothingError;
44
+ function assertOrgErasureMatchesModels(client, options = {}) {
45
+ const orgField = options.orgField ?? tenant_isolation_extension_1.DEFAULT_ORG_FIELD;
46
+ const exemptModels = options.exemptModels ?? [];
47
+ const scoped = (0, tenant_isolation_extension_1.collectOrgScopedModels)(client, orgField, exemptModels);
48
+ if (scoped.size === 0) {
49
+ throw new OrgErasureMatchesNothingError(orgField, Object.keys((0, tenant_isolation_extension_1.readRuntimeModels)(client)), exemptModels);
50
+ }
51
+ return scoped;
52
+ }
21
53
  function delegateKeyForModel(modelName) {
22
54
  return modelName.charAt(0).toLowerCase() + modelName.slice(1);
23
55
  }
@@ -31,7 +63,7 @@ async function eraseOrgData(client, orgId, options = {}) {
31
63
  if (orgId.trim() === '') {
32
64
  throw new Error('[org-erasure] orgId must be a non-empty string.');
33
65
  }
34
- const scoped = (0, tenant_isolation_extension_1.collectOrgScopedModels)(client, orgField, options.exemptModels ?? []);
66
+ const scoped = assertOrgErasureMatchesModels(client, options);
35
67
  const deletedByModel = {};
36
68
  let remaining = [...scoped];
37
69
  let passes = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"org-erasure.js","sourceRoot":"","sources":["../../../src/lib/tenant-isolation/org-erasure.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"org-erasure.js","sourceRoot":"","sources":["../../../src/lib/tenant-isolation/org-erasure.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/biome-database-nest",
3
- "version": "0.11.0",
3
+ "version": "0.12.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)",
package/src/index.ts CHANGED
@@ -97,8 +97,10 @@ export {
97
97
  type UnscopedModelDeclarations,
98
98
  } from './lib/tenant-isolation/unscoped-models';
99
99
  export {
100
+ assertOrgErasureMatchesModels,
100
101
  eraseOrgData,
101
102
  OrgErasureBlockedError,
103
+ OrgErasureMatchesNothingError,
102
104
  type EraseOrgDataOptions,
103
105
  type OrgErasureReport,
104
106
  } from './lib/tenant-isolation/org-erasure';
@@ -25,10 +25,19 @@
25
25
  *
26
26
  * The org filter is applied inside {@link eraseOrgData}; the runner passes the
27
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.
28
36
  */
29
37
  import {
30
38
  Controller,
31
39
  Inject,
40
+ Logger,
32
41
  Param,
33
42
  Post,
34
43
  UseGuards,
@@ -40,10 +49,12 @@ import { ServiceActorGuard } from '@xemahq/platform-common';
40
49
  import { InternalRouteAudience, XemaInternalRoute } from '@xemahq/xema-decorators';
41
50
 
42
51
  import {
52
+ assertOrgErasureMatchesModels,
43
53
  eraseOrgData,
44
54
  type EraseOrgDataOptions,
45
55
  type OrgErasureReport,
46
56
  } from '../tenant-isolation/org-erasure';
57
+ import { DEFAULT_ORG_FIELD } from '../tenant-isolation/tenant-isolation.extension';
47
58
 
48
59
  import type { TenantScopedApi } from './prisma-service-factory';
49
60
 
@@ -112,10 +123,33 @@ export class OrgErasureModule {
112
123
  providers: [
113
124
  {
114
125
  provide: ORG_ERASURE_RUNNER,
115
- useFactory: (prisma: AsSystemProvider): OrgErasureRunner => ({
116
- erase: (orgId: string): Promise<OrgErasureReport> =>
117
- eraseOrgData(prisma.asSystem(), orgId, eraseOptions),
118
- }),
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
+ },
119
153
  inject: [prismaServiceToken],
120
154
  },
121
155
  ],
@@ -19,10 +19,15 @@
19
19
  * row, or a cycle) blocks deletion — we FAIL LOUD with the blocked models
20
20
  * rather than leave a silent partial erasure. Idempotent + resumable: re-running
21
21
  * re-deletes already-gone rows as no-ops, so the orchestrator saga can retry.
22
+ *
23
+ * A participant that matches ZERO models is likewise a FAILURE, not a clean run
24
+ * — see {@link assertOrgErasureMatchesModels} for why, and for the distinction
25
+ * (models vs rows) that makes that assertion correct rather than a new bug.
22
26
  */
23
27
  import {
24
28
  DEFAULT_ORG_FIELD,
25
29
  collectOrgScopedModels,
30
+ readRuntimeModels,
26
31
  } from './tenant-isolation.extension';
27
32
 
28
33
  /** Prisma's error code for a foreign-key constraint failure on delete. */
@@ -68,6 +73,80 @@ export class OrgErasureBlockedError extends Error {
68
73
  }
69
74
  }
70
75
 
76
+ /**
77
+ * Thrown when an erasure participant's configuration matches NO org-scoped
78
+ * model at all — it is structurally incapable of deleting anything.
79
+ *
80
+ * This is a MISCONFIGURATION, never an empty org. `memory-api` shipped as a
81
+ * registered GDPR erasure participant whose every model spells the org column
82
+ * `organizationId` while the participant was registered with no `orgField`, so
83
+ * {@link collectOrgScopedModels} returned an empty set, the delete loop never
84
+ * executed, and every erasure answered `200 {totalDeleted: 0}` — which the
85
+ * orchestrator saga records as a SUCCESSFUL erasure. A Data Subject Request
86
+ * answered from that record is false in writing.
87
+ *
88
+ * There is no degraded mode here: a participant that cannot delete anything
89
+ * must not be allowed to report success.
90
+ */
91
+ export class OrgErasureMatchesNothingError extends Error {
92
+ constructor(
93
+ readonly orgField: string,
94
+ readonly knownModels: readonly string[],
95
+ readonly exemptModels: readonly string[],
96
+ ) {
97
+ super(
98
+ `[org-erasure] this erasure participant matched ZERO models on org ` +
99
+ `field "${orgField}" — it cannot delete anything, so it MUST NOT ` +
100
+ `report a successful erasure (the orchestrator would record proof of ` +
101
+ `an erasure that never happened). Known models: [${knownModels.join(
102
+ ', ',
103
+ )}]` +
104
+ (exemptModels.length > 0
105
+ ? `; exemptModels: [${exemptModels.join(', ')}]` +
106
+ ` — exempting every org-scoped model leaves nothing to erase`
107
+ : '') +
108
+ `. Fix the participant's orgField (or stop registering the service as ` +
109
+ `an org-erasure participant) rather than relaxing this check.`,
110
+ );
111
+ this.name = 'OrgErasureMatchesNothingError';
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Assert the participant can possibly delete something, and return the models
117
+ * it covers.
118
+ *
119
+ * **Models, never rows.** Zero MODELS matched means the participant is
120
+ * structurally incapable of erasing — a configuration defect. Zero ROWS deleted
121
+ * across a non-empty model set is a legitimate clean run: an org that genuinely
122
+ * holds no data in this service must succeed (and re-running an erasure must
123
+ * stay idempotent). Conflating the two would fail every honest erasure for an
124
+ * org with no data here, which is strictly worse than the bug this closes.
125
+ *
126
+ * Exported so a participant can gate at BOOT as well as per request:
127
+ * {@link OrgErasureModule} calls it while wiring its runner, so a service
128
+ * refuses to start rather than advertise `xema.org-data-erasure` it cannot
129
+ * honour. Boot proves the CONFIGURATION; the per-request call inside
130
+ * {@link eraseOrgData} proves the RUN that is about to be recorded as erasure
131
+ * evidence. Both are cheap and neither subsumes the other.
132
+ */
133
+ export function assertOrgErasureMatchesModels(
134
+ client: unknown,
135
+ options: EraseOrgDataOptions = {},
136
+ ): ReadonlySet<string> {
137
+ const orgField = options.orgField ?? DEFAULT_ORG_FIELD;
138
+ const exemptModels = options.exemptModels ?? [];
139
+ const scoped = collectOrgScopedModels(client, orgField, exemptModels);
140
+ if (scoped.size === 0) {
141
+ throw new OrgErasureMatchesNothingError(
142
+ orgField,
143
+ Object.keys(readRuntimeModels(client)),
144
+ exemptModels,
145
+ );
146
+ }
147
+ return scoped;
148
+ }
149
+
71
150
  interface DeleteManyDelegate {
72
151
  deleteMany(args: { where: Record<string, unknown> }): Promise<{ count: number }>;
73
152
  }
@@ -103,11 +182,9 @@ export async function eraseOrgData(
103
182
  throw new Error('[org-erasure] orgId must be a non-empty string.');
104
183
  }
105
184
 
106
- const scoped = collectOrgScopedModels(
107
- client,
108
- orgField,
109
- options.exemptModels ?? [],
110
- );
185
+ // Matching no model is a misconfiguration, not an empty org — never let it
186
+ // return `{ totalDeleted: 0 }`, which the orchestrator reads as success.
187
+ const scoped = assertOrgErasureMatchesModels(client, options);
111
188
 
112
189
  const deletedByModel: Record<string, number> = {};
113
190
  let remaining = [...scoped];