@ddd-ts-kit/knex 0.7.0 → 0.9.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.
Files changed (114) hide show
  1. package/README.md +91 -0
  2. package/dist/index.d.ts +21 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +21 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/outbox/add-outbox-tenant-column.d.ts +9 -0
  7. package/dist/outbox/add-outbox-tenant-column.d.ts.map +1 -0
  8. package/dist/outbox/add-outbox-tenant-column.js +14 -0
  9. package/dist/outbox/add-outbox-tenant-column.js.map +1 -0
  10. package/dist/outbox/create-outbox-tables.js +3 -0
  11. package/dist/outbox/create-outbox-tables.js.map +1 -1
  12. package/dist/outbox/knex-outbox-store.d.ts +7 -0
  13. package/dist/outbox/knex-outbox-store.d.ts.map +1 -1
  14. package/dist/outbox/knex-outbox-store.js +4 -0
  15. package/dist/outbox/knex-outbox-store.js.map +1 -1
  16. package/dist/outbox/outbox-tables-options.d.ts +2 -0
  17. package/dist/outbox/outbox-tables-options.d.ts.map +1 -1
  18. package/dist/read-model/knex-read-model-options.d.ts +11 -0
  19. package/dist/read-model/knex-read-model-options.d.ts.map +1 -1
  20. package/dist/read-model/knex-read-model-table.js +1 -1
  21. package/dist/read-model/knex-read-model-table.js.map +1 -1
  22. package/dist/read-model/knex-stamped-rows.d.ts +4 -1
  23. package/dist/read-model/knex-stamped-rows.d.ts.map +1 -1
  24. package/dist/read-model/knex-stamped-rows.js +15 -2
  25. package/dist/read-model/knex-stamped-rows.js.map +1 -1
  26. package/dist/tenancy/create-tenant-routed-unit-of-work.d.ts +13 -0
  27. package/dist/tenancy/create-tenant-routed-unit-of-work.d.ts.map +1 -0
  28. package/dist/tenancy/create-tenant-routed-unit-of-work.js +37 -0
  29. package/dist/tenancy/create-tenant-routed-unit-of-work.js.map +1 -0
  30. package/dist/tenancy/cross-tenant-unit-of-work-error.d.ts +8 -0
  31. package/dist/tenancy/cross-tenant-unit-of-work-error.d.ts.map +1 -0
  32. package/dist/tenancy/cross-tenant-unit-of-work-error.js +11 -0
  33. package/dist/tenancy/cross-tenant-unit-of-work-error.js.map +1 -0
  34. package/dist/tenancy/enable-tenant-isolation.d.ts +8 -0
  35. package/dist/tenancy/enable-tenant-isolation.d.ts.map +1 -0
  36. package/dist/tenancy/enable-tenant-isolation.js +11 -0
  37. package/dist/tenancy/enable-tenant-isolation.js.map +1 -0
  38. package/dist/tenancy/invalid-tenant-identifier-error.d.ts +5 -0
  39. package/dist/tenancy/invalid-tenant-identifier-error.d.ts.map +1 -0
  40. package/dist/tenancy/invalid-tenant-identifier-error.js +8 -0
  41. package/dist/tenancy/invalid-tenant-identifier-error.js.map +1 -0
  42. package/dist/tenancy/migrate-every-tenant.d.ts +10 -0
  43. package/dist/tenancy/migrate-every-tenant.d.ts.map +1 -0
  44. package/dist/tenancy/migrate-every-tenant.js +16 -0
  45. package/dist/tenancy/migrate-every-tenant.js.map +1 -0
  46. package/dist/tenancy/migrate-tenant-schema.d.ts +9 -0
  47. package/dist/tenancy/migrate-tenant-schema.d.ts.map +1 -0
  48. package/dist/tenancy/migrate-tenant-schema.js +15 -0
  49. package/dist/tenancy/migrate-tenant-schema.js.map +1 -0
  50. package/dist/tenancy/own-tenant-table.d.ts +10 -0
  51. package/dist/tenancy/own-tenant-table.d.ts.map +1 -0
  52. package/dist/tenancy/own-tenant-table.js +11 -0
  53. package/dist/tenancy/own-tenant-table.js.map +1 -0
  54. package/dist/tenancy/statement-outside-unit-of-work-error.d.ts +5 -0
  55. package/dist/tenancy/statement-outside-unit-of-work-error.d.ts.map +1 -0
  56. package/dist/tenancy/statement-outside-unit-of-work-error.js +8 -0
  57. package/dist/tenancy/statement-outside-unit-of-work-error.js.map +1 -0
  58. package/dist/tenancy/table-in.d.ts +6 -0
  59. package/dist/tenancy/table-in.d.ts.map +1 -0
  60. package/dist/tenancy/table-in.js +6 -0
  61. package/dist/tenancy/table-in.js.map +1 -0
  62. package/dist/tenancy/tenant-databases-options.d.ts +15 -0
  63. package/dist/tenancy/tenant-databases-options.d.ts.map +1 -0
  64. package/dist/tenancy/tenant-databases-options.js +2 -0
  65. package/dist/tenancy/tenant-databases-options.js.map +1 -0
  66. package/dist/tenancy/tenant-databases.d.ts +29 -0
  67. package/dist/tenancy/tenant-databases.d.ts.map +1 -0
  68. package/dist/tenancy/tenant-databases.js +80 -0
  69. package/dist/tenancy/tenant-databases.js.map +1 -0
  70. package/dist/tenancy/tenant-isolation-statements.d.ts +12 -0
  71. package/dist/tenancy/tenant-isolation-statements.d.ts.map +1 -0
  72. package/dist/tenancy/tenant-isolation-statements.js +31 -0
  73. package/dist/tenancy/tenant-isolation-statements.js.map +1 -0
  74. package/dist/tenancy/tenant-of-migration.d.ts +7 -0
  75. package/dist/tenancy/tenant-of-migration.d.ts.map +1 -0
  76. package/dist/tenancy/tenant-of-migration.js +12 -0
  77. package/dist/tenancy/tenant-of-migration.js.map +1 -0
  78. package/dist/tenancy/tenant-schema-of.d.ts +8 -0
  79. package/dist/tenancy/tenant-schema-of.d.ts.map +1 -0
  80. package/dist/tenancy/tenant-schema-of.js +21 -0
  81. package/dist/tenancy/tenant-schema-of.js.map +1 -0
  82. package/dist/tenancy/tenant-schema-table.d.ts +11 -0
  83. package/dist/tenancy/tenant-schema-table.d.ts.map +1 -0
  84. package/dist/tenancy/tenant-schema-table.js +13 -0
  85. package/dist/tenancy/tenant-schema-table.js.map +1 -0
  86. package/dist/tenancy/tenant-schema-target.d.ts +6 -0
  87. package/dist/tenancy/tenant-schema-target.d.ts.map +1 -0
  88. package/dist/tenancy/tenant-schema-target.js +2 -0
  89. package/dist/tenancy/tenant-schema-target.js.map +1 -0
  90. package/dist/tenancy/tenant-search-path.d.ts +11 -0
  91. package/dist/tenancy/tenant-search-path.d.ts.map +1 -0
  92. package/dist/tenancy/tenant-search-path.js +16 -0
  93. package/dist/tenancy/tenant-search-path.js.map +1 -0
  94. package/dist/tenancy/tenant-session.d.ts +10 -0
  95. package/dist/tenancy/tenant-session.d.ts.map +1 -0
  96. package/dist/tenancy/tenant-session.js +21 -0
  97. package/dist/tenancy/tenant-session.js.map +1 -0
  98. package/dist/tenancy/tenant-table-target.d.ts +7 -0
  99. package/dist/tenancy/tenant-table-target.d.ts.map +1 -0
  100. package/dist/tenancy/tenant-table-target.js +2 -0
  101. package/dist/tenancy/tenant-table-target.js.map +1 -0
  102. package/dist/tenancy/tenant-table.d.ts +12 -0
  103. package/dist/tenancy/tenant-table.d.ts.map +1 -0
  104. package/dist/tenancy/tenant-table.js +12 -0
  105. package/dist/tenancy/tenant-table.js.map +1 -0
  106. package/dist/tenancy/where-tenant.d.ts +10 -0
  107. package/dist/tenancy/where-tenant.d.ts.map +1 -0
  108. package/dist/tenancy/where-tenant.js +15 -0
  109. package/dist/tenancy/where-tenant.js.map +1 -0
  110. package/dist/unit-of-work/create-knex-unit-of-work.d.ts +8 -1
  111. package/dist/unit-of-work/create-knex-unit-of-work.d.ts.map +1 -1
  112. package/dist/unit-of-work/create-knex-unit-of-work.js +7 -2
  113. package/dist/unit-of-work/create-knex-unit-of-work.js.map +1 -1
  114. package/package.json +4 -4
@@ -0,0 +1,8 @@
1
+ import type { Knex } from 'knex';
2
+ import type { TenantTableTarget } from './tenant-table-target.js';
3
+ /**
4
+ * Row-level security on a table of tenant data, from the migration that creates it:
5
+ * `await enableTenantIsolation(knex, { schema: 'billing', table: 'invoices' })`. PostgreSQL only.
6
+ */
7
+ export declare function enableTenantIsolation(knex: Knex, target: TenantTableTarget): Promise<void>;
8
+ //# sourceMappingURL=enable-tenant-isolation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enable-tenant-isolation.d.ts","sourceRoot":"","sources":["../../src/tenancy/enable-tenant-isolation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhG"}
@@ -0,0 +1,11 @@
1
+ import { tenantIsolationStatements } from './tenant-isolation-statements.js';
2
+ /**
3
+ * Row-level security on a table of tenant data, from the migration that creates it:
4
+ * `await enableTenantIsolation(knex, { schema: 'billing', table: 'invoices' })`. PostgreSQL only.
5
+ */
6
+ export async function enableTenantIsolation(knex, target) {
7
+ for (const statement of tenantIsolationStatements(target)) {
8
+ await knex.raw(statement);
9
+ }
10
+ }
11
+ //# sourceMappingURL=enable-tenant-isolation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enable-tenant-isolation.js","sourceRoot":"","sources":["../../src/tenancy/enable-tenant-isolation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAG7E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,MAAyB;IAC/E,KAAK,MAAM,SAAS,IAAI,yBAAyB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** A tenant id that cannot name a schema: refused rather than escaped into an identifier. */
2
+ export declare class InvalidTenantIdentifierError extends Error {
3
+ constructor(tenantId: string, reason: string);
4
+ }
5
+ //# sourceMappingURL=invalid-tenant-identifier-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalid-tenant-identifier-error.d.ts","sourceRoot":"","sources":["../../src/tenancy/invalid-tenant-identifier-error.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,YAAY,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAG3C;CACF"}
@@ -0,0 +1,8 @@
1
+ /** A tenant id that cannot name a schema: refused rather than escaped into an identifier. */
2
+ export class InvalidTenantIdentifierError extends Error {
3
+ constructor(tenantId, reason) {
4
+ super(`The tenant id "${tenantId}" cannot name a schema: ${reason}`);
5
+ this.name = 'InvalidTenantIdentifierError';
6
+ }
7
+ }
8
+ //# sourceMappingURL=invalid-tenant-identifier-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalid-tenant-identifier-error.js","sourceRoot":"","sources":["../../src/tenancy/invalid-tenant-identifier-error.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IACrD,YAAY,QAAgB,EAAE,MAAc;QAC1C,KAAK,CAAC,kBAAkB,QAAQ,2BAA2B,MAAM,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ import type { Knex } from 'knex';
2
+ import type { TenantDirectory } from '@ddd-ts-kit/kernel';
3
+ import type { TenantDatabases } from './tenant-databases.js';
4
+ /**
5
+ * Applies `migrate` to the database of every tenant of the directory, one after the other, and
6
+ * stops at the first that fails, naming it: the tenants before it are migrated, the ones after
7
+ * are not, and running it again resumes. Provisioning a tenant is the same call for one tenant.
8
+ */
9
+ export declare function migrateEveryTenant(databases: TenantDatabases, directory: TenantDirectory, migrate: (knex: Knex, tenantId: string) => Promise<void>): Promise<void>;
10
+ //# sourceMappingURL=migrate-every-tenant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-every-tenant.d.ts","sourceRoot":"","sources":["../../src/tenancy/migrate-every-tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAC1B,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACvD,OAAO,CAAC,IAAI,CAAC,CAQf"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Applies `migrate` to the database of every tenant of the directory, one after the other, and
3
+ * stops at the first that fails, naming it: the tenants before it are migrated, the ones after
4
+ * are not, and running it again resumes. Provisioning a tenant is the same call for one tenant.
5
+ */
6
+ export async function migrateEveryTenant(databases, directory, migrate) {
7
+ for (const tenantId of await directory.tenants()) {
8
+ try {
9
+ await databases.lease(tenantId, (database) => migrate(database, tenantId));
10
+ }
11
+ catch (error) {
12
+ throw new Error(`The migration of tenant "${tenantId}" failed`, { cause: error });
13
+ }
14
+ }
15
+ }
16
+ //# sourceMappingURL=migrate-every-tenant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-every-tenant.js","sourceRoot":"","sources":["../../src/tenancy/migrate-every-tenant.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAA0B,EAC1B,SAA0B,EAC1B,OAAwD;IAExD,KAAK,MAAM,QAAQ,IAAI,MAAM,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,UAAU,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Applies the migrations of `config` (the tenant migrations, a directory of their own) for one
4
+ * tenant: they read the tenant with `tenantOfMigration(knex)`, and the record of what was
5
+ * applied lives in the tenant's own schema, so every tenant moves forward on its own. This is
6
+ * also how a tenant is provisioned: its first run creates its schemas and tables.
7
+ */
8
+ export declare function migrateTenantSchema(knex: Knex, tenantId: string, config: Knex.MigratorConfig): Promise<void>;
9
+ //# sourceMappingURL=migrate-tenant-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-tenant-schema.d.ts","sourceRoot":"","sources":["../../src/tenancy/migrate-tenant-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAMjC;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAKlH"}
@@ -0,0 +1,15 @@
1
+ import { tenantSchemaOf } from './tenant-schema-of.js';
2
+ /** Where the migrations of each tenant are recorded: `migrations__<tenant>`. */
3
+ const BOOKKEEPING_SCHEMA = 'migrations';
4
+ /**
5
+ * Applies the migrations of `config` (the tenant migrations, a directory of their own) for one
6
+ * tenant: they read the tenant with `tenantOfMigration(knex)`, and the record of what was
7
+ * applied lives in the tenant's own schema, so every tenant moves forward on its own. This is
8
+ * also how a tenant is provisioned: its first run creates its schemas and tables.
9
+ */
10
+ export async function migrateTenantSchema(knex, tenantId, config) {
11
+ const schemaName = tenantSchemaOf(BOOKKEEPING_SCHEMA, tenantId);
12
+ await knex.schema.createSchemaIfNotExists(schemaName);
13
+ await knex.withUserParams({ ...knex.userParams, tenantId }).migrate.latest({ ...config, schemaName });
14
+ }
15
+ //# sourceMappingURL=migrate-tenant-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-tenant-schema.js","sourceRoot":"","sources":["../../src/tenancy/migrate-tenant-schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,gFAAgF;AAChF,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAAU,EAAE,QAAgB,EAAE,MAA2B;IACjG,MAAM,UAAU,GAAG,cAAc,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;IAEhE,MAAM,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC;IACtD,MAAM,IAAI,CAAC,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AACxG,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Knex } from 'knex';
2
+ import { type ExecutionScope } from '@ddd-ts-kit/kernel';
3
+ import type { KnexContext } from '../unit-of-work/knex-context.js';
4
+ import type { TenantTableTarget } from './tenant-table-target.js';
5
+ /**
6
+ * The rows of the one tenant of the work in progress, for an update or a delete: an elevation
7
+ * widens reads, never writes. An insert spreads `tenantRow(tenantScopeOf(scope))` into its row.
8
+ */
9
+ export declare function ownTenantTable<Row extends object>(context: KnexContext, scope: ExecutionScope, target: TenantTableTarget): Knex.QueryBuilder<Row, Row[]>;
10
+ //# sourceMappingURL=own-tenant-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"own-tenant-table.d.ts","sourceRoot":"","sources":["../../src/tenancy/own-tenant-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,SAAS,MAAM,EAC/C,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,iBAAiB,GACxB,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAE/B"}
@@ -0,0 +1,11 @@
1
+ import { tenantScopeOf } from '@ddd-ts-kit/kernel';
2
+ import { tableIn } from './table-in.js';
3
+ import { TENANT_COLUMN } from './where-tenant.js';
4
+ /**
5
+ * The rows of the one tenant of the work in progress, for an update or a delete: an elevation
6
+ * widens reads, never writes. An insert spreads `tenantRow(tenantScopeOf(scope))` into its row.
7
+ */
8
+ export function ownTenantTable(context, scope, target) {
9
+ return tableIn(context, target).where(target.column ?? TENANT_COLUMN, tenantScopeOf(scope).tenantId);
10
+ }
11
+ //# sourceMappingURL=own-tenant-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"own-tenant-table.js","sourceRoot":"","sources":["../../src/tenancy/own-tenant-table.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAoB,EACpB,KAAqB,EACrB,MAAyB;IAEzB,OAAO,OAAO,CAAM,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,IAAI,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC5G,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** A statement asked of the database-per-tenant unit of work outside of a transaction: it would hold no tenant's database. */
2
+ export declare class StatementOutsideUnitOfWorkError extends Error {
3
+ constructor();
4
+ }
5
+ //# sourceMappingURL=statement-outside-unit-of-work-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statement-outside-unit-of-work-error.d.ts","sourceRoot":"","sources":["../../src/tenancy/statement-outside-unit-of-work-error.ts"],"names":[],"mappings":"AAAA,8HAA8H;AAC9H,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,cAGC;CACF"}
@@ -0,0 +1,8 @@
1
+ /** A statement asked of the database-per-tenant unit of work outside of a transaction: it would hold no tenant's database. */
2
+ export class StatementOutsideUnitOfWorkError extends Error {
3
+ constructor() {
4
+ super('Under database per tenant, every statement runs inside the unit of work (run): none is open');
5
+ this.name = 'StatementOutsideUnitOfWorkError';
6
+ }
7
+ }
8
+ //# sourceMappingURL=statement-outside-unit-of-work-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statement-outside-unit-of-work-error.js","sourceRoot":"","sources":["../../src/tenancy/statement-outside-unit-of-work-error.ts"],"names":[],"mappings":"AAAA,8HAA8H;AAC9H,MAAM,OAAO,+BAAgC,SAAQ,KAAK;IACxD;QACE,KAAK,CAAC,6FAA6F,CAAC,CAAC;QACrG,IAAI,CAAC,IAAI,GAAG,iCAAiC,CAAC;IAChD,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import type { Knex } from 'knex';
2
+ import type { KnexContext } from '../unit-of-work/knex-context.js';
3
+ import type { TenantTableTarget } from './tenant-table-target.js';
4
+ /** The table, in its schema, on the executor of the unit of work. */
5
+ export declare function tableIn<Row extends object>(context: KnexContext, target: TenantTableTarget): Knex.QueryBuilder<Row, Row[]>;
6
+ //# sourceMappingURL=table-in.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-in.d.ts","sourceRoot":"","sources":["../../src/tenancy/table-in.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,qEAAqE;AACrE,wBAAgB,OAAO,CAAC,GAAG,SAAS,MAAM,EACxC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,iBAAiB,GACxB,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAI/B"}
@@ -0,0 +1,6 @@
1
+ /** The table, in its schema, on the executor of the unit of work. */
2
+ export function tableIn(context, target) {
3
+ const table = context.executor()(target.table);
4
+ return target.schema === undefined ? table : table.withSchema(target.schema);
5
+ }
6
+ //# sourceMappingURL=table-in.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-in.js","sourceRoot":"","sources":["../../src/tenancy/table-in.ts"],"names":[],"mappings":"AAIA,qEAAqE;AACrE,MAAM,UAAU,OAAO,CACrB,OAAoB,EACpB,MAAyB;IAEzB,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAa,MAAM,CAAC,KAAK,CAAC,CAAC;IAE3D,OAAO,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Knex } from 'knex';
2
+ export interface TenantDatabasesOptions {
3
+ /** The connection of a tenant's database: its own configuration, pool included (keep `pool.max` small). */
4
+ readonly connectionOf: (tenantId: string) => Knex.Config;
5
+ /**
6
+ * How many tenants have an instance at once, those being closed included: an idle one, the
7
+ * least recently used, is closed when a new tenant needs one, and work waits when every
8
+ * instance is held. Default 20. The connections the server must accept are at most
9
+ * `maxTenants * pool.max` per instance of the application.
10
+ */
11
+ readonly maxTenants?: number;
12
+ /** Builds a knex instance; default `knex(config)`. */
13
+ readonly open?: (config: Knex.Config) => Knex;
14
+ }
15
+ //# sourceMappingURL=tenant-databases-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-databases-options.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-databases-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACrC,2GAA2G;IAC3G,QAAQ,CAAC,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC;IACzD;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;CAC/C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tenant-databases-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-databases-options.js","sourceRoot":"","sources":["../../src/tenancy/tenant-databases-options.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import { type Knex } from 'knex';
2
+ import type { TenantDatabasesOptions } from './tenant-databases-options.js';
3
+ /**
4
+ * One database per tenant: a knex instance (its own bounded pool) per tenant, opened on first
5
+ * use. At most `maxTenants` instances exist at once, those being closed included, so the
6
+ * connections stay within `maxTenants * pool.max`. A tenant's work holds its instance by a lease
7
+ * (`lease`): an instance is closed only when no work holds it, the least recently used first,
8
+ * and work for another tenant waits for a place rather than opening beyond the bound.
9
+ * `createTenantRoutedUnitOfWork` takes a lease for each transaction.
10
+ */
11
+ export declare class TenantDatabases {
12
+ private readonly options;
13
+ private readonly pools;
14
+ private readonly draining;
15
+ private waiting;
16
+ constructor(options: TenantDatabasesOptions);
17
+ /** Runs `work` with the database of the tenant, held open until `work` settles. */
18
+ lease<T>(tenantId: string, work: (database: Knex) => Promise<T>): Promise<T>;
19
+ /** The tenants whose pool is open, least recently used first. */
20
+ tenants(): readonly string[];
21
+ close(): Promise<void>;
22
+ /** The instance of the tenant, most recently used now; once the bound has room for it when it has none. */
23
+ private acquire;
24
+ /** A new instance when the bound has room, after closing an idle one if it must; `undefined` to wait. */
25
+ private openedWithinBound;
26
+ private drain;
27
+ private wake;
28
+ }
29
+ //# sourceMappingURL=tenant-databases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-databases.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-databases.ts"],"names":[],"mappings":"AAAA,OAAa,EAAE,KAAK,IAAI,EAAE,MAAM,MAAM,CAAC;AAEvC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAU5E;;;;;;;GAOG;AACH,qBAAa,eAAe;IAOd,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IAErD,OAAO,CAAC,OAAO,CAAsB;IAErC,YAA6B,OAAO,EAAE,sBAAsB,EAAI;IAEhE,mFAAmF;IAC7E,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CASjF;IAED,iEAAiE;IACjE,OAAO,IAAI,SAAS,MAAM,EAAE,CAE3B;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;IAED,2GAA2G;YAC7F,OAAO;IAgBrB,yGAAyG;IACzG,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,IAAI;CASb"}
@@ -0,0 +1,80 @@
1
+ import knex, {} from 'knex';
2
+ import { assertTenantId } from '@ddd-ts-kit/kernel';
3
+ const DEFAULT_MAX_TENANTS = 20;
4
+ /**
5
+ * One database per tenant: a knex instance (its own bounded pool) per tenant, opened on first
6
+ * use. At most `maxTenants` instances exist at once, those being closed included, so the
7
+ * connections stay within `maxTenants * pool.max`. A tenant's work holds its instance by a lease
8
+ * (`lease`): an instance is closed only when no work holds it, the least recently used first,
9
+ * and work for another tenant waits for a place rather than opening beyond the bound.
10
+ * `createTenantRoutedUnitOfWork` takes a lease for each transaction.
11
+ */
12
+ export class TenantDatabases {
13
+ options;
14
+ pools = new Map();
15
+ draining = new Set();
16
+ waiting = [];
17
+ constructor(options) {
18
+ this.options = options;
19
+ }
20
+ /** Runs `work` with the database of the tenant, held open until `work` settles. */
21
+ async lease(tenantId, work) {
22
+ const pool = await this.acquire(assertTenantId(tenantId));
23
+ try {
24
+ return await work(pool.knex);
25
+ }
26
+ finally {
27
+ pool.leases -= 1;
28
+ this.wake();
29
+ }
30
+ }
31
+ /** The tenants whose pool is open, least recently used first. */
32
+ tenants() {
33
+ return [...this.pools.keys()];
34
+ }
35
+ async close() {
36
+ const all = [...this.pools.values()];
37
+ this.pools.clear();
38
+ await Promise.all([...all.map((pool) => pool.knex.destroy()), ...this.draining]);
39
+ }
40
+ /** The instance of the tenant, most recently used now; once the bound has room for it when it has none. */
41
+ async acquire(tenantId) {
42
+ const pool = this.pools.get(tenantId) ?? this.openedWithinBound(tenantId);
43
+ if (pool === undefined) {
44
+ await new Promise((resolve) => this.waiting.push(resolve));
45
+ return this.acquire(tenantId);
46
+ }
47
+ this.pools.delete(tenantId);
48
+ this.pools.set(tenantId, pool);
49
+ pool.leases += 1;
50
+ return pool;
51
+ }
52
+ /** A new instance when the bound has room, after closing an idle one if it must; `undefined` to wait. */
53
+ openedWithinBound(tenantId) {
54
+ const limit = this.options.maxTenants ?? DEFAULT_MAX_TENANTS;
55
+ if (this.pools.size + this.draining.size < limit) {
56
+ return { knex: (this.options.open ?? knex)(this.options.connectionOf(tenantId)), leases: 0 };
57
+ }
58
+ const idle = [...this.pools].find(([, pool]) => pool.leases === 0);
59
+ if (idle !== undefined) {
60
+ this.drain(idle[0], idle[1]);
61
+ }
62
+ return undefined;
63
+ }
64
+ drain(tenantId, pool) {
65
+ this.pools.delete(tenantId);
66
+ const drained = pool.knex.destroy().finally(() => {
67
+ this.draining.delete(drained);
68
+ this.wake();
69
+ });
70
+ this.draining.add(drained);
71
+ }
72
+ wake() {
73
+ const waiting = this.waiting;
74
+ this.waiting = [];
75
+ for (const resolve of waiting) {
76
+ resolve();
77
+ }
78
+ }
79
+ }
80
+ //# sourceMappingURL=tenant-databases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-databases.js","sourceRoot":"","sources":["../../src/tenancy/tenant-databases.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { TenantTableTarget } from './tenant-table-target.js';
2
+ /**
3
+ * The PostgreSQL statements that make the database itself refuse a row of another tenant, the
4
+ * second line behind the tenant condition of every statement: row-level security enabled and
5
+ * forced (so that the owner of the table obeys it too), and one policy per command. A select
6
+ * sees the tenant of the work and the tenants an elevation reaches; an insert, an update and a
7
+ * delete only ever touch `ddd_ts_kit.tenant_id`, elevated or not, even without a condition of
8
+ * their own. Work that set nothing sees nothing and writes nothing. The role of the application
9
+ * must not be a superuser nor have `BYPASSRLS`: both ignore every policy.
10
+ */
11
+ export declare function tenantIsolationStatements(target: TenantTableTarget): readonly string[];
12
+ //# sourceMappingURL=tenant-isolation-statements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-isolation-statements.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-isolation-statements.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,MAAM,EAAE,CAatF"}
@@ -0,0 +1,31 @@
1
+ import { TENANT_ISOLATION_POLICIES, tenantPolicyExpressions } from '@ddd-ts-kit/kernel';
2
+ import { TENANT_COLUMN } from './where-tenant.js';
3
+ /**
4
+ * The PostgreSQL statements that make the database itself refuse a row of another tenant, the
5
+ * second line behind the tenant condition of every statement: row-level security enabled and
6
+ * forced (so that the owner of the table obeys it too), and one policy per command. A select
7
+ * sees the tenant of the work and the tenants an elevation reaches; an insert, an update and a
8
+ * delete only ever touch `ddd_ts_kit.tenant_id`, elevated or not, even without a condition of
9
+ * their own. Work that set nothing sees nothing and writes nothing. The role of the application
10
+ * must not be a superuser nor have `BYPASSRLS`: both ignore every policy.
11
+ */
12
+ export function tenantIsolationStatements(target) {
13
+ const table = qualified(target);
14
+ const { read, write } = tenantPolicyExpressions(quoted(target.column ?? TENANT_COLUMN));
15
+ const policy = (name, rest) => `create policy ${name} on ${table} ${rest}`;
16
+ return [
17
+ `alter table ${table} enable row level security`,
18
+ `alter table ${table} force row level security`,
19
+ policy(TENANT_ISOLATION_POLICIES.select, `for select using (${read})`),
20
+ policy(TENANT_ISOLATION_POLICIES.insert, `for insert with check (${write})`),
21
+ policy(TENANT_ISOLATION_POLICIES.update, `for update using (${write}) with check (${write})`),
22
+ policy(TENANT_ISOLATION_POLICIES.delete, `for delete using (${write})`),
23
+ ];
24
+ }
25
+ function qualified({ table, schema }) {
26
+ return schema === undefined ? quoted(table) : `${quoted(schema)}.${quoted(table)}`;
27
+ }
28
+ function quoted(identifier) {
29
+ return `"${identifier.replaceAll('"', '""')}"`;
30
+ }
31
+ //# sourceMappingURL=tenant-isolation-statements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-isolation-statements.js","sourceRoot":"","sources":["../../src/tenancy/tenant-isolation-statements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAExF,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAyB;IACjE,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC;IACxF,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,IAAY,EAAU,EAAE,CAAC,iBAAiB,IAAI,OAAO,KAAK,IAAI,IAAI,EAAE,CAAC;IAEnG,OAAO;QACL,eAAe,KAAK,4BAA4B;QAChD,eAAe,KAAK,2BAA2B;QAC/C,MAAM,CAAC,yBAAyB,CAAC,MAAM,EAAE,qBAAqB,IAAI,GAAG,CAAC;QACtE,MAAM,CAAC,yBAAyB,CAAC,MAAM,EAAE,0BAA0B,KAAK,GAAG,CAAC;QAC5E,MAAM,CAAC,yBAAyB,CAAC,MAAM,EAAE,qBAAqB,KAAK,iBAAiB,KAAK,GAAG,CAAC;QAC7F,MAAM,CAAC,yBAAyB,CAAC,MAAM,EAAE,qBAAqB,KAAK,GAAG,CAAC;KACxE,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAqB;IACrD,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACrF,CAAC;AAED,SAAS,MAAM,CAAC,UAAkB;IAChC,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AACjD,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * In a migration run by `migrateTenantSchema`: the tenant it is applied for, to name its schemas
4
+ * with `tenantSchemaOf`. A migration run otherwise names none, and this throws.
5
+ */
6
+ export declare function tenantOfMigration(knex: Knex): string;
7
+ //# sourceMappingURL=tenant-of-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-of-migration.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-of-migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAUpD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * In a migration run by `migrateTenantSchema`: the tenant it is applied for, to name its schemas
3
+ * with `tenantSchemaOf`. A migration run otherwise names none, and this throws.
4
+ */
5
+ export function tenantOfMigration(knex) {
6
+ const tenantId = knex.userParams['tenantId'];
7
+ if (typeof tenantId !== 'string') {
8
+ throw new TypeError('This migration is applied per tenant: run it with migrateTenantSchema(knex, tenantId, config).');
9
+ }
10
+ return tenantId;
11
+ }
12
+ //# sourceMappingURL=tenant-of-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-of-migration.js","sourceRoot":"","sources":["../../src/tenancy/tenant-of-migration.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAU;IAC1C,MAAM,QAAQ,GAAY,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAEtD,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CACjB,gGAAgG,CACjG,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The schema of a context for one tenant, under the schema-per-tenant strategy:
3
+ * `tenantSchemaOf('billing', 'acme')` is `billing__acme`. A tenant id that a schema name could
4
+ * not hold as it is (outside the grammar of `isTenantId`, or too long with the base) is refused, never altered: two tenants
5
+ * must never share a schema.
6
+ */
7
+ export declare function tenantSchemaOf(base: string, tenantId: string): string;
8
+ //# sourceMappingURL=tenant-schema-of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-schema-of.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-schema-of.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYrE"}
@@ -0,0 +1,21 @@
1
+ import { isTenantId } from '@ddd-ts-kit/kernel';
2
+ import { InvalidTenantIdentifierError } from './invalid-tenant-identifier-error.js';
3
+ /** The longest identifier PostgreSQL keeps whole: a longer one is truncated, and two tenants could meet. */
4
+ const LONGEST_IDENTIFIER = 63;
5
+ /**
6
+ * The schema of a context for one tenant, under the schema-per-tenant strategy:
7
+ * `tenantSchemaOf('billing', 'acme')` is `billing__acme`. A tenant id that a schema name could
8
+ * not hold as it is (outside the grammar of `isTenantId`, or too long with the base) is refused, never altered: two tenants
9
+ * must never share a schema.
10
+ */
11
+ export function tenantSchemaOf(base, tenantId) {
12
+ if (!isTenantId(tenantId)) {
13
+ throw new InvalidTenantIdentifierError(tenantId, 'it is outside the grammar of isTenantId');
14
+ }
15
+ const schema = `${base}__${tenantId}`;
16
+ if (schema.length > LONGEST_IDENTIFIER) {
17
+ throw new InvalidTenantIdentifierError(tenantId, `"${schema}" is longer than ${LONGEST_IDENTIFIER} characters`);
18
+ }
19
+ return schema;
20
+ }
21
+ //# sourceMappingURL=tenant-schema-of.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-schema-of.js","sourceRoot":"","sources":["../../src/tenancy/tenant-schema-of.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AAEpF,4GAA4G;AAC5G,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,QAAgB;IAC3D,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,4BAA4B,CAAC,QAAQ,EAAE,yCAAyC,CAAC,CAAC;IAC9F,CAAC;IAED,MAAM,MAAM,GAAG,GAAG,IAAI,KAAK,QAAQ,EAAE,CAAC;IAEtC,IAAI,MAAM,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;QACvC,MAAM,IAAI,4BAA4B,CAAC,QAAQ,EAAE,IAAI,MAAM,oBAAoB,kBAAkB,aAAa,CAAC,CAAC;IAClH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Knex } from 'knex';
2
+ import { type ExecutionScope } from '@ddd-ts-kit/kernel';
3
+ import type { KnexContext } from '../unit-of-work/knex-context.js';
4
+ import type { TenantSchemaTarget } from './tenant-schema-target.js';
5
+ /**
6
+ * The table in the schema of the tenant of the work in progress (`<schema>__<tenant>`): reads
7
+ * and writes reach that tenant's copy only. Work that names no tenant is refused, an elevation
8
+ * included: across tenants, staff read one tenant schema at a time.
9
+ */
10
+ export declare function tenantSchemaTable<Row extends object>(context: KnexContext, scope: ExecutionScope, target: TenantSchemaTarget): Knex.QueryBuilder<Row, Row[]>;
11
+ //# sourceMappingURL=tenant-schema-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-schema-table.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-schema-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAGnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,SAAS,MAAM,EAClD,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,kBAAkB,GACzB,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAI/B"}
@@ -0,0 +1,13 @@
1
+ import { tenantScopeOf } from '@ddd-ts-kit/kernel';
2
+ import { tableIn } from './table-in.js';
3
+ import { tenantSchemaOf } from './tenant-schema-of.js';
4
+ /**
5
+ * The table in the schema of the tenant of the work in progress (`<schema>__<tenant>`): reads
6
+ * and writes reach that tenant's copy only. Work that names no tenant is refused, an elevation
7
+ * included: across tenants, staff read one tenant schema at a time.
8
+ */
9
+ export function tenantSchemaTable(context, scope, target) {
10
+ const schema = tenantSchemaOf(target.schema, tenantScopeOf(scope).tenantId);
11
+ return tableIn(context, { table: target.table, schema });
12
+ }
13
+ //# sourceMappingURL=tenant-schema-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-schema-table.js","sourceRoot":"","sources":["../../src/tenancy/tenant-schema-table.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAoB,EACpB,KAAqB,EACrB,MAA0B;IAE1B,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC;IAE5E,OAAO,OAAO,CAAM,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,6 @@
1
+ /** A table of a context under the schema-per-tenant strategy: its name and the schema of the context. */
2
+ export interface TenantSchemaTarget {
3
+ readonly table: string;
4
+ readonly schema: string;
5
+ }
6
+ //# sourceMappingURL=tenant-schema-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-schema-target.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-schema-target.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tenant-schema-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-schema-target.js","sourceRoot":"","sources":["../../src/tenancy/tenant-schema-target.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import type { Knex } from 'knex';
2
+ import type { ExecutionScope } from '@ddd-ts-kit/kernel';
3
+ /**
4
+ * The `begin` of a unit of work under the schema-per-tenant strategy, for SQL that names its
5
+ * tables without a schema: the search path of the transaction is the tenant's schemas of the
6
+ * contexts given, and nothing else, so that a missing table fails instead of falling through to
7
+ * another schema. Work without a tenant gets an empty search path. The kit's own adapters name
8
+ * their schema (`tenantSchemaTable`) and do not need it.
9
+ */
10
+ export declare function tenantSearchPath(scope: ExecutionScope, schemas: readonly string[]): (transaction: Knex.Transaction) => Promise<void>;
11
+ //# sourceMappingURL=tenant-search-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-search-path.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-search-path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAOlD"}
@@ -0,0 +1,16 @@
1
+ import { tenantSchemaOf } from './tenant-schema-of.js';
2
+ /**
3
+ * The `begin` of a unit of work under the schema-per-tenant strategy, for SQL that names its
4
+ * tables without a schema: the search path of the transaction is the tenant's schemas of the
5
+ * contexts given, and nothing else, so that a missing table fails instead of falling through to
6
+ * another schema. Work without a tenant gets an empty search path. The kit's own adapters name
7
+ * their schema (`tenantSchemaTable`) and do not need it.
8
+ */
9
+ export function tenantSearchPath(scope, schemas) {
10
+ return async (transaction) => {
11
+ const tenantId = scope.current()?.tenantId;
12
+ const path = tenantId === undefined ? [] : schemas.map((schema) => `"${tenantSchemaOf(schema, tenantId)}"`);
13
+ await transaction.raw('select set_config(?, ?, true)', ['search_path', path.length === 0 ? '""' : path.join(', ')]);
14
+ };
15
+ }
16
+ //# sourceMappingURL=tenant-search-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-search-path.js","sourceRoot":"","sources":["../../src/tenancy/tenant-search-path.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAqB,EACrB,OAA0B;IAE1B,OAAO,KAAK,EAAE,WAAW,EAAE,EAAE;QAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC;QAC3C,MAAM,IAAI,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;QAE5G,MAAM,WAAW,CAAC,GAAG,CAAC,+BAA+B,EAAE,CAAC,aAAa,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Knex } from 'knex';
2
+ import { type ExecutionScope } from '@ddd-ts-kit/kernel';
3
+ /**
4
+ * The `begin` of a unit of work under row-level security: sets the tenant settings for the
5
+ * transaction only (`set_config(..., true)`), from the execution context of the work, so that a
6
+ * pooled connection never carries them to the next transaction. Work without a tenant sets them
7
+ * empty, and the policies then show it nothing.
8
+ */
9
+ export declare function tenantSession(scope: ExecutionScope): (transaction: Knex.Transaction) => Promise<void>;
10
+ //# sourceMappingURL=tenant-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-session.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE/F;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAarG"}