velocious 1.0.482 → 1.0.484

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 (33) hide show
  1. package/README.md +2 -0
  2. package/build/cli/commands/db/tenants/drop.js +34 -0
  3. package/build/cli/tenant-database-command-helper.js +8 -85
  4. package/build/database/tenants/data-copier.js +269 -0
  5. package/build/database/tenants/tenant-table-plan.js +20 -0
  6. package/build/src/cli/commands/db/tenants/drop.d.ts +13 -0
  7. package/build/src/cli/commands/db/tenants/drop.d.ts.map +1 -0
  8. package/build/src/cli/commands/db/tenants/drop.js +31 -0
  9. package/build/src/cli/tenant-database-command-helper.d.ts +0 -20
  10. package/build/src/cli/tenant-database-command-helper.d.ts.map +1 -1
  11. package/build/src/cli/tenant-database-command-helper.js +8 -73
  12. package/build/src/database/tenants/data-copier.d.ts +125 -0
  13. package/build/src/database/tenants/data-copier.d.ts.map +1 -0
  14. package/build/src/database/tenants/data-copier.js +232 -0
  15. package/build/src/database/tenants/tenant-table-plan.d.ts +30 -0
  16. package/build/src/database/tenants/tenant-table-plan.d.ts.map +1 -0
  17. package/build/src/database/tenants/tenant-table-plan.js +3 -0
  18. package/build/src/tenants/tenant-iterator.d.ts +52 -0
  19. package/build/src/tenants/tenant-iterator.d.ts.map +1 -0
  20. package/build/src/tenants/tenant-iterator.js +97 -0
  21. package/build/src/tenants/tenant.d.ts +61 -0
  22. package/build/src/tenants/tenant.d.ts.map +1 -0
  23. package/build/src/tenants/tenant.js +93 -0
  24. package/build/tenants/tenant-iterator.js +111 -0
  25. package/build/tenants/tenant.js +104 -0
  26. package/build/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
  28. package/src/cli/commands/db/tenants/drop.js +34 -0
  29. package/src/cli/tenant-database-command-helper.js +8 -85
  30. package/src/database/tenants/data-copier.js +269 -0
  31. package/src/database/tenants/tenant-table-plan.js +20 -0
  32. package/src/tenants/tenant-iterator.js +111 -0
  33. package/src/tenants/tenant.js +104 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Runs a callback once within each tenant's context for a tenant database identifier,
3
+ * optionally several tenants at a time. Each tenant is entered with `runWithTenant`, and a
4
+ * tenant whose database identifier is inactive throws rather than running the callback
5
+ * against the wrong connection. When iterating in parallel the per-tenant failures are
6
+ * collected and rethrown together as an `AggregateError` so one bad tenant does not hide the
7
+ * others. This is the iteration engine shared by the `db:tenants:*` CLI commands and the
8
+ * runtime {@link Tenant} façade; the caller is responsible for producing the tenant list.
9
+ */
10
+ export default class TenantIterator {
11
+ /**
12
+ * Builds a human-readable label for a tenant for use in error messages.
13
+ * @param {?} tenant
14
+ * @returns {string}
15
+ */
16
+ static tenantLabel(tenant: unknown): string;
17
+ /**
18
+ * Creates an iterator bound to a configuration and tenant database identifier.
19
+ * @param {{configuration: import("../configuration.js").default, identifier: string, parallelCount?: number}} args
20
+ */
21
+ constructor({ configuration, identifier, parallelCount }: {
22
+ configuration: import("../configuration.js").default;
23
+ identifier: string;
24
+ parallelCount?: number;
25
+ });
26
+ configuration: import("../configuration.js").default;
27
+ identifier: string;
28
+ parallelCount: number;
29
+ /**
30
+ * Runs `callback` within each tenant's context and returns how many tenants were processed.
31
+ * @param {Array<?>} tenants
32
+ * @param {function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>} callback
33
+ * @returns {Promise<number>}
34
+ */
35
+ run(tenants: Array<unknown>, callback: (arg0: {
36
+ databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType;
37
+ tenant: unknown;
38
+ }) => Promise<void>): Promise<number>;
39
+ /**
40
+ * Enters one tenant's context and runs the callback, asserting the database is active first.
41
+ * @param {{callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, tenant: ?}} args
42
+ * @returns {Promise<void>}
43
+ */
44
+ runTenantCallback({ callback, tenant }: {
45
+ callback: (arg0: {
46
+ databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType;
47
+ tenant: unknown;
48
+ }) => Promise<void>;
49
+ tenant: unknown;
50
+ }): Promise<void>;
51
+ }
52
+ //# sourceMappingURL=tenant-iterator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-iterator.d.ts","sourceRoot":"","sources":["../../../src/tenants/tenant-iterator.js"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH;IAmFE;;;;OAIG;IACH,2BAHW,OAAC,GACC,MAAM,CAYlB;IAjGD;;;OAGG;IACH,0DAFW;QAAC,aAAa,EAAE,OAAO,qBAAqB,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAC,EAM5G;IAHC,qDAAkC;IAClC,mBAA4B;IAC5B,sBAAkC;IAGpC;;;;;OAKG;IACH,aAJW,KAAK,CAAC,OAAC,CAAC,YACR,CAAS,IAAiG,EAAjG;QAAC,qBAAqB,EAAE,OAAO,2BAA2B,EAAE,yBAAyB,CAAC;QAAC,MAAM,EAAE,OAAC,CAAA;KAAC,KAAI,OAAO,CAAC,IAAI,CAAC,GACzH,OAAO,CAAC,MAAM,CAAC,CAgD3B;IAED;;;;OAIG;IACH,wCAHW;QAAC,QAAQ,EAAE,CAAS,IAAiG,EAAjG;YAAC,qBAAqB,EAAE,OAAO,2BAA2B,EAAE,yBAAyB,CAAC;YAAC,MAAM,EAAE,OAAC,CAAA;SAAC,KAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,MAAM,EAAE,OAAC,CAAA;KAAC,GAChJ,OAAO,CAAC,IAAI,CAAC,CAazB;CAkBF"}
@@ -0,0 +1,97 @@
1
+ // @ts-check
2
+ /**
3
+ * Runs a callback once within each tenant's context for a tenant database identifier,
4
+ * optionally several tenants at a time. Each tenant is entered with `runWithTenant`, and a
5
+ * tenant whose database identifier is inactive throws rather than running the callback
6
+ * against the wrong connection. When iterating in parallel the per-tenant failures are
7
+ * collected and rethrown together as an `AggregateError` so one bad tenant does not hide the
8
+ * others. This is the iteration engine shared by the `db:tenants:*` CLI commands and the
9
+ * runtime {@link Tenant} façade; the caller is responsible for producing the tenant list.
10
+ */
11
+ export default class TenantIterator {
12
+ /**
13
+ * Creates an iterator bound to a configuration and tenant database identifier.
14
+ * @param {{configuration: import("../configuration.js").default, identifier: string, parallelCount?: number}} args
15
+ */
16
+ constructor({ configuration, identifier, parallelCount = 1 }) {
17
+ this.configuration = configuration;
18
+ this.identifier = identifier;
19
+ this.parallelCount = parallelCount;
20
+ }
21
+ /**
22
+ * Runs `callback` within each tenant's context and returns how many tenants were processed.
23
+ * @param {Array<?>} tenants
24
+ * @param {function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>} callback
25
+ * @returns {Promise<number>}
26
+ */
27
+ async run(tenants, callback) {
28
+ if (this.parallelCount <= 1) {
29
+ for (const tenant of tenants) {
30
+ await this.runTenantCallback({ callback, tenant });
31
+ }
32
+ return tenants.length;
33
+ }
34
+ /** @type {Array<{error: Error, tenant: ?}>} */
35
+ const failures = [];
36
+ const workers = [];
37
+ let tenantIndex = 0;
38
+ const workerCount = Math.min(this.parallelCount, tenants.length);
39
+ for (let workerIndex = 0; workerIndex < workerCount; workerIndex++) {
40
+ workers.push((async () => {
41
+ while (tenantIndex < tenants.length) {
42
+ const tenant = tenants[tenantIndex];
43
+ tenantIndex++;
44
+ try {
45
+ await this.runTenantCallback({ callback, tenant });
46
+ }
47
+ catch (error) {
48
+ failures.push({
49
+ error: error instanceof Error ? error : new Error(String(error)),
50
+ tenant
51
+ });
52
+ }
53
+ }
54
+ })());
55
+ }
56
+ await Promise.all(workers);
57
+ if (failures.length > 0) {
58
+ const failedTenantLabels = failures.map((failure) => TenantIterator.tenantLabel(failure.tenant)).join(", ");
59
+ throw new AggregateError(failures.map((failure) => failure.error), `Failed tenant database command for tenant(s): ${failedTenantLabels}`);
60
+ }
61
+ return tenants.length;
62
+ }
63
+ /**
64
+ * Enters one tenant's context and runs the callback, asserting the database is active first.
65
+ * @param {{callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, tenant: ?}} args
66
+ * @returns {Promise<void>}
67
+ */
68
+ async runTenantCallback({ callback, tenant }) {
69
+ await this.configuration.runWithTenant(tenant, async () => {
70
+ if (!this.configuration.isDatabaseIdentifierActive(this.identifier)) {
71
+ throw new Error(`Tenant database identifier ${this.identifier} is inactive for tenant: ${TenantIterator.tenantLabel(tenant)}`);
72
+ }
73
+ await callback({
74
+ databaseConfiguration: this.configuration.resolveDatabaseConfiguration(this.identifier),
75
+ tenant
76
+ });
77
+ });
78
+ }
79
+ /**
80
+ * Builds a human-readable label for a tenant for use in error messages.
81
+ * @param {?} tenant
82
+ * @returns {string}
83
+ */
84
+ static tenantLabel(tenant) {
85
+ if (tenant && typeof tenant === "object") {
86
+ const tenantObject = /** @type {{id?: ?, name?: ?, slug?: ?}} */ (tenant);
87
+ if (tenantObject.slug)
88
+ return String(tenantObject.slug);
89
+ if (tenantObject.name)
90
+ return String(tenantObject.name);
91
+ if (tenantObject.id)
92
+ return String(tenantObject.id);
93
+ }
94
+ return JSON.stringify(tenant);
95
+ }
96
+ }
97
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVuYW50LWl0ZXJhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3RlbmFudHMvdGVuYW50LWl0ZXJhdG9yLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWjs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxPQUFPLE9BQU8sY0FBYztJQUNqQzs7O09BR0c7SUFDSCxZQUFZLEVBQUMsYUFBYSxFQUFFLFVBQVUsRUFBRSxhQUFhLEdBQUcsQ0FBQyxFQUFDO1FBQ3hELElBQUksQ0FBQyxhQUFhLEdBQUcsYUFBYSxDQUFBO1FBQ2xDLElBQUksQ0FBQyxVQUFVLEdBQUcsVUFBVSxDQUFBO1FBQzVCLElBQUksQ0FBQyxhQUFhLEdBQUcsYUFBYSxDQUFBO0lBQ3BDLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNILEtBQUssQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLFFBQVE7UUFDekIsSUFBSSxJQUFJLENBQUMsYUFBYSxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQzVCLEtBQUssTUFBTSxNQUFNLElBQUksT0FBTyxFQUFFLENBQUM7Z0JBQzdCLE1BQU0sSUFBSSxDQUFDLGlCQUFpQixDQUFDLEVBQUMsUUFBUSxFQUFFLE1BQU0sRUFBQyxDQUFDLENBQUE7WUFDbEQsQ0FBQztZQUVELE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FBQTtRQUN2QixDQUFDO1FBRUQsK0NBQStDO1FBQy9DLE1BQU0sUUFBUSxHQUFHLEVBQUUsQ0FBQTtRQUNuQixNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUE7UUFDbEIsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFBO1FBQ25CLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLGFBQWEsRUFBRSxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUE7UUFFaEUsS0FBSyxJQUFJLFdBQVcsR0FBRyxDQUFDLEVBQUUsV0FBVyxHQUFHLFdBQVcsRUFBRSxXQUFXLEVBQUUsRUFB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QU0sQ0FBQyxDQUFBO0lBQy9CLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8vIEB0cy1jaGVja1xuXG4vKipcbiAqIFJ1bnMgYSBjYWxsYmFjayBvbmNlIHdpdGhpbiBlYWNoIHRlbmFudCdzIGNvbnRleHQgZm9yIGEgdGVuYW50IGRhdGFiYXNlIGlkZW50aWZpZXIsXG4gKiBvcHRpb25hbGx5IHNldmVyYWwgdGVuYW50cyBhdCBhIHRpbWUuIEVhY2ggdGVuYW50IGlzIGVudGVyZWQgd2l0aCBgcnVuV2l0aFRlbmFudGAsIGFuZCBhXG4gKiB0ZW5hbnQgd2hvc2UgZGF0YWJhc2UgaWRlbnRpZmllciBpcyBpbmFjdGl2ZSB0aHJvd3MgcmF0aGVyIHRoYW4gcnVubmluZyB0aGUgY2FsbGJhY2tcbiAqIGFnYWluc3QgdGhlIHdyb25nIGNvbm5lY3Rpb24uIFdoZW4gaXRlcmF0aW5nIGluIHBhcmFsbGVsIHRoZSBwZXItdGVuYW50IGZhaWx1cmVzIGFyZVxuICogY29sbGVjdGVkIGFuZCByZXRocm93biB0b2dldGhlciBhcyBhbiBgQWdncmVnYXRlRXJyb3JgIHNvIG9uZSBiYWQgdGVuYW50IGRvZXMgbm90IGhpZGUgdGhlXG4gKiBvdGhlcnMuIFRoaXMgaXMgdGhlIGl0ZXJhdGlvbiBlbmdpbmUgc2hhcmVkIGJ5IHRoZSBgZGI6dGVuYW50czoqYCBDTEkgY29tbWFuZHMgYW5kIHRoZVxuICogcnVudGltZSB7QGxpbmsgVGVuYW50fSBmYcOnYWRlOyB0aGUgY2FsbGVyIGlzIHJlc3BvbnNpYmxlIGZvciBwcm9kdWNpbmcgdGhlIHRlbmFudCBsaXN0LlxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBUZW5hbnRJdGVyYXRvciB7XG4gIC8qKlxuICAgKiBDcmVhdGVzIGFuIGl0ZXJhdG9yIGJvdW5kIHRvIGEgY29uZmlndXJhdGlvbiBhbmQgdGVuYW50IGRhdGFiYXNlIGlkZW50aWZpZXIuXG4gICAqIEBwYXJhbSB7e2NvbmZpZ3VyYXRpb246IGltcG9ydChcIi4uL2NvbmZpZ3VyYXRpb24uanNcIikuZGVmYXVsdCwgaWRlbnRpZmllcjogc3RyaW5nLCBwYXJhbGxlbENvdW50PzogbnVtYmVyfX0gYXJnc1xuICAgKi9cbiAgY29uc3RydWN0b3Ioe2NvbmZpZ3VyYXRpb24sIGlkZW50aWZpZXIsIHBhcmFsbGVsQ291bnQgPSAxfSkge1xuICAgIHRoaXMuY29uZmlndXJhdGlvbiA9IGNvbmZpZ3VyYXRpb25cbiAgICB0aGlzLmlkZW50aWZpZXIgPSBpZGVudGlmaWVyXG4gICAgdGhpcy5wYXJhbGxlbENvdW50ID0gcGFyYWxsZWxDb3VudFxuICB9XG5cbiAgLyoqXG4gICAqIFJ1bnMgYGNhbGxiYWNrYCB3aXRoaW4gZWFjaCB0ZW5hbnQncyBjb250ZXh0IGFuZCByZXR1cm5zIGhvdyBtYW55IHRlbmFudHMgd2VyZSBwcm9jZXNzZWQuXG4gICAqIEBwYXJhbSB7QXJyYXk8Pz59IHRlbmFudHNcbiAgICogQHBhcmFtIHtmdW5jdGlvbih7ZGF0YWJhc2VDb25maWd1cmF0aW9uOiBpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLkRhdGFiYXNlQ29uZmlndXJhdGlvblR5cGUsIHRlbmFudDogP30pIDogUHJvbWlzZTx2b2lkPn0gY2FsbGJhY2tcbiAgICogQHJldHVybnMge1Byb21pc2U8bnVtYmVyPn1cbiAgICovXG4gIGFzeW5jIHJ1bih0ZW5hbnRzLCBjYWxsYmFjaykge1xuICAgIGlmICh0aGlzLnBhcmFsbGVsQ291bnQgPD0gMSkge1xuICAgICAgZm9yIChjb25zdCB0ZW5hbnQgb2YgdGVuYW50cykge1xuICAgICAgICBhd2FpdCB0aGlzLnJ1blRlbmFudENhbGxiYWNrKHtjYWxsYmFjaywgdGVuYW50fSlcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHRlbmFudHMubGVuZ3RoXG4gICAgfVxuXG4gICAgLyoqIEB0eXBlIHtBcnJheTx7ZXJyb3I6IEVycm9yLCB0ZW5hbnQ6ID99Pn0gKi9cbiAgICBjb25zdCBmYWlsdXJlcyA9IFtdXG4gICAgY29uc3Qgd29ya2VycyA9IFtdXG4gICAgbGV0IHRlbmFudEluZGV4ID0gMFxuICAgIGNvbnN0IHdvcmtlckNvdW50ID0gTWF0aC5taW4odGhpcy5wYXJhbGxlbENvdW50LCB0ZW5hbnRzLmxlbmd0aClcblxuICAgIGZvciAobGV0IHdvcmtlckluZGV4ID0gMDsgd29ya2VySW5kZXggPCB3b3JrZXJDb3VudDsgd29ya2VySW5kZXgrKykge1xuICAgICAgd29ya2Vycy5wdXNoKChhc3luYyAoKSA9PiB7XG4gICAgICAgIHdoaWxlICh0ZW5hbnRJbmRleCA8IHRlbmFudHMubGVuZ3RoKSB7XG4gICAgICAgICAgY29uc3QgdGVuYW50ID0gdGVuYW50c1t0ZW5hbnRJbmRleF1cblxuICAgICAgICAgIHRlbmFudEluZGV4KytcblxuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICBhd2FpdCB0aGlzLnJ1blRlbmFudENhbGxiYWNrKHtjYWxsYmFjaywgdGVuYW50fSlcbiAgICAgICAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgICAgICAgZmFpbHVyZXMucHVzaCh7XG4gICAgICAgICAgICAgIGVycm9yOiBlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IgOiBuZXcgRXJyb3IoU3RyaW5nKGVycm9yKSksXG4gICAgICAgICAgICAgIHRlbmFudFxuICAgICAgICAgICAgfSlcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH0pKCkpXG4gICAgfVxuXG4gICAgYXdhaXQgUHJvbWlzZS5hbGwod29ya2VycylcblxuICAgIGlmIChmYWlsdXJlcy5sZW5ndGggPiAwKSB7XG4gICAgICBjb25zdCBmYWlsZWRUZW5hbnRMYWJlbHMgPSBmYWlsdXJlcy5tYXAoKGZhaWx1cmUpID0+IFRlbmFudEl0ZXJhdG9yLnRlbmFudExhYmVsKGZhaWx1cmUudGVuYW50KSkuam9pbihcIiwgXCIpXG5cbiAgICAgIHRocm93IG5ldyBBZ2dyZWdhdGVFcnJvcihcbiAgICAgICAgZmFpbHVyZXMubWFwKChmYWlsdXJlKSA9PiBmYWlsdXJlLmVycm9yKSxcbiAgICAgICAgYEZhaWxlZCB0ZW5hbnQgZGF0YWJhc2UgY29tbWFuZCBmb3IgdGVuYW50KHMpOiAke2ZhaWxlZFRlbmFudExhYmVsc31gXG4gICAgICApXG4gICAgfVxuXG4gICAgcmV0dXJuIHRlbmFudHMubGVuZ3RoXG4gIH1cblxuICAvKipcbiAgICogRW50ZXJzIG9uZSB0ZW5hbnQncyBjb250ZXh0IGFuZCBydW5zIHRoZSBjYWxsYmFjaywgYXNzZXJ0aW5nIHRoZSBkYXRhYmFzZSBpcyBhY3RpdmUgZmlyc3QuXG4gICAqIEBwYXJhbSB7e2NhbGxiYWNrOiBmdW5jdGlvbih7ZGF0YWJhc2VDb25maWd1cmF0aW9uOiBpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLkRhdGFiYXNlQ29uZmlndXJhdGlvblR5cGUsIHRlbmFudDogP30pIDogUHJvbWlzZTx2b2lkPiwgdGVuYW50OiA/fX0gYXJnc1xuICAgKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAgICovXG4gIGFzeW5jIHJ1blRlbmFudENhbGxiYWNrKHtjYWxsYmFjaywgdGVuYW50fSkge1xuICAgIGF3YWl0IHRoaXMuY29uZmlndXJhdGlvbi5ydW5XaXRoVGVuYW50KHRlbmFudCwgYXN5bmMgKCkgPT4ge1xuICAgICAgaWYgKCF0aGlzLmNvbmZpZ3VyYXRpb24uaXNEYXRhYmFzZUlkZW50aWZpZXJBY3RpdmUodGhpcy5pZGVudGlmaWVyKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFRlbmFudCBkYXRhYmFzZSBpZGVudGlmaWVyICR7dGhpcy5pZGVudGlmaWVyfSBpcyBpbmFjdGl2ZSBmb3IgdGVuYW50OiAke1RlbmFudEl0ZXJhdG9yLnRlbmFudExhYmVsKHRlbmFudCl9YClcbiAgICAgIH1cblxuICAgICAgYXdhaXQgY2FsbGJhY2soe1xuICAgICAgICBkYXRhYmFzZUNvbmZpZ3VyYXRpb246IHRoaXMuY29uZmlndXJhdGlvbi5yZXNvbHZlRGF0YWJhc2VDb25maWd1cmF0aW9uKHRoaXMuaWRlbnRpZmllciksXG4gICAgICAgIHRlbmFudFxuICAgICAgfSlcbiAgICB9KVxuICB9XG5cbiAgLyoqXG4gICAqIEJ1aWxkcyBhIGh1bWFuLXJlYWRhYmxlIGxhYmVsIGZvciBhIHRlbmFudCBmb3IgdXNlIGluIGVycm9yIG1lc3NhZ2VzLlxuICAgKiBAcGFyYW0gez99IHRlbmFudFxuICAgKiBAcmV0dXJucyB7c3RyaW5nfVxuICAgKi9cbiAgc3RhdGljIHRlbmFudExhYmVsKHRlbmFudCkge1xuICAgIGlmICh0ZW5hbnQgJiYgdHlwZW9mIHRlbmFudCA9PT0gXCJvYmplY3RcIikge1xuICAgICAgY29uc3QgdGVuYW50T2JqZWN0ID0gLyoqIEB0eXBlIHt7aWQ/OiA/LCBuYW1lPzogPywgc2x1Zz86ID99fSAqLyAodGVuYW50KVxuXG4gICAgICBpZiAodGVuYW50T2JqZWN0LnNsdWcpIHJldHVybiBTdHJpbmcodGVuYW50T2JqZWN0LnNsdWcpXG4gICAgICBpZiAodGVuYW50T2JqZWN0Lm5hbWUpIHJldHVybiBTdHJpbmcodGVuYW50T2JqZWN0Lm5hbWUpXG4gICAgICBpZiAodGVuYW50T2JqZWN0LmlkKSByZXR1cm4gU3RyaW5nKHRlbmFudE9iamVjdC5pZClcbiAgICB9XG5cbiAgICByZXR1cm4gSlNPTi5zdHJpbmdpZnkodGVuYW50KVxuICB9XG59XG4iXX0=
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Apartment-style runtime façade for multi-tenant apps. A "tenant" is whatever descriptor
3
+ * object the app's `tenantDatabaseResolver` understands (an account, a project, …); this
4
+ * class is the single discoverable home for switching into a tenant's context, reading the
5
+ * current one, iterating every tenant of a database identifier, and dropping a tenant's
6
+ * database. Switching delegates to {@link Current} (which owns the async-context tenant
7
+ * state) and additionally runs the callback inside `ensureConnections`, so entering a tenant
8
+ * makes its database immediately queryable — the apartment-style "switch" semantics — without
9
+ * the caller establishing connections itself; iteration and drop drive the app's tenant
10
+ * database provider hooks.
11
+ */
12
+ export default class Tenant {
13
+ /**
14
+ * Runs `callback` with `tenant` as the current tenant, restoring the previous tenant after.
15
+ * The callback runs inside `ensureConnections`, so every database identifier the tenant
16
+ * activates (the global database plus the tenant's database) has a checked-out connection
17
+ * available for the callback's duration: switching into a tenant makes it queryable without
18
+ * the caller wiring up connections. Already-checked-out connections are reused, so nesting
19
+ * `Tenant.with` calls does not open redundant connections. The callback receives the active
20
+ * connections keyed by identifier, the same as `ensureConnections`.
21
+ * @param {object} tenant Descriptor understood by the app's tenantDatabaseResolver.
22
+ * @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<?>} callback
23
+ * @returns {Promise<?>}
24
+ */
25
+ static with(tenant: object, callback: (connections: Record<string, import("../database/drivers/base.js").default>) => Promise<unknown>): Promise<unknown>;
26
+ /**
27
+ * The current tenant descriptor, or undefined when running outside any tenant context.
28
+ * @returns {Record<string, unknown> | undefined}
29
+ */
30
+ static current(): Record<string, unknown> | undefined;
31
+ /**
32
+ * Lists the tenants for a database identifier through the provider and runs `callback`
33
+ * within each tenant's context, optionally filtered and several at a time. Like
34
+ * {@link Tenant.with}, the callback runs inside `ensureConnections` so each tenant's
35
+ * database is queryable without the caller wiring up connections. Returns how many tenants
36
+ * the callback ran for (after filtering).
37
+ * @param {{identifier: string, callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, parallel?: number, filter?: (tenant: ?) => boolean, configuration?: import("../configuration.js").default}} args
38
+ * @returns {Promise<number>}
39
+ */
40
+ static each({ identifier, callback, parallel, filter, configuration }: {
41
+ identifier: string;
42
+ callback: (arg0: {
43
+ databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType;
44
+ tenant: unknown;
45
+ }) => Promise<void>;
46
+ parallel?: number;
47
+ filter?: (tenant: unknown) => boolean;
48
+ configuration?: import("../configuration.js").default;
49
+ }): Promise<number>;
50
+ /**
51
+ * Drops one tenant's database/schema through the provider's `dropDatabase` hook.
52
+ * @param {{identifier: string, tenant: object, configuration?: import("../configuration.js").default}} args
53
+ * @returns {Promise<void>}
54
+ */
55
+ static drop({ identifier, tenant, configuration }: {
56
+ identifier: string;
57
+ tenant: object;
58
+ configuration?: import("../configuration.js").default;
59
+ }): Promise<void>;
60
+ }
61
+ //# sourceMappingURL=tenant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../../src/tenants/tenant.js"],"names":[],"mappings":"AAKA;;;;;;;;;;GAUG;AACH;IACE;;;;;;;;;;;OAWG;IACH,oBAJW,MAAM,YACN,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,6BAA6B,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAC,CAAC,GACxF,OAAO,CAAC,OAAC,CAAC,CAMtB;IAED;;;OAGG;IACH,kBAFa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAI/C;IAED;;;;;;;;OAQG;IACH,uEAHW;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,CAAS,IAAiG,EAAjG;YAAC,qBAAqB,EAAE,OAAO,2BAA2B,EAAE,yBAAyB,CAAC;YAAC,MAAM,EAAE,OAAC,CAAA;SAAC,KAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAC,KAAK,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,qBAAqB,EAAE,OAAO,CAAA;KAAC,GACpQ,OAAO,CAAC,MAAM,CAAC,CAsB3B;IAED;;;;OAIG;IACH,mDAHW;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,qBAAqB,EAAE,OAAO,CAAA;KAAC,GACzF,OAAO,CAAC,IAAI,CAAC,CAyBzB;CACF"}
@@ -0,0 +1,93 @@
1
+ // @ts-check
2
+ import Current from "../current.js";
3
+ import TenantIterator from "./tenant-iterator.js";
4
+ /**
5
+ * Apartment-style runtime façade for multi-tenant apps. A "tenant" is whatever descriptor
6
+ * object the app's `tenantDatabaseResolver` understands (an account, a project, …); this
7
+ * class is the single discoverable home for switching into a tenant's context, reading the
8
+ * current one, iterating every tenant of a database identifier, and dropping a tenant's
9
+ * database. Switching delegates to {@link Current} (which owns the async-context tenant
10
+ * state) and additionally runs the callback inside `ensureConnections`, so entering a tenant
11
+ * makes its database immediately queryable — the apartment-style "switch" semantics — without
12
+ * the caller establishing connections itself; iteration and drop drive the app's tenant
13
+ * database provider hooks.
14
+ */
15
+ export default class Tenant {
16
+ /**
17
+ * Runs `callback` with `tenant` as the current tenant, restoring the previous tenant after.
18
+ * The callback runs inside `ensureConnections`, so every database identifier the tenant
19
+ * activates (the global database plus the tenant's database) has a checked-out connection
20
+ * available for the callback's duration: switching into a tenant makes it queryable without
21
+ * the caller wiring up connections. Already-checked-out connections are reused, so nesting
22
+ * `Tenant.with` calls does not open redundant connections. The callback receives the active
23
+ * connections keyed by identifier, the same as `ensureConnections`.
24
+ * @param {object} tenant Descriptor understood by the app's tenantDatabaseResolver.
25
+ * @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<?>} callback
26
+ * @returns {Promise<?>}
27
+ */
28
+ static async with(tenant, callback) {
29
+ const configuration = Current.configuration();
30
+ return await Current.withTenant(tenant, async () => await configuration.ensureConnections(callback));
31
+ }
32
+ /**
33
+ * The current tenant descriptor, or undefined when running outside any tenant context.
34
+ * @returns {Record<string, unknown> | undefined}
35
+ */
36
+ static current() {
37
+ return Current.tenant();
38
+ }
39
+ /**
40
+ * Lists the tenants for a database identifier through the provider and runs `callback`
41
+ * within each tenant's context, optionally filtered and several at a time. Like
42
+ * {@link Tenant.with}, the callback runs inside `ensureConnections` so each tenant's
43
+ * database is queryable without the caller wiring up connections. Returns how many tenants
44
+ * the callback ran for (after filtering).
45
+ * @param {{identifier: string, callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, parallel?: number, filter?: (tenant: ?) => boolean, configuration?: import("../configuration.js").default}} args
46
+ * @returns {Promise<number>}
47
+ */
48
+ static async each({ identifier, callback, parallel = 1, filter, configuration = Current.configuration() }) {
49
+ const provider = configuration.getTenantDatabaseProvider(identifier);
50
+ const listedTenants = await configuration.ensureConnections({ name: `Tenant.each: ${identifier}` }, async () => {
51
+ return await provider.listTenants({ configuration, identifier });
52
+ });
53
+ if (!Array.isArray(listedTenants)) {
54
+ throw new Error(`Tenant database provider for ${identifier} must return an array from listTenants`);
55
+ }
56
+ const tenants = filter ? listedTenants.filter(filter) : listedTenants;
57
+ const iterator = new TenantIterator({ configuration, identifier, parallelCount: parallel });
58
+ // Run each tenant's callback inside ensureConnections so the iterator stays
59
+ // connection-agnostic (the db:tenants:* CLI commands share TenantIterator and must run
60
+ // their callbacks, such as create, before the tenant database exists) while runtime
61
+ // iteration here gets the tenant's connections established the same way Tenant.with does.
62
+ return await iterator.run(tenants, async (callbackArgs) => {
63
+ await configuration.ensureConnections({ name: `Tenant.each: ${identifier}` }, async () => await callback(callbackArgs));
64
+ });
65
+ }
66
+ /**
67
+ * Drops one tenant's database/schema through the provider's `dropDatabase` hook.
68
+ * @param {{identifier: string, tenant: object, configuration?: import("../configuration.js").default}} args
69
+ * @returns {Promise<void>}
70
+ */
71
+ static async drop({ identifier, tenant, configuration = Current.configuration() }) {
72
+ const provider = configuration.getTenantDatabaseProvider(identifier);
73
+ if (typeof provider.dropDatabase !== "function") {
74
+ throw new Error(`Tenant database provider for ${identifier} must define dropDatabase to drop a tenant`);
75
+ }
76
+ await configuration.runWithTenant(tenant, async () => {
77
+ // Guard against an unresolved tenant. resolveDatabaseConfiguration falls back to the
78
+ // base (template/default) tenant database when the resolver returns nothing for this
79
+ // descriptor, so without this check a provider that drops by databaseConfiguration.name
80
+ // would drop the template database instead of rejecting the bad tenant.
81
+ if (!configuration.isDatabaseIdentifierActive(identifier)) {
82
+ throw new Error(`Tenant database identifier ${identifier} is inactive for tenant: ${TenantIterator.tenantLabel(tenant)}`);
83
+ }
84
+ await provider.dropDatabase?.({
85
+ configuration,
86
+ databaseConfiguration: configuration.resolveDatabaseConfiguration(identifier),
87
+ identifier,
88
+ tenant
89
+ });
90
+ });
91
+ }
92
+ }
93
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVuYW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3RlbmFudHMvdGVuYW50LmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWixPQUFPLE9BQU8sTUFBTSxlQUFlLENBQUE7QUFDbkMsT0FBTyxjQUFjLE1BQU0sc0JBQXNCLENBQUE7QUFFakQ7Ozs7Ozs7Ozs7R0FVRztBQUNILE1BQU0sQ0FBQyxPQUFPLE9BQU8sTUFBTTtJQUN6Qjs7Ozs7Ozs7Ozs7T0FXRztJQUNILE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxRQUFRO1FBQ2hDLE1BQU0sYUFBYSxHQUFHLE9BQU8sQ0FBQyxhQUFhLEVBQUUsQ0FBQTtRQUU3QyxPQUFPLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxNQUFNLEVBQUUsS0FBSyxJQUFJLEVBQUUsQ0FBQyxNQUFNLGFBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFBO0lBQ3RHLENBQUM7SUFFRDs7O09BR0c7SUFDSCxNQUFNLENBQUMsT0FBTztRQUNaLE9BQU8sT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFBO0lBQ3pCLENBQUM7SUFFRDs7Ozs7Ozs7T0FRRztJQUNILE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUMsVUFBVSxFQUFFLFFBQVEsRUFBRSxRQUFRLEdBQUcsQ0FBQyxFQUFFLE1BQU0sRUFBRSxhQUFhLEdBQUcsT0FBTyxDQUFDLGFBQWEsRUFBRSxFQUFDO1FBQ3JHLE1BQU0sUUFBUSxHQUFHLGFBQWEsQ0FBQyx5QkFBeUIsQ0FBQyxVQUFVLENBQUMsQ0FBQTtRQUNwRSxNQUFNLGFBQWEsR0FBRyxNQUFNLGFBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxFQUFDLElBQUksRUFBRSxnQkFBZ0IsVUFBVSxFQUFFLEVBQUMsRUFBRSxLQUFLLElBQUksRUFBRTtZQUMzRyxPQUFPLE1BQU0sUUFBUSxDQUFDLFdBQVcsQ0FBQyxFQUFDLGFBQWEsRUFBRSxVQUFVLEVBQUMsQ0FBQyxDQUFBO1FBQ2hFLENBQUMsQ0FBQyxDQUFBO1FBRUYsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQztZQUNsQyxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxVQUFVLHdDQUF3QyxDQUFDLENBQUE7UUFDckcsQ0FBQztRQUVELE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxDQUFDLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsYUFBYSxDQUFBO1FBQ3JFLE1BQU0sUUFBUSxHQUFHLElBQUksY0FBYyxDQUFDLEVBQUMsYUFBYSxFQUFFLFVBQVUsRUFBRSxhQUFhLEVBQUUsUUFBUSxFQUFDLENBQUMsQ0FBQTtRQUV6Riw0RUFBNEU7UUFDNUUsdUZBQXVGO1FBQ3ZGLG9GQUFvRjtRQUNwRiwwRkFBMEY7UUFDMUYsT0FBTyxNQUFNLFFBQVEsQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEtBQUssRUFBRSxZQUFZLEVBQUUsRUFBRTtZQUN4RCxNQUFNLGFBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxFQUFDLElBQUksRUFBRSxnQkFBZ0IsVUFBVSxFQUFFLEVBQUMsRUFBRSxLQUFLLElBQUksRUFBRSxDQUFDLE1BQU0sUUFBUSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUE7UUFDdkgsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNILE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUMsVUFBVSxFQUFFLE1BQU0sRUFBRSxhQUFhLEdBQUcsT0FBTyxDQUFDLGFBQWEsRUFBRSxFQUFDO1FBQzdFLE1BQU0sUUFBUSxHQUFHLGFBQWEsQ0FBQyx5QkFBeUIsQ0FBQyxVQUFVLENBQUMsQ0FBQTtRQUVwRSxJQUFJLE9BQU8sUUFBUSxDQUFDLFlBQVksS0FBSyxVQUFVLEVBQUUsQ0FBQztZQUNoRCxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxVQUFVLDRDQUE0QyxDQUFDLENBQUE7UUFDekcsQ0FBQztRQUVELE1BQU0sYUFBYSxDQUFDLGFBQWEsQ0FBQyxNQUFNLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDbkQscUZBQXFGO1lBQ3JGLHFGQUFxRjtZQUNyRix3RkFBd0Y7WUFDeEYsd0VBQXdFO1lBQ3hFLElBQUksQ0FBQyxhQUFhLENBQUMsMEJBQTBCLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQztnQkFDMUQsTUFBTSxJQUFJLEtBQUssQ0FBQyw4QkFBOEIsVUFBVSw0QkFBNEIsY0FBYyxDQUFDLFdBQVcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUE7WUFDM0gsQ0FBQztZQUVELE1BQU0sUUFBUSxDQUFDLFlBQVksRUFBRSxDQUFDO2dCQUM1QixhQUFhO2dCQUNiLHFCQUFxQixFQUFFLGFBQWEsQ0FBQyw0QkFBNEIsQ0FBQyxVQUFVLENBQUM7Z0JBQzdFLFVBQVU7Z0JBQ1YsTUFBTTthQUNQLENBQUMsQ0FBQTtRQUNKLENBQUMsQ0FBQyxDQUFBO0lBQ0osQ0FBQztDQUNGIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbmltcG9ydCBDdXJyZW50IGZyb20gXCIuLi9jdXJyZW50LmpzXCJcbmltcG9ydCBUZW5hbnRJdGVyYXRvciBmcm9tIFwiLi90ZW5hbnQtaXRlcmF0b3IuanNcIlxuXG4vKipcbiAqIEFwYXJ0bWVudC1zdHlsZSBydW50aW1lIGZhw6dhZGUgZm9yIG11bHRpLXRlbmFudCBhcHBzLiBBIFwidGVuYW50XCIgaXMgd2hhdGV2ZXIgZGVzY3JpcHRvclxuICogb2JqZWN0IHRoZSBhcHAncyBgdGVuYW50RGF0YWJhc2VSZXNvbHZlcmAgdW5kZXJzdGFuZHMgKGFuIGFjY291bnQsIGEgcHJvamVjdCwg4oCmKTsgdGhpc1xuICogY2xhc3MgaXMgdGhlIHNpbmdsZSBkaXNjb3ZlcmFibGUgaG9tZSBmb3Igc3dpdGNoaW5nIGludG8gYSB0ZW5hbnQncyBjb250ZXh0LCByZWFkaW5nIHRoZVxuICogY3VycmVudCBvbmUsIGl0ZXJhdGluZyBldmVyeSB0ZW5hbnQgb2YgYSBkYXRhYmFzZSBpZGVudGlmaWVyLCBhbmQgZHJvcHBpbmcgYSB0ZW5hbnQnc1xuICogZGF0YWJhc2UuIFN3aXRjaGluZyBkZWxlZ2F0ZXMgdG8ge0BsaW5rIEN1cnJlbnR9ICh3aGljaCBvd25zIHRoZSBhc3luYy1jb250ZXh0IHRlbmFudFxuICogc3RhdGUpIGFuZCBhZGRpdGlvbmFsbHkgcnVucyB0aGUgY2FsbGJhY2sgaW5zaWRlIGBlbnN1cmVDb25uZWN0aW9uc2AsIHNvIGVudGVyaW5nIGEgdGVuYW50XG4gKiBtYWtlcyBpdHMgZGF0YWJhc2UgaW1tZWRpYXRlbHkgcXVlcnlhYmxlIOKAlCB0aGUgYXBhcnRtZW50LXN0eWxlIFwic3dpdGNoXCIgc2VtYW50aWNzIOKAlCB3aXRob3V0XG4gKiB0aGUgY2FsbGVyIGVzdGFibGlzaGluZyBjb25uZWN0aW9ucyBpdHNlbGY7IGl0ZXJhdGlvbiBhbmQgZHJvcCBkcml2ZSB0aGUgYXBwJ3MgdGVuYW50XG4gKiBkYXRhYmFzZSBwcm92aWRlciBob29rcy5cbiAqL1xuZXhwb3J0IGRlZmF1bHQgY2xhc3MgVGVuYW50IHtcbiAgLyoqXG4gICAqIFJ1bnMgYGNhbGxiYWNrYCB3aXRoIGB0ZW5hbnRgIGFzIHRoZSBjdXJyZW50IHRlbmFudCwgcmVzdG9yaW5nIHRoZSBwcmV2aW91cyB0ZW5hbnQgYWZ0ZXIuXG4gICAqIFRoZSBjYWxsYmFjayBydW5zIGluc2lkZSBgZW5zdXJlQ29ubmVjdGlvbnNgLCBzbyBldmVyeSBkYXRhYmFzZSBpZGVudGlmaWVyIHRoZSB0ZW5hbnRcbiAgICogYWN0aXZhdGVzICh0aGUgZ2xvYmFsIGRhdGFiYXNlIHBsdXMgdGhlIHRlbmFudCdzIGRhdGFiYXNlKSBoYXMgYSBjaGVja2VkLW91dCBjb25uZWN0aW9uXG4gICAqIGF2YWlsYWJsZSBmb3IgdGhlIGNhbGxiYWNrJ3MgZHVyYXRpb246IHN3aXRjaGluZyBpbnRvIGEgdGVuYW50IG1ha2VzIGl0IHF1ZXJ5YWJsZSB3aXRob3V0XG4gICAqIHRoZSBjYWxsZXIgd2lyaW5nIHVwIGNvbm5lY3Rpb25zLiBBbHJlYWR5LWNoZWNrZWQtb3V0IGNvbm5lY3Rpb25zIGFyZSByZXVzZWQsIHNvIG5lc3RpbmdcbiAgICogYFRlbmFudC53aXRoYCBjYWxscyBkb2VzIG5vdCBvcGVuIHJlZHVuZGFudCBjb25uZWN0aW9ucy4gVGhlIGNhbGxiYWNrIHJlY2VpdmVzIHRoZSBhY3RpdmVcbiAgICogY29ubmVjdGlvbnMga2V5ZWQgYnkgaWRlbnRpZmllciwgdGhlIHNhbWUgYXMgYGVuc3VyZUNvbm5lY3Rpb25zYC5cbiAgICogQHBhcmFtIHtvYmplY3R9IHRlbmFudCBEZXNjcmlwdG9yIHVuZGVyc3Rvb2QgYnkgdGhlIGFwcCdzIHRlbmFudERhdGFiYXNlUmVzb2x2ZXIuXG4gICAqIEBwYXJhbSB7KGNvbm5lY3Rpb25zOiBSZWNvcmQ8c3RyaW5nLCBpbXBvcnQoXCIuLi9kYXRhYmFzZS9kcml2ZXJzL2Jhc2UuanNcIikuZGVmYXVsdD4pID0+IFByb21pc2U8Pz59IGNhbGxiYWNrXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPD8+fVxuICAgKi9cbiAgc3RhdGljIGFzeW5jIHdpdGgodGVuYW50LCBjYWxsYmFjaykge1xuICAgIGNvbnN0IGNvbmZpZ3VyYXRpb24gPSBDdXJyZW50LmNvbmZpZ3VyYXRpb24oKVxuXG4gICAgcmV0dXJuIGF3YWl0IEN1cnJlbnQud2l0aFRlbmFudCh0ZW5hbnQsIGFzeW5jICgpID0+IGF3YWl0IGNvbmZpZ3VyYXRpb24uZW5zdXJlQ29ubmVjdGlvbnMoY2FsbGJhY2spKVxuICB9XG5cbiAgLyoqXG4gICAqIFRoZSBjdXJyZW50IHRlbmFudCBkZXNjcmlwdG9yLCBvciB1bmRlZmluZWQgd2hlbiBydW5uaW5nIG91dHNpZGUgYW55IHRlbmFudCBjb250ZXh0LlxuICAgKiBAcmV0dXJucyB7UmVjb3JkPHN0cmluZywgdW5rbm93bj4gfCB1bmRlZmluZWR9XG4gICAqL1xuICBzdGF0aWMgY3VycmVudCgpIHtcbiAgICByZXR1cm4gQ3VycmVudC50ZW5hbnQoKVxuICB9XG5cbiAgLyoqXG4gICAqIExpc3RzIHRoZSB0ZW5hbnRzIGZvciBhIGRhdGFiYXNlIGlkZW50aWZpZXIgdGhyb3VnaCB0aGUgcHJvdmlkZXIgYW5kIHJ1bnMgYGNhbGxiYWNrYFxuICAgKiB3aXRoaW4gZWFjaCB0ZW5hbnQncyBjb250ZXh0LCBvcHRpb25hbGx5IGZpbHRlcmVkIGFuZCBzZXZlcmFsIGF0IGEgdGltZS4gTGlrZVxuICAgKiB7QGxpbmsgVGVuYW50LndpdGh9LCB0aGUgY2FsbGJhY2sgcnVucyBpbnNpZGUgYGVuc3VyZUNvbm5lY3Rpb25zYCBzbyBlYWNoIHRlbmFudCdzXG4gICAqIGRhdGFiYXNlIGlzIHF1ZXJ5YWJsZSB3aXRob3V0IHRoZSBjYWxsZXIgd2lyaW5nIHVwIGNvbm5lY3Rpb25zLiBSZXR1cm5zIGhvdyBtYW55IHRlbmFudHNcbiAgICogdGhlIGNhbGxiYWNrIHJhbiBmb3IgKGFmdGVyIGZpbHRlcmluZykuXG4gICAqIEBwYXJhbSB7e2lkZW50aWZpZXI6IHN0cmluZywgY2FsbGJhY2s6IGZ1bmN0aW9uKHtkYXRhYmFzZUNvbmZpZ3VyYXRpb246IGltcG9ydChcIi4uL2NvbmZpZ3VyYXRpb24tdHlwZXMuanNcIikuRGF0YWJhc2VDb25maWd1cmF0aW9uVHlwZSwgdGVuYW50OiA/fSkgOiBQcm9taXNlPHZvaWQ+LCBwYXJhbGxlbD86IG51bWJlciwgZmlsdGVyPzogKHRlbmFudDogPykgPT4gYm9vbGVhbiwgY29uZmlndXJhdGlvbj86IGltcG9ydChcIi4uL2NvbmZpZ3VyYXRpb24uanNcIikuZGVmYXVsdH19IGFyZ3NcbiAgICogQHJldHVybnMge1Byb21pc2U8bnVtYmVyPn1cbiAgICovXG4gIHN0YXRpYyBhc3luYyBlYWNoKHtpZGVudGlmaWVyLCBjYWxsYmFjaywgcGFyYWxsZWwgPSAxLCBmaWx0ZXIsIGNvbmZpZ3VyYXRpb24gPSBDdXJyZW50LmNvbmZpZ3VyYXRpb24oKX0pIHtcbiAgICBjb25zdCBwcm92aWRlciA9IGNvbmZpZ3VyYXRpb24uZ2V0VGVuYW50RGF0YWJhc2VQcm92aWRlcihpZGVudGlmaWVyKVxuICAgIGNvbnN0IGxpc3RlZFRlbmFudHMgPSBhd2FpdCBjb25maWd1cmF0aW9uLmVuc3VyZUNvbm5lY3Rpb25zKHtuYW1lOiBgVGVuYW50LmVhY2g6ICR7aWRlbnRpZmllcn1gfSwgYXN5bmMgKCkgPT4ge1xuICAgICAgcmV0dXJuIGF3YWl0IHByb3ZpZGVyLmxpc3RUZW5hbnRzKHtjb25maWd1cmF0aW9uLCBpZGVudGlmaWVyfSlcbiAgICB9KVxuXG4gICAgaWYgKCFBcnJheS5pc0FycmF5KGxpc3RlZFRlbmFudHMpKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYFRlbmFudCBkYXRhYmFzZSBwcm92aWRlciBmb3IgJHtpZGVudGlmaWVyfSBtdXN0IHJldHVybiBhbiBhcnJheSBmcm9tIGxpc3RUZW5hbnRzYClcbiAgICB9XG5cbiAgICBjb25zdCB0ZW5hbnRzID0gZmlsdGVyID8gbGlzdGVkVGVuYW50cy5maWx0ZXIoZmlsdGVyKSA6IGxpc3RlZFRlbmFudHNcbiAgICBjb25zdCBpdGVyYXRvciA9IG5ldyBUZW5hbnRJdGVyYXRvcih7Y29uZmlndXJhdGlvbiwgaWRlbnRpZmllciwgcGFyYWxsZWxDb3VudDogcGFyYWxsZWx9KVxuXG4gICAgLy8gUnVuIGVhY2ggdGVuYW50J3MgY2FsbGJhY2sgaW5zaWRlIGVuc3VyZUNvbm5lY3Rpb25zIHNvIHRoZSBpdGVyYXRvciBzdGF5c1xuICAgIC8vIGNvbm5lY3Rpb24tYWdub3N0aWMgKHRoZSBkYjp0ZW5hbnRzOiogQ0xJIGNvbW1hbmRzIHNoYXJlIFRlbmFudEl0ZXJhdG9yIGFuZCBtdXN0IHJ1blxuICAgIC8vIHRoZWlyIGNhbGxiYWNrcywgc3VjaCBhcyBjcmVhdGUsIGJlZm9yZSB0aGUgdGVuYW50IGRhdGFiYXNlIGV4aXN0cykgd2hpbGUgcnVudGltZVxuICAgIC8vIGl0ZXJhdGlvbiBoZXJlIGdldHMgdGhlIHRlbmFudCdzIGNvbm5lY3Rpb25zIGVzdGFibGlzaGVkIHRoZSBzYW1lIHdheSBUZW5hbnQud2l0aCBkb2VzLlxuICAgIHJldHVybiBhd2FpdCBpdGVyYXRvci5ydW4odGVuYW50cywgYXN5bmMgKGNhbGxiYWNrQXJncykgPT4ge1xuICAgICAgYXdhaXQgY29uZmlndXJhdGlvbi5lbnN1cmVDb25uZWN0aW9ucyh7bmFtZTogYFRlbmFudC5lYWNoOiAke2lkZW50aWZpZXJ9YH0sIGFzeW5jICgpID0+IGF3YWl0IGNhbGxiYWNrKGNhbGxiYWNrQXJncykpXG4gICAgfSlcbiAgfVxuXG4gIC8qKlxuICAgKiBEcm9wcyBvbmUgdGVuYW50J3MgZGF0YWJhc2Uvc2NoZW1hIHRocm91Z2ggdGhlIHByb3ZpZGVyJ3MgYGRyb3BEYXRhYmFzZWAgaG9vay5cbiAgICogQHBhcmFtIHt7aWRlbnRpZmllcjogc3RyaW5nLCB0ZW5hbnQ6IG9iamVjdCwgY29uZmlndXJhdGlvbj86IGltcG9ydChcIi4uL2NvbmZpZ3VyYXRpb24uanNcIikuZGVmYXVsdH19IGFyZ3NcbiAgICogQHJldHVybnMge1Byb21pc2U8dm9pZD59XG4gICAqL1xuICBzdGF0aWMgYXN5bmMgZHJvcCh7aWRlbnRpZmllciwgdGVuYW50LCBjb25maWd1cmF0aW9uID0gQ3VycmVudC5jb25maWd1cmF0aW9uKCl9KSB7XG4gICAgY29uc3QgcHJvdmlkZXIgPSBjb25maWd1cmF0aW9uLmdldFRlbmFudERhdGFiYXNlUHJvdmlkZXIoaWRlbnRpZmllcilcblxuICAgIGlmICh0eXBlb2YgcHJvdmlkZXIuZHJvcERhdGFiYXNlICE9PSBcImZ1bmN0aW9uXCIpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgVGVuYW50IGRhdGFiYXNlIHByb3ZpZGVyIGZvciAke2lkZW50aWZpZXJ9IG11c3QgZGVmaW5lIGRyb3BEYXRhYmFzZSB0byBkcm9wIGEgdGVuYW50YClcbiAgICB9XG5cbiAgICBhd2FpdCBjb25maWd1cmF0aW9uLnJ1bldpdGhUZW5hbnQodGVuYW50LCBhc3luYyAoKSA9PiB7XG4gICAgICAvLyBHdWFyZCBhZ2FpbnN0IGFuIHVucmVzb2x2ZWQgdGVuYW50LiByZXNvbHZlRGF0YWJhc2VDb25maWd1cmF0aW9uIGZhbGxzIGJhY2sgdG8gdGhlXG4gICAgICAvLyBiYXNlICh0ZW1wbGF0ZS9kZWZhdWx0KSB0ZW5hbnQgZGF0YWJhc2Ugd2hlbiB0aGUgcmVzb2x2ZXIgcmV0dXJucyBub3RoaW5nIGZvciB0aGlzXG4gICAgICAvLyBkZXNjcmlwdG9yLCBzbyB3aXRob3V0IHRoaXMgY2hlY2sgYSBwcm92aWRlciB0aGF0IGRyb3BzIGJ5IGRhdGFiYXNlQ29uZmlndXJhdGlvbi5uYW1lXG4gICAgICAvLyB3b3VsZCBkcm9wIHRoZSB0ZW1wbGF0ZSBkYXRhYmFzZSBpbnN0ZWFkIG9mIHJlamVjdGluZyB0aGUgYmFkIHRlbmFudC5cbiAgICAgIGlmICghY29uZmlndXJhdGlvbi5pc0RhdGFiYXNlSWRlbnRpZmllckFjdGl2ZShpZGVudGlmaWVyKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFRlbmFudCBkYXRhYmFzZSBpZGVudGlmaWVyICR7aWRlbnRpZmllcn0gaXMgaW5hY3RpdmUgZm9yIHRlbmFudDogJHtUZW5hbnRJdGVyYXRvci50ZW5hbnRMYWJlbCh0ZW5hbnQpfWApXG4gICAgICB9XG5cbiAgICAgIGF3YWl0IHByb3ZpZGVyLmRyb3BEYXRhYmFzZT8uKHtcbiAgICAgICAgY29uZmlndXJhdGlvbixcbiAgICAgICAgZGF0YWJhc2VDb25maWd1cmF0aW9uOiBjb25maWd1cmF0aW9uLnJlc29sdmVEYXRhYmFzZUNvbmZpZ3VyYXRpb24oaWRlbnRpZmllciksXG4gICAgICAgIGlkZW50aWZpZXIsXG4gICAgICAgIHRlbmFudFxuICAgICAgfSlcbiAgICB9KVxuICB9XG59XG4iXX0=
@@ -0,0 +1,111 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Runs a callback once within each tenant's context for a tenant database identifier,
5
+ * optionally several tenants at a time. Each tenant is entered with `runWithTenant`, and a
6
+ * tenant whose database identifier is inactive throws rather than running the callback
7
+ * against the wrong connection. When iterating in parallel the per-tenant failures are
8
+ * collected and rethrown together as an `AggregateError` so one bad tenant does not hide the
9
+ * others. This is the iteration engine shared by the `db:tenants:*` CLI commands and the
10
+ * runtime {@link Tenant} façade; the caller is responsible for producing the tenant list.
11
+ */
12
+ export default class TenantIterator {
13
+ /**
14
+ * Creates an iterator bound to a configuration and tenant database identifier.
15
+ * @param {{configuration: import("../configuration.js").default, identifier: string, parallelCount?: number}} args
16
+ */
17
+ constructor({configuration, identifier, parallelCount = 1}) {
18
+ this.configuration = configuration
19
+ this.identifier = identifier
20
+ this.parallelCount = parallelCount
21
+ }
22
+
23
+ /**
24
+ * Runs `callback` within each tenant's context and returns how many tenants were processed.
25
+ * @param {Array<?>} tenants
26
+ * @param {function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>} callback
27
+ * @returns {Promise<number>}
28
+ */
29
+ async run(tenants, callback) {
30
+ if (this.parallelCount <= 1) {
31
+ for (const tenant of tenants) {
32
+ await this.runTenantCallback({callback, tenant})
33
+ }
34
+
35
+ return tenants.length
36
+ }
37
+
38
+ /** @type {Array<{error: Error, tenant: ?}>} */
39
+ const failures = []
40
+ const workers = []
41
+ let tenantIndex = 0
42
+ const workerCount = Math.min(this.parallelCount, tenants.length)
43
+
44
+ for (let workerIndex = 0; workerIndex < workerCount; workerIndex++) {
45
+ workers.push((async () => {
46
+ while (tenantIndex < tenants.length) {
47
+ const tenant = tenants[tenantIndex]
48
+
49
+ tenantIndex++
50
+
51
+ try {
52
+ await this.runTenantCallback({callback, tenant})
53
+ } catch (error) {
54
+ failures.push({
55
+ error: error instanceof Error ? error : new Error(String(error)),
56
+ tenant
57
+ })
58
+ }
59
+ }
60
+ })())
61
+ }
62
+
63
+ await Promise.all(workers)
64
+
65
+ if (failures.length > 0) {
66
+ const failedTenantLabels = failures.map((failure) => TenantIterator.tenantLabel(failure.tenant)).join(", ")
67
+
68
+ throw new AggregateError(
69
+ failures.map((failure) => failure.error),
70
+ `Failed tenant database command for tenant(s): ${failedTenantLabels}`
71
+ )
72
+ }
73
+
74
+ return tenants.length
75
+ }
76
+
77
+ /**
78
+ * Enters one tenant's context and runs the callback, asserting the database is active first.
79
+ * @param {{callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, tenant: ?}} args
80
+ * @returns {Promise<void>}
81
+ */
82
+ async runTenantCallback({callback, tenant}) {
83
+ await this.configuration.runWithTenant(tenant, async () => {
84
+ if (!this.configuration.isDatabaseIdentifierActive(this.identifier)) {
85
+ throw new Error(`Tenant database identifier ${this.identifier} is inactive for tenant: ${TenantIterator.tenantLabel(tenant)}`)
86
+ }
87
+
88
+ await callback({
89
+ databaseConfiguration: this.configuration.resolveDatabaseConfiguration(this.identifier),
90
+ tenant
91
+ })
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Builds a human-readable label for a tenant for use in error messages.
97
+ * @param {?} tenant
98
+ * @returns {string}
99
+ */
100
+ static tenantLabel(tenant) {
101
+ if (tenant && typeof tenant === "object") {
102
+ const tenantObject = /** @type {{id?: ?, name?: ?, slug?: ?}} */ (tenant)
103
+
104
+ if (tenantObject.slug) return String(tenantObject.slug)
105
+ if (tenantObject.name) return String(tenantObject.name)
106
+ if (tenantObject.id) return String(tenantObject.id)
107
+ }
108
+
109
+ return JSON.stringify(tenant)
110
+ }
111
+ }
@@ -0,0 +1,104 @@
1
+ // @ts-check
2
+
3
+ import Current from "../current.js"
4
+ import TenantIterator from "./tenant-iterator.js"
5
+
6
+ /**
7
+ * Apartment-style runtime façade for multi-tenant apps. A "tenant" is whatever descriptor
8
+ * object the app's `tenantDatabaseResolver` understands (an account, a project, …); this
9
+ * class is the single discoverable home for switching into a tenant's context, reading the
10
+ * current one, iterating every tenant of a database identifier, and dropping a tenant's
11
+ * database. Switching delegates to {@link Current} (which owns the async-context tenant
12
+ * state) and additionally runs the callback inside `ensureConnections`, so entering a tenant
13
+ * makes its database immediately queryable — the apartment-style "switch" semantics — without
14
+ * the caller establishing connections itself; iteration and drop drive the app's tenant
15
+ * database provider hooks.
16
+ */
17
+ export default class Tenant {
18
+ /**
19
+ * Runs `callback` with `tenant` as the current tenant, restoring the previous tenant after.
20
+ * The callback runs inside `ensureConnections`, so every database identifier the tenant
21
+ * activates (the global database plus the tenant's database) has a checked-out connection
22
+ * available for the callback's duration: switching into a tenant makes it queryable without
23
+ * the caller wiring up connections. Already-checked-out connections are reused, so nesting
24
+ * `Tenant.with` calls does not open redundant connections. The callback receives the active
25
+ * connections keyed by identifier, the same as `ensureConnections`.
26
+ * @param {object} tenant Descriptor understood by the app's tenantDatabaseResolver.
27
+ * @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<?>} callback
28
+ * @returns {Promise<?>}
29
+ */
30
+ static async with(tenant, callback) {
31
+ const configuration = Current.configuration()
32
+
33
+ return await Current.withTenant(tenant, async () => await configuration.ensureConnections(callback))
34
+ }
35
+
36
+ /**
37
+ * The current tenant descriptor, or undefined when running outside any tenant context.
38
+ * @returns {Record<string, unknown> | undefined}
39
+ */
40
+ static current() {
41
+ return Current.tenant()
42
+ }
43
+
44
+ /**
45
+ * Lists the tenants for a database identifier through the provider and runs `callback`
46
+ * within each tenant's context, optionally filtered and several at a time. Like
47
+ * {@link Tenant.with}, the callback runs inside `ensureConnections` so each tenant's
48
+ * database is queryable without the caller wiring up connections. Returns how many tenants
49
+ * the callback ran for (after filtering).
50
+ * @param {{identifier: string, callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, parallel?: number, filter?: (tenant: ?) => boolean, configuration?: import("../configuration.js").default}} args
51
+ * @returns {Promise<number>}
52
+ */
53
+ static async each({identifier, callback, parallel = 1, filter, configuration = Current.configuration()}) {
54
+ const provider = configuration.getTenantDatabaseProvider(identifier)
55
+ const listedTenants = await configuration.ensureConnections({name: `Tenant.each: ${identifier}`}, async () => {
56
+ return await provider.listTenants({configuration, identifier})
57
+ })
58
+
59
+ if (!Array.isArray(listedTenants)) {
60
+ throw new Error(`Tenant database provider for ${identifier} must return an array from listTenants`)
61
+ }
62
+
63
+ const tenants = filter ? listedTenants.filter(filter) : listedTenants
64
+ const iterator = new TenantIterator({configuration, identifier, parallelCount: parallel})
65
+
66
+ // Run each tenant's callback inside ensureConnections so the iterator stays
67
+ // connection-agnostic (the db:tenants:* CLI commands share TenantIterator and must run
68
+ // their callbacks, such as create, before the tenant database exists) while runtime
69
+ // iteration here gets the tenant's connections established the same way Tenant.with does.
70
+ return await iterator.run(tenants, async (callbackArgs) => {
71
+ await configuration.ensureConnections({name: `Tenant.each: ${identifier}`}, async () => await callback(callbackArgs))
72
+ })
73
+ }
74
+
75
+ /**
76
+ * Drops one tenant's database/schema through the provider's `dropDatabase` hook.
77
+ * @param {{identifier: string, tenant: object, configuration?: import("../configuration.js").default}} args
78
+ * @returns {Promise<void>}
79
+ */
80
+ static async drop({identifier, tenant, configuration = Current.configuration()}) {
81
+ const provider = configuration.getTenantDatabaseProvider(identifier)
82
+
83
+ if (typeof provider.dropDatabase !== "function") {
84
+ throw new Error(`Tenant database provider for ${identifier} must define dropDatabase to drop a tenant`)
85
+ }
86
+
87
+ await configuration.runWithTenant(tenant, async () => {
88
+ // Guard against an unresolved tenant. resolveDatabaseConfiguration falls back to the
89
+ // base (template/default) tenant database when the resolver returns nothing for this
90
+ // descriptor, so without this check a provider that drops by databaseConfiguration.name
91
+ // would drop the template database instead of rejecting the bad tenant.
92
+ if (!configuration.isDatabaseIdentifierActive(identifier)) {
93
+ throw new Error(`Tenant database identifier ${identifier} is inactive for tenant: ${TenantIterator.tenantLabel(tenant)}`)
94
+ }
95
+
96
+ await provider.dropDatabase?.({
97
+ configuration,
98
+ databaseConfiguration: configuration.resolveDatabaseConfiguration(identifier),
99
+ identifier,
100
+ tenant
101
+ })
102
+ })
103
+ }
104
+ }