@stratum-hq/mysql 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/adapters/database.d.ts +19 -0
- package/dist/adapters/database.d.ts.map +1 -0
- package/dist/adapters/database.js +71 -0
- package/dist/adapters/database.js.map +1 -0
- package/dist/adapters/shared.d.ts +30 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +103 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/table.d.ts +23 -0
- package/dist/adapters/table.d.ts.map +1 -0
- package/dist/adapters/table.js +70 -0
- package/dist/adapters/table.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/knex.d.ts +21 -0
- package/dist/integrations/knex.d.ts.map +1 -0
- package/dist/integrations/knex.js +16 -0
- package/dist/integrations/knex.js.map +1 -0
- package/dist/integrations/sequelize.d.ts +12 -0
- package/dist/integrations/sequelize.d.ts.map +1 -0
- package/dist/integrations/sequelize.js +25 -0
- package/dist/integrations/sequelize.js.map +1 -0
- package/dist/integrations/typeorm-subscriber.d.ts +18 -0
- package/dist/integrations/typeorm-subscriber.d.ts.map +1 -0
- package/dist/integrations/typeorm-subscriber.js +22 -0
- package/dist/integrations/typeorm-subscriber.js.map +1 -0
- package/dist/pool-manager.d.ts +48 -0
- package/dist/pool-manager.d.ts.map +1 -0
- package/dist/pool-manager.js +149 -0
- package/dist/pool-manager.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +51 -0
- package/dist/utils.js.map +1 -0
- package/dist/views/manager.d.ts +18 -0
- package/dist/views/manager.d.ts.map +1 -0
- package/dist/views/manager.js +40 -0
- package/dist/views/manager.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @stratum-hq/mysql
|
|
2
|
+
|
|
3
|
+
MySQL tenant isolation for [Stratum](https://github.com/stratum-hq/Stratum). Three isolation strategies, ORM integrations, and MySQL View utilities.
|
|
4
|
+
|
|
5
|
+
## Isolation Strategies
|
|
6
|
+
|
|
7
|
+
| Strategy | Mechanism | Security Level |
|
|
8
|
+
|----------|-----------|----------------|
|
|
9
|
+
| **Shared table** | Application-level `WHERE tenant_id = ?` on all queries | App-level |
|
|
10
|
+
| **Table-per-tenant** | Separate tables named `{base}_{tenantSlug}` | Structural |
|
|
11
|
+
| **Database-per-tenant** | Separate databases per tenant, LRU-managed pool | Full |
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @stratum-hq/mysql mysql2
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Shared Table (recommended for most apps)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import mysql from "mysql2/promise";
|
|
23
|
+
import { MysqlSharedAdapter } from "@stratum-hq/mysql";
|
|
24
|
+
|
|
25
|
+
const pool = mysql.createPool(process.env.MYSQL_URL);
|
|
26
|
+
const adapter = new MysqlSharedAdapter({ pool, databaseName: "myapp" });
|
|
27
|
+
|
|
28
|
+
// Structured query methods auto-inject tenant_id
|
|
29
|
+
const users = await adapter.scopedSelect("tenant-a", "users");
|
|
30
|
+
await adapter.scopedInsert("tenant-a", "users", { name: "Alice" });
|
|
31
|
+
await adapter.scopedUpdate("tenant-a", "users", { name: "Bob" }, { id: 1 });
|
|
32
|
+
await adapter.scopedDelete("tenant-a", "users", { id: 1 });
|
|
33
|
+
|
|
34
|
+
// Raw escape hatch (you own the WHERE clause)
|
|
35
|
+
await adapter.scopedRawQuery("SELECT * FROM users WHERE tenant_id = ? AND active = ?", ["tenant-a", true]);
|
|
36
|
+
|
|
37
|
+
// GDPR purge
|
|
38
|
+
await adapter.purgeTenantData("tenant-a");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Table-per-Tenant
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { MysqlTableAdapter } from "@stratum-hq/mysql";
|
|
45
|
+
|
|
46
|
+
const adapter = new MysqlTableAdapter({ pool, databaseName: "myapp" });
|
|
47
|
+
|
|
48
|
+
// Returns escaped table name: `users_tenanta`
|
|
49
|
+
const tableName = adapter.scopedTable("tenanta", "users");
|
|
50
|
+
|
|
51
|
+
// Use the pool directly with the scoped table name
|
|
52
|
+
const [rows] = await pool.query(`SELECT * FROM ${tableName}`);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Database-per-Tenant
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { MysqlDatabaseAdapter } from "@stratum-hq/mysql";
|
|
59
|
+
|
|
60
|
+
const adapter = new MysqlDatabaseAdapter({
|
|
61
|
+
createPool: (uri) => mysql.createPool(uri),
|
|
62
|
+
baseUri: "mysql://root@localhost:3306/placeholder",
|
|
63
|
+
maxPools: 20,
|
|
64
|
+
idleTimeoutMs: 60000,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Returns a pool connected to stratum_tenant_tenanta
|
|
68
|
+
const tenantPool = await adapter.getPool("tenanta");
|
|
69
|
+
const [rows] = await tenantPool.query("SELECT * FROM users");
|
|
70
|
+
|
|
71
|
+
// Clean up on shutdown
|
|
72
|
+
await adapter.closeAll();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## ORM Integrations
|
|
76
|
+
|
|
77
|
+
### TypeORM Subscriber (writes only)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { StratumTypeOrmSubscriber } from "@stratum-hq/mysql";
|
|
81
|
+
|
|
82
|
+
// Add to your TypeORM data source subscribers
|
|
83
|
+
const dataSource = new DataSource({
|
|
84
|
+
subscribers: [StratumTypeOrmSubscriber],
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Limitation:** TypeORM subscribers can intercept writes but not reads. Use the shared-table adapter's structured methods for tenant-scoped reads.
|
|
89
|
+
|
|
90
|
+
### Knex Helper
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { withTenantScope } from "@stratum-hq/mysql";
|
|
94
|
+
|
|
95
|
+
const tenantKnex = withTenantScope(knex, "tenant-a");
|
|
96
|
+
const users = await tenantKnex("users").select("*");
|
|
97
|
+
// Automatically adds: WHERE tenant_id = 'tenant-a'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Sequelize Adapter
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { withMysqlTenantScope } from "@stratum-hq/mysql";
|
|
104
|
+
|
|
105
|
+
await withMysqlTenantScope(sequelize, "tenant-a", async (scoped) => {
|
|
106
|
+
// Session variable @stratum_tenant_id is set for this scope
|
|
107
|
+
// Guaranteed cleanup via try/finally, even on errors
|
|
108
|
+
const users = await scoped.query("SELECT * FROM users_view");
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## MySQL Views (convenience layer)
|
|
113
|
+
|
|
114
|
+
Views provide a convenient way to create tenant-scoped read access using MySQL session variables.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { createTenantView, setTenantSession } from "@stratum-hq/mysql";
|
|
118
|
+
|
|
119
|
+
// Create a view that filters by @stratum_tenant_id
|
|
120
|
+
await createTenantView(pool, "users");
|
|
121
|
+
// Creates: CREATE OR REPLACE VIEW users_tenant_view AS
|
|
122
|
+
// SELECT * FROM users WHERE tenant_id = @stratum_tenant_id
|
|
123
|
+
|
|
124
|
+
// Set the session variable, then query the view
|
|
125
|
+
const conn = await pool.getConnection();
|
|
126
|
+
await setTenantSession(conn, "tenant-a");
|
|
127
|
+
const [rows] = await conn.query("SELECT * FROM users_tenant_view");
|
|
128
|
+
conn.release();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Important:** Views are NOT a security boundary (unlike Postgres RLS). Queries to underlying tables bypass isolation entirely. Views referencing session variables may not use indexes efficiently on large tables. Use the shared-table adapter's structured methods for high-performance workloads.
|
|
132
|
+
|
|
133
|
+
## GDPR Compliance
|
|
134
|
+
|
|
135
|
+
All three adapters implement `purgeTenantData(tenantSlug)`:
|
|
136
|
+
|
|
137
|
+
- **Shared table**: discovers tenant tables via `INFORMATION_SCHEMA`, then `DELETE FROM table WHERE tenant_id = ?`
|
|
138
|
+
- **Table-per-tenant**: discovers tables via `SHOW TABLES LIKE '%_slug'`, then `DROP TABLE`
|
|
139
|
+
- **Database-per-tenant**: `DROP DATABASE stratum_tenant_slug`
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MysqlAdapter, MysqlDatabaseAdapterOptions, MysqlPoolLike, PurgeResult, AdapterStats } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Database-per-tenant adapter: each tenant gets a dedicated MySQL database.
|
|
4
|
+
* Connections are managed by MysqlPoolManager with LRU eviction.
|
|
5
|
+
*/
|
|
6
|
+
export declare class MysqlDatabaseAdapter implements MysqlAdapter {
|
|
7
|
+
private readonly poolManager;
|
|
8
|
+
constructor(options: MysqlDatabaseAdapterOptions);
|
|
9
|
+
/** Returns a MysqlPoolLike for the tenant's dedicated database. */
|
|
10
|
+
getPool(tenantSlug: string): Promise<MysqlPoolLike>;
|
|
11
|
+
/** Decrements the refCount for the pool associated with the given slug. */
|
|
12
|
+
releasePool(tenantSlug: string): void;
|
|
13
|
+
purgeTenantData(tenantSlug: string): Promise<PurgeResult>;
|
|
14
|
+
/** Closes all pooled connections. Call during application shutdown. */
|
|
15
|
+
closeAll(): Promise<void>;
|
|
16
|
+
/** Returns adapter statistics. */
|
|
17
|
+
getStats(): AdapterStats;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/adapters/database.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,2BAA2B,EAC3B,aAAa,EACb,WAAW,EACX,YAAY,EACb,MAAM,aAAa,CAAC;AAIrB;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;gBAEnC,OAAO,EAAE,2BAA2B;IAShD,mEAAmE;IAC7D,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAKzD,2EAA2E;IAC3E,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAI/B,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IA2B/D,uEAAuE;IACjE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,kCAAkC;IAClC,QAAQ,IAAI,YAAY;CAMzB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MysqlDatabaseAdapter = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
const pool_manager_js_1 = require("../pool-manager.js");
|
|
6
|
+
const utils_js_1 = require("../utils.js");
|
|
7
|
+
/**
|
|
8
|
+
* Database-per-tenant adapter: each tenant gets a dedicated MySQL database.
|
|
9
|
+
* Connections are managed by MysqlPoolManager with LRU eviction.
|
|
10
|
+
*/
|
|
11
|
+
class MysqlDatabaseAdapter {
|
|
12
|
+
poolManager;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.poolManager = new pool_manager_js_1.MysqlPoolManager({
|
|
15
|
+
createPool: options.createPool,
|
|
16
|
+
baseUri: options.baseUri,
|
|
17
|
+
maxPools: options.maxPools,
|
|
18
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Returns a MysqlPoolLike for the tenant's dedicated database. */
|
|
22
|
+
async getPool(tenantSlug) {
|
|
23
|
+
(0, core_1.validateSlug)(tenantSlug);
|
|
24
|
+
return this.poolManager.getPool(tenantSlug);
|
|
25
|
+
}
|
|
26
|
+
/** Decrements the refCount for the pool associated with the given slug. */
|
|
27
|
+
releasePool(tenantSlug) {
|
|
28
|
+
this.poolManager.releasePool(tenantSlug);
|
|
29
|
+
}
|
|
30
|
+
async purgeTenantData(tenantSlug) {
|
|
31
|
+
(0, core_1.validateSlug)(tenantSlug);
|
|
32
|
+
try {
|
|
33
|
+
const pool = await this.poolManager.getPool(tenantSlug);
|
|
34
|
+
const dbName = `stratum_tenant_${tenantSlug}`;
|
|
35
|
+
try {
|
|
36
|
+
await pool.query(`DROP DATABASE IF EXISTS ${(0, utils_js_1.escapeIdentifier)(dbName)}`);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
this.poolManager.releasePool(tenantSlug);
|
|
40
|
+
}
|
|
41
|
+
await this.poolManager.closePool(tenantSlug);
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
tablesProcessed: 0,
|
|
45
|
+
rowsDeleted: 0,
|
|
46
|
+
errors: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
tablesProcessed: 0,
|
|
53
|
+
rowsDeleted: 0,
|
|
54
|
+
errors: [{ table: "*", error: err }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Closes all pooled connections. Call during application shutdown. */
|
|
59
|
+
async closeAll() {
|
|
60
|
+
await this.poolManager.closeAll();
|
|
61
|
+
}
|
|
62
|
+
/** Returns adapter statistics. */
|
|
63
|
+
getStats() {
|
|
64
|
+
return {
|
|
65
|
+
strategy: "database-per-tenant",
|
|
66
|
+
...this.poolManager.getStats(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.MysqlDatabaseAdapter = MysqlDatabaseAdapter;
|
|
71
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/adapters/database.ts"],"names":[],"mappings":";;;AAAA,2CAAgD;AAQhD,wDAAsD;AACtD,0CAA+C;AAE/C;;;GAGG;AACH,MAAa,oBAAoB;IACd,WAAW,CAAmB;IAE/C,YAAY,OAAoC;QAC9C,IAAI,CAAC,WAAW,GAAG,IAAI,kCAAgB,CAAC;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,aAAa,EAAE,OAAO,CAAC,aAAa;SACrC,CAAC,CAAC;IACL,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,OAAO,CAAC,UAAkB;QAC9B,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QACzB,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,CAAC;IAED,2EAA2E;IAC3E,WAAW,CAAC,UAAkB;QAC5B,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,UAAkB;QACtC,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YACxD,MAAM,MAAM,GAAG,kBAAkB,UAAU,EAAE,CAAC;YAC9C,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,KAAK,CAAC,2BAA2B,IAAA,2BAAgB,EAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1E,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;YAC3C,CAAC;YACD,MAAM,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;YAC7C,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,eAAe,EAAE,CAAC;gBAClB,WAAW,EAAE,CAAC;gBACd,MAAM,EAAE,EAAE;aACX,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,eAAe,EAAE,CAAC;gBAClB,WAAW,EAAE,CAAC;gBACd,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAY,EAAE,CAAC;aAC9C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;IAED,kCAAkC;IAClC,QAAQ;QACN,OAAO;YACL,QAAQ,EAAE,qBAAqB;YAC/B,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE;SAC/B,CAAC;IACJ,CAAC;CACF;AA9DD,oDA8DC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MysqlAdapter, MysqlSharedAdapterOptions, PurgeResult, AdapterStats } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared-table adapter: all tenants share the same tables, isolated by a
|
|
4
|
+
* `tenant_id` column injected into every query via structured methods.
|
|
5
|
+
*/
|
|
6
|
+
export declare class MysqlSharedAdapter implements MysqlAdapter {
|
|
7
|
+
private readonly pool;
|
|
8
|
+
private readonly databaseName;
|
|
9
|
+
constructor(options: MysqlSharedAdapterOptions);
|
|
10
|
+
/**
|
|
11
|
+
* SELECT with WHERE tenant_id = ?. conditions is a Record turned into AND clauses.
|
|
12
|
+
* columns defaults to *.
|
|
13
|
+
*/
|
|
14
|
+
scopedSelect(tenantId: string, table: string, conditions?: Record<string, unknown>, columns?: string[]): Promise<unknown[]>;
|
|
15
|
+
/** INSERT with tenant_id added to data. */
|
|
16
|
+
scopedInsert(tenantId: string, table: string, data: Record<string, unknown>): Promise<unknown>;
|
|
17
|
+
/** UPDATE SET ... WHERE tenant_id = ? AND conditions. */
|
|
18
|
+
scopedUpdate(tenantId: string, table: string, data: Record<string, unknown>, conditions: Record<string, unknown>): Promise<unknown>;
|
|
19
|
+
/** DELETE WHERE tenant_id = ? AND optional conditions. */
|
|
20
|
+
scopedDelete(tenantId: string, table: string, conditions?: Record<string, unknown>): Promise<unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Executes SQL as-is. Caller owns the WHERE clause and all tenant scoping.
|
|
23
|
+
* No tenant_id injection is performed.
|
|
24
|
+
*/
|
|
25
|
+
scopedRawQuery(sql: string, params?: unknown[]): Promise<unknown>;
|
|
26
|
+
purgeTenantData(tenantId: string): Promise<PurgeResult>;
|
|
27
|
+
/** Returns adapter statistics. */
|
|
28
|
+
getStats(): AdapterStats;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,yBAAyB,EAEzB,WAAW,EACX,YAAY,EACb,MAAM,aAAa,CAAC;AAGrB;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,OAAO,EAAE,yBAAyB;IAK9C;;;OAGG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,GAAE,MAAM,EAAU,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC;IAqBrB,2CAA2C;IACrC,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,OAAO,CAAC;IAanB,yDAAyD;IACnD,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,OAAO,CAAC;IAqBnB,0DAA0D;IACpD,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,OAAO,CAAC;IAkBnB;;;OAGG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAIjE,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAuB7D,kCAAkC;IAClC,QAAQ,IAAI,YAAY;CAGzB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MysqlSharedAdapter = void 0;
|
|
4
|
+
const utils_js_1 = require("../utils.js");
|
|
5
|
+
/**
|
|
6
|
+
* Shared-table adapter: all tenants share the same tables, isolated by a
|
|
7
|
+
* `tenant_id` column injected into every query via structured methods.
|
|
8
|
+
*/
|
|
9
|
+
class MysqlSharedAdapter {
|
|
10
|
+
pool;
|
|
11
|
+
databaseName;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.pool = options.pool;
|
|
14
|
+
this.databaseName = options.databaseName;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* SELECT with WHERE tenant_id = ?. conditions is a Record turned into AND clauses.
|
|
18
|
+
* columns defaults to *.
|
|
19
|
+
*/
|
|
20
|
+
async scopedSelect(tenantId, table, conditions, columns = ["*"]) {
|
|
21
|
+
(0, utils_js_1.assertTenantId)(tenantId);
|
|
22
|
+
const cols = columns.map((c) => (c === "*" ? "*" : (0, utils_js_1.escapeIdentifier)(c))).join(", ");
|
|
23
|
+
const qualifiedTable = `${(0, utils_js_1.escapeIdentifier)(this.databaseName)}.${(0, utils_js_1.escapeIdentifier)(table)}`;
|
|
24
|
+
const whereClauses = [`tenant_id = ?`];
|
|
25
|
+
const values = [tenantId];
|
|
26
|
+
if (conditions) {
|
|
27
|
+
for (const [col, val] of Object.entries(conditions)) {
|
|
28
|
+
whereClauses.push(`${(0, utils_js_1.escapeIdentifier)(col)} = ?`);
|
|
29
|
+
values.push(val);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const sql = `SELECT ${cols} FROM ${qualifiedTable} WHERE ${whereClauses.join(" AND ")}`;
|
|
33
|
+
const [rows] = await this.pool.query(sql, values);
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
/** INSERT with tenant_id added to data. */
|
|
37
|
+
async scopedInsert(tenantId, table, data) {
|
|
38
|
+
(0, utils_js_1.assertTenantId)(tenantId);
|
|
39
|
+
const row = { ...data, tenant_id: tenantId };
|
|
40
|
+
const qualifiedTable = `${(0, utils_js_1.escapeIdentifier)(this.databaseName)}.${(0, utils_js_1.escapeIdentifier)(table)}`;
|
|
41
|
+
const cols = Object.keys(row).map(utils_js_1.escapeIdentifier).join(", ");
|
|
42
|
+
const placeholders = Object.keys(row).map(() => "?").join(", ");
|
|
43
|
+
const values = Object.values(row);
|
|
44
|
+
const sql = `INSERT INTO ${qualifiedTable} (${cols}) VALUES (${placeholders})`;
|
|
45
|
+
return this.pool.query(sql, values);
|
|
46
|
+
}
|
|
47
|
+
/** UPDATE SET ... WHERE tenant_id = ? AND conditions. */
|
|
48
|
+
async scopedUpdate(tenantId, table, data, conditions) {
|
|
49
|
+
(0, utils_js_1.assertTenantId)(tenantId);
|
|
50
|
+
const qualifiedTable = `${(0, utils_js_1.escapeIdentifier)(this.databaseName)}.${(0, utils_js_1.escapeIdentifier)(table)}`;
|
|
51
|
+
const setClauses = Object.keys(data)
|
|
52
|
+
.map((col) => `${(0, utils_js_1.escapeIdentifier)(col)} = ?`)
|
|
53
|
+
.join(", ");
|
|
54
|
+
const setValues = Object.values(data);
|
|
55
|
+
const whereClauses = [`tenant_id = ?`];
|
|
56
|
+
const whereValues = [tenantId];
|
|
57
|
+
for (const [col, val] of Object.entries(conditions)) {
|
|
58
|
+
whereClauses.push(`${(0, utils_js_1.escapeIdentifier)(col)} = ?`);
|
|
59
|
+
whereValues.push(val);
|
|
60
|
+
}
|
|
61
|
+
const sql = `UPDATE ${qualifiedTable} SET ${setClauses} WHERE ${whereClauses.join(" AND ")}`;
|
|
62
|
+
return this.pool.query(sql, [...setValues, ...whereValues]);
|
|
63
|
+
}
|
|
64
|
+
/** DELETE WHERE tenant_id = ? AND optional conditions. */
|
|
65
|
+
async scopedDelete(tenantId, table, conditions) {
|
|
66
|
+
(0, utils_js_1.assertTenantId)(tenantId);
|
|
67
|
+
const qualifiedTable = `${(0, utils_js_1.escapeIdentifier)(this.databaseName)}.${(0, utils_js_1.escapeIdentifier)(table)}`;
|
|
68
|
+
const whereClauses = [`tenant_id = ?`];
|
|
69
|
+
const values = [tenantId];
|
|
70
|
+
if (conditions) {
|
|
71
|
+
for (const [col, val] of Object.entries(conditions)) {
|
|
72
|
+
whereClauses.push(`${(0, utils_js_1.escapeIdentifier)(col)} = ?`);
|
|
73
|
+
values.push(val);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const sql = `DELETE FROM ${qualifiedTable} WHERE ${whereClauses.join(" AND ")}`;
|
|
77
|
+
return this.pool.query(sql, values);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Executes SQL as-is. Caller owns the WHERE clause and all tenant scoping.
|
|
81
|
+
* No tenant_id injection is performed.
|
|
82
|
+
*/
|
|
83
|
+
async scopedRawQuery(sql, params) {
|
|
84
|
+
return this.pool.query(sql, params);
|
|
85
|
+
}
|
|
86
|
+
async purgeTenantData(tenantId) {
|
|
87
|
+
(0, utils_js_1.assertTenantId)(tenantId);
|
|
88
|
+
// Discover all tables that have a tenant_id column in this database.
|
|
89
|
+
const [tableRows] = await this.pool.query(`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME = 'tenant_id' AND TABLE_SCHEMA = ?`, [this.databaseName]);
|
|
90
|
+
const results = await Promise.allSettled(tableRows.map(async (row) => {
|
|
91
|
+
const qualifiedTable = `${(0, utils_js_1.escapeIdentifier)(this.databaseName)}.${(0, utils_js_1.escapeIdentifier)(row.TABLE_NAME)}`;
|
|
92
|
+
const [result] = await this.pool.query(`DELETE FROM ${qualifiedTable} WHERE tenant_id = ?`, [tenantId]);
|
|
93
|
+
return { table: row.TABLE_NAME, rowsDeleted: result.affectedRows ?? 0 };
|
|
94
|
+
}));
|
|
95
|
+
return (0, utils_js_1.aggregatePurgeResults)(results);
|
|
96
|
+
}
|
|
97
|
+
/** Returns adapter statistics. */
|
|
98
|
+
getStats() {
|
|
99
|
+
return { strategy: "shared" };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.MysqlSharedAdapter = MysqlSharedAdapter;
|
|
103
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MysqlAdapter, MysqlTableAdapterOptions, MysqlPoolLike, PurgeResult, AdapterStats } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Table-per-tenant adapter: each tenant gets its own set of tables
|
|
4
|
+
* named `{baseTableName}_{tenantSlug}`.
|
|
5
|
+
*
|
|
6
|
+
* No filter injection needed -- isolation is structural.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MysqlTableAdapter implements MysqlAdapter {
|
|
9
|
+
private readonly pool;
|
|
10
|
+
private readonly databaseName;
|
|
11
|
+
constructor(options: MysqlTableAdapterOptions);
|
|
12
|
+
/**
|
|
13
|
+
* Validates slug and returns the escaped tenant-scoped table name
|
|
14
|
+
* in the form `{baseTableName}_{tenantSlug}`.
|
|
15
|
+
*/
|
|
16
|
+
scopedTable(tenantSlug: string, baseTableName: string): string;
|
|
17
|
+
/** Returns the underlying pool for raw queries against tenant tables. */
|
|
18
|
+
getPool(): MysqlPoolLike;
|
|
19
|
+
purgeTenantData(tenantSlug: string): Promise<PurgeResult>;
|
|
20
|
+
/** Returns adapter statistics. */
|
|
21
|
+
getStats(): AdapterStats;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/adapters/table.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,aAAa,EACb,WAAW,EACX,YAAY,EACb,MAAM,aAAa,CAAC;AAGrB;;;;;GAKG;AACH,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,OAAO,EAAE,wBAAwB;IAK7C;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM;IAM9D,yEAAyE;IACzE,OAAO,IAAI,aAAa;IAIlB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAqC/D,kCAAkC;IAClC,QAAQ,IAAI,YAAY;CAGzB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MysqlTableAdapter = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
const utils_js_1 = require("../utils.js");
|
|
6
|
+
/**
|
|
7
|
+
* Table-per-tenant adapter: each tenant gets its own set of tables
|
|
8
|
+
* named `{baseTableName}_{tenantSlug}`.
|
|
9
|
+
*
|
|
10
|
+
* No filter injection needed -- isolation is structural.
|
|
11
|
+
*/
|
|
12
|
+
class MysqlTableAdapter {
|
|
13
|
+
pool;
|
|
14
|
+
databaseName;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.pool = options.pool;
|
|
17
|
+
this.databaseName = options.databaseName;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validates slug and returns the escaped tenant-scoped table name
|
|
21
|
+
* in the form `{baseTableName}_{tenantSlug}`.
|
|
22
|
+
*/
|
|
23
|
+
scopedTable(tenantSlug, baseTableName) {
|
|
24
|
+
(0, core_1.validateSlug)(tenantSlug);
|
|
25
|
+
const tableName = `${baseTableName}_${tenantSlug}`;
|
|
26
|
+
return (0, utils_js_1.escapeIdentifier)(tableName);
|
|
27
|
+
}
|
|
28
|
+
/** Returns the underlying pool for raw queries against tenant tables. */
|
|
29
|
+
getPool() {
|
|
30
|
+
return this.pool;
|
|
31
|
+
}
|
|
32
|
+
async purgeTenantData(tenantSlug) {
|
|
33
|
+
(0, utils_js_1.assertTenantId)(tenantSlug);
|
|
34
|
+
(0, core_1.validateSlug)(tenantSlug);
|
|
35
|
+
const escapedDb = (0, utils_js_1.escapeIdentifier)(this.databaseName);
|
|
36
|
+
// SHOW TABLES LIKE '%_tenantSlug' to find all tenant tables.
|
|
37
|
+
const pattern = `%_${tenantSlug}`;
|
|
38
|
+
const [tableRows] = await this.pool.query(`SHOW TABLES FROM ${escapedDb} LIKE ?`, [pattern]);
|
|
39
|
+
const errors = [];
|
|
40
|
+
let tablesProcessed = 0;
|
|
41
|
+
const exactSuffix = `_${tenantSlug}`;
|
|
42
|
+
for (const row of tableRows) {
|
|
43
|
+
const tableName = Object.values(row)[0];
|
|
44
|
+
if (!tableName)
|
|
45
|
+
continue;
|
|
46
|
+
// Post-filter: verify exact suffix match to prevent 'e' matching 'orders_acme'
|
|
47
|
+
if (!tableName.endsWith(exactSuffix))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
await this.pool.query(`DROP TABLE IF EXISTS ${escapedDb}.${(0, utils_js_1.escapeIdentifier)(tableName)}`);
|
|
51
|
+
tablesProcessed++;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
errors.push({ table: tableName, error: err });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
success: errors.length === 0,
|
|
59
|
+
tablesProcessed,
|
|
60
|
+
rowsDeleted: 0,
|
|
61
|
+
errors,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Returns adapter statistics. */
|
|
65
|
+
getStats() {
|
|
66
|
+
return { strategy: "table-per-tenant" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.MysqlTableAdapter = MysqlTableAdapter;
|
|
70
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.js","sourceRoot":"","sources":["../../src/adapters/table.ts"],"names":[],"mappings":";;;AAAA,2CAAgD;AAQhD,0CAA+D;AAE/D;;;;;GAKG;AACH,MAAa,iBAAiB;IACX,IAAI,CAAgB;IACpB,YAAY,CAAS;IAEtC,YAAY,OAAiC;QAC3C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,UAAkB,EAAE,aAAqB;QACnD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QACzB,MAAM,SAAS,GAAG,GAAG,aAAa,IAAI,UAAU,EAAE,CAAC;QACnD,OAAO,IAAA,2BAAgB,EAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAED,yEAAyE;IACzE,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,UAAkB;QACtC,IAAA,yBAAc,EAAC,UAAU,CAAC,CAAC;QAC3B,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QAEzB,MAAM,SAAS,GAAG,IAAA,2BAAgB,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACtD,6DAA6D;QAC7D,MAAM,OAAO,GAAG,KAAK,UAAU,EAAE,CAAC;QAClC,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CACvC,oBAAoB,SAAS,SAAS,EACtC,CAAC,OAAO,CAAC,CACkC,CAAC;QAE9C,MAAM,MAAM,GAA0B,EAAE,CAAC;QACzC,IAAI,eAAe,GAAG,CAAC,CAAC;QAExB,MAAM,WAAW,GAAG,IAAI,UAAU,EAAE,CAAC;QACrC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACxC,IAAI,CAAC,SAAS;gBAAE,SAAS;YACzB,+EAA+E;YAC/E,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAAE,SAAS;YAC/C,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,wBAAwB,SAAS,IAAI,IAAA,2BAAgB,EAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBAC1F,eAAe,EAAE,CAAC;YACpB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,GAAY,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QAED,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;YAC5B,eAAe;YACf,WAAW,EAAE,CAAC;YACd,MAAM;SACP,CAAC;IACJ,CAAC;IAED,kCAAkC;IAClC,QAAQ;QACN,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC1C,CAAC;CACF;AAjED,8CAiEC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { MysqlConnectionLike, MysqlPoolLike, MysqlAdapter, PurgeResult, AdapterStats, MysqlSharedAdapterOptions, MysqlTableAdapterOptions, MysqlDatabaseAdapterOptions, MysqlPoolManagerOptions, } from "./types.js";
|
|
2
|
+
export { MysqlSharedAdapter } from "./adapters/shared.js";
|
|
3
|
+
export { MysqlTableAdapter } from "./adapters/table.js";
|
|
4
|
+
export { MysqlDatabaseAdapter } from "./adapters/database.js";
|
|
5
|
+
export { MysqlPoolManager } from "./pool-manager.js";
|
|
6
|
+
export { createTenantView, dropTenantView, setTenantSession } from "./views/manager.js";
|
|
7
|
+
export { StratumTypeOrmSubscriber } from "./integrations/typeorm-subscriber.js";
|
|
8
|
+
export type { InsertEvent, EntitySubscriberInterface } from "./integrations/typeorm-subscriber.js";
|
|
9
|
+
export { withTenantScope } from "./integrations/knex.js";
|
|
10
|
+
export type { KnexLike, KnexQueryBuilderLike } from "./integrations/knex.js";
|
|
11
|
+
export { withMysqlTenantScope } from "./integrations/sequelize.js";
|
|
12
|
+
export type { SequelizeLike } from "./integrations/sequelize.js";
|
|
13
|
+
export { assertTenantId, escapeIdentifier, aggregatePurgeResults } from "./utils.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGxF,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAChF,YAAY,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAGjE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aggregatePurgeResults = exports.escapeIdentifier = exports.assertTenantId = exports.withMysqlTenantScope = exports.withTenantScope = exports.StratumTypeOrmSubscriber = exports.setTenantSession = exports.dropTenantView = exports.createTenantView = exports.MysqlPoolManager = exports.MysqlDatabaseAdapter = exports.MysqlTableAdapter = exports.MysqlSharedAdapter = void 0;
|
|
4
|
+
// Adapters
|
|
5
|
+
var shared_js_1 = require("./adapters/shared.js");
|
|
6
|
+
Object.defineProperty(exports, "MysqlSharedAdapter", { enumerable: true, get: function () { return shared_js_1.MysqlSharedAdapter; } });
|
|
7
|
+
var table_js_1 = require("./adapters/table.js");
|
|
8
|
+
Object.defineProperty(exports, "MysqlTableAdapter", { enumerable: true, get: function () { return table_js_1.MysqlTableAdapter; } });
|
|
9
|
+
var database_js_1 = require("./adapters/database.js");
|
|
10
|
+
Object.defineProperty(exports, "MysqlDatabaseAdapter", { enumerable: true, get: function () { return database_js_1.MysqlDatabaseAdapter; } });
|
|
11
|
+
// Pool manager
|
|
12
|
+
var pool_manager_js_1 = require("./pool-manager.js");
|
|
13
|
+
Object.defineProperty(exports, "MysqlPoolManager", { enumerable: true, get: function () { return pool_manager_js_1.MysqlPoolManager; } });
|
|
14
|
+
// Views
|
|
15
|
+
var manager_js_1 = require("./views/manager.js");
|
|
16
|
+
Object.defineProperty(exports, "createTenantView", { enumerable: true, get: function () { return manager_js_1.createTenantView; } });
|
|
17
|
+
Object.defineProperty(exports, "dropTenantView", { enumerable: true, get: function () { return manager_js_1.dropTenantView; } });
|
|
18
|
+
Object.defineProperty(exports, "setTenantSession", { enumerable: true, get: function () { return manager_js_1.setTenantSession; } });
|
|
19
|
+
// Integrations
|
|
20
|
+
var typeorm_subscriber_js_1 = require("./integrations/typeorm-subscriber.js");
|
|
21
|
+
Object.defineProperty(exports, "StratumTypeOrmSubscriber", { enumerable: true, get: function () { return typeorm_subscriber_js_1.StratumTypeOrmSubscriber; } });
|
|
22
|
+
var knex_js_1 = require("./integrations/knex.js");
|
|
23
|
+
Object.defineProperty(exports, "withTenantScope", { enumerable: true, get: function () { return knex_js_1.withTenantScope; } });
|
|
24
|
+
var sequelize_js_1 = require("./integrations/sequelize.js");
|
|
25
|
+
Object.defineProperty(exports, "withMysqlTenantScope", { enumerable: true, get: function () { return sequelize_js_1.withMysqlTenantScope; } });
|
|
26
|
+
// Utilities
|
|
27
|
+
var utils_js_1 = require("./utils.js");
|
|
28
|
+
Object.defineProperty(exports, "assertTenantId", { enumerable: true, get: function () { return utils_js_1.assertTenantId; } });
|
|
29
|
+
Object.defineProperty(exports, "escapeIdentifier", { enumerable: true, get: function () { return utils_js_1.escapeIdentifier; } });
|
|
30
|
+
Object.defineProperty(exports, "aggregatePurgeResults", { enumerable: true, get: function () { return utils_js_1.aggregatePurgeResults; } });
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAaA,WAAW;AACX,kDAA0D;AAAjD,+GAAA,kBAAkB,OAAA;AAC3B,gDAAwD;AAA/C,6GAAA,iBAAiB,OAAA;AAC1B,sDAA8D;AAArD,mHAAA,oBAAoB,OAAA;AAE7B,eAAe;AACf,qDAAqD;AAA5C,mHAAA,gBAAgB,OAAA;AAEzB,QAAQ;AACR,iDAAwF;AAA/E,8GAAA,gBAAgB,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAE3D,eAAe;AACf,8EAAgF;AAAvE,iIAAA,wBAAwB,OAAA;AAEjC,kDAAyD;AAAhD,0GAAA,eAAe,OAAA;AAExB,4DAAmE;AAA1D,oHAAA,oBAAoB,OAAA;AAG7B,YAAY;AACZ,uCAAqF;AAA5E,0GAAA,cAAc,OAAA;AAAE,4GAAA,gBAAgB,OAAA;AAAE,iHAAA,qBAAqB,OAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface KnexQueryBuilderLike {
|
|
2
|
+
where(col: string, val: unknown): this;
|
|
3
|
+
select(...cols: string[]): this;
|
|
4
|
+
insert(data: Record<string, unknown>): Promise<unknown>;
|
|
5
|
+
update(data: Record<string, unknown>): this;
|
|
6
|
+
delete(): this;
|
|
7
|
+
first(): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface KnexLike {
|
|
10
|
+
(tableName: string): KnexQueryBuilderLike;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns a wrapper function that pre-scopes queries to the given tenant.
|
|
14
|
+
* Call the returned function with a table name to get a WHERE tenant_id = ? builder.
|
|
15
|
+
*
|
|
16
|
+
* Known limitation: the .where() clause is ignored by Knex on INSERT operations.
|
|
17
|
+
* You must include tenant_id in the data object yourself when inserting:
|
|
18
|
+
* await scoped("users").insert({ tenant_id: tenantId, name: "Alice" });
|
|
19
|
+
*/
|
|
20
|
+
export declare function withTenantScope(knex: KnexLike, tenantId: string): (tableName: string) => KnexQueryBuilderLike;
|
|
21
|
+
//# sourceMappingURL=knex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knex.d.ts","sourceRoot":"","sources":["../../src/integrations/knex.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,MAAM,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5C,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,CAAC;CAC3C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,MAAM,GACf,CAAC,SAAS,EAAE,MAAM,KAAK,oBAAoB,CAE7C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Structural type for Knex (no hard dependency on knex) ───
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.withTenantScope = withTenantScope;
|
|
5
|
+
/**
|
|
6
|
+
* Returns a wrapper function that pre-scopes queries to the given tenant.
|
|
7
|
+
* Call the returned function with a table name to get a WHERE tenant_id = ? builder.
|
|
8
|
+
*
|
|
9
|
+
* Known limitation: the .where() clause is ignored by Knex on INSERT operations.
|
|
10
|
+
* You must include tenant_id in the data object yourself when inserting:
|
|
11
|
+
* await scoped("users").insert({ tenant_id: tenantId, name: "Alice" });
|
|
12
|
+
*/
|
|
13
|
+
function withTenantScope(knex, tenantId) {
|
|
14
|
+
return (tableName) => knex(tableName).where("tenant_id", tenantId);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=knex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knex.js","sourceRoot":"","sources":["../../src/integrations/knex.ts"],"names":[],"mappings":";AAAA,gEAAgE;;AAuBhE,0CAKC;AAbD;;;;;;;GAOG;AACH,SAAgB,eAAe,CAC7B,IAAc,EACd,QAAgB;IAEhB,OAAO,CAAC,SAAiB,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SequelizeLike {
|
|
2
|
+
query(sql: string, options?: unknown): Promise<unknown>;
|
|
3
|
+
transaction<T>(fn: (t: unknown) => Promise<T>): Promise<T>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Runs fn inside a session-variable scope for MySQL tenant isolation.
|
|
7
|
+
* Uses a Sequelize transaction to guarantee all queries (SET, user queries,
|
|
8
|
+
* SET NULL) run on the same pooled connection. The finally block clears the
|
|
9
|
+
* session variable even if fn throws.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withMysqlTenantScope<T>(sequelize: SequelizeLike, tenantId: string, fn: (sequelize: SequelizeLike) => Promise<T>): Promise<T>;
|
|
12
|
+
//# sourceMappingURL=sequelize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequelize.d.ts","sourceRoot":"","sources":["../../src/integrations/sequelize.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC5D;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,SAAS,EAAE,aAAa,EACxB,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3C,OAAO,CAAC,CAAC,CAAC,CAYZ"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Structural types (no hard dependency on sequelize) ───
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.withMysqlTenantScope = withMysqlTenantScope;
|
|
5
|
+
/**
|
|
6
|
+
* Runs fn inside a session-variable scope for MySQL tenant isolation.
|
|
7
|
+
* Uses a Sequelize transaction to guarantee all queries (SET, user queries,
|
|
8
|
+
* SET NULL) run on the same pooled connection. The finally block clears the
|
|
9
|
+
* session variable even if fn throws.
|
|
10
|
+
*/
|
|
11
|
+
async function withMysqlTenantScope(sequelize, tenantId, fn) {
|
|
12
|
+
return sequelize.transaction(async (transaction) => {
|
|
13
|
+
await sequelize.query("SET @stratum_tenant_id = ?", {
|
|
14
|
+
replacements: [tenantId],
|
|
15
|
+
transaction,
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
return await fn(sequelize);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await sequelize.query("SET @stratum_tenant_id = NULL", { transaction });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=sequelize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequelize.js","sourceRoot":"","sources":["../../src/integrations/sequelize.ts"],"names":[],"mappings":";AAAA,6DAA6D;;AAa7D,oDAgBC;AAtBD;;;;;GAKG;AACI,KAAK,UAAU,oBAAoB,CACxC,SAAwB,EACxB,QAAgB,EAChB,EAA4C;IAE5C,OAAO,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE;QACjD,MAAM,SAAS,CAAC,KAAK,CAAC,4BAA4B,EAAE;YAClD,YAAY,EAAE,CAAC,QAAQ,CAAC;YACxB,WAAW;SACZ,CAAC,CAAC;QACH,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;gBAAS,CAAC;YACT,MAAM,SAAS,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface InsertEvent {
|
|
2
|
+
entity: Record<string, unknown>;
|
|
3
|
+
}
|
|
4
|
+
export interface EntitySubscriberInterface {
|
|
5
|
+
beforeInsert?(event: InsertEvent): void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* TypeORM subscriber that injects tenant_id into inserted entities via ALS.
|
|
9
|
+
*
|
|
10
|
+
* Known limitation: TypeORM subscribers cannot intercept query filtering.
|
|
11
|
+
* This subscriber handles writes only. Use the shared-table adapter's
|
|
12
|
+
* structured methods for tenant-scoped reads.
|
|
13
|
+
*/
|
|
14
|
+
export declare class StratumTypeOrmSubscriber implements EntitySubscriberInterface {
|
|
15
|
+
/** Injects the current tenant's ID into the entity before insert. */
|
|
16
|
+
beforeInsert(event: InsertEvent): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=typeorm-subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typeorm-subscriber.d.ts","sourceRoot":"","sources":["../../src/integrations/typeorm-subscriber.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC,YAAY,CAAC,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED;;;;;;GAMG;AACH,qBAAa,wBAAyB,YAAW,yBAAyB;IACxE,qEAAqE;IACrE,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;CAKvC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StratumTypeOrmSubscriber = void 0;
|
|
4
|
+
const sdk_1 = require("@stratum-hq/sdk");
|
|
5
|
+
const utils_js_1 = require("../utils.js");
|
|
6
|
+
/**
|
|
7
|
+
* TypeORM subscriber that injects tenant_id into inserted entities via ALS.
|
|
8
|
+
*
|
|
9
|
+
* Known limitation: TypeORM subscribers cannot intercept query filtering.
|
|
10
|
+
* This subscriber handles writes only. Use the shared-table adapter's
|
|
11
|
+
* structured methods for tenant-scoped reads.
|
|
12
|
+
*/
|
|
13
|
+
class StratumTypeOrmSubscriber {
|
|
14
|
+
/** Injects the current tenant's ID into the entity before insert. */
|
|
15
|
+
beforeInsert(event) {
|
|
16
|
+
const context = (0, sdk_1.getTenantContext)();
|
|
17
|
+
(0, utils_js_1.assertTenantId)(context.tenant_id);
|
|
18
|
+
event.entity["tenant_id"] = context.tenant_id;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.StratumTypeOrmSubscriber = StratumTypeOrmSubscriber;
|
|
22
|
+
//# sourceMappingURL=typeorm-subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typeorm-subscriber.js","sourceRoot":"","sources":["../../src/integrations/typeorm-subscriber.ts"],"names":[],"mappings":";;;AAAA,yCAAmD;AACnD,0CAA6C;AAY7C;;;;;;GAMG;AACH,MAAa,wBAAwB;IACnC,qEAAqE;IACrE,YAAY,CAAC,KAAkB;QAC7B,MAAM,OAAO,GAAG,IAAA,sBAAgB,GAAE,CAAC;QACnC,IAAA,yBAAc,EAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAClC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;IAChD,CAAC;CACF;AAPD,4DAOC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MysqlPoolLike, MysqlPoolManagerOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Manages a collection of per-tenant MySQL connection pools with LRU eviction
|
|
4
|
+
* and active-query-aware reference counting.
|
|
5
|
+
*
|
|
6
|
+
* Pools are keyed by tenant slug and created on first access.
|
|
7
|
+
* When the pool count exceeds maxPools, the least-recently-used pool
|
|
8
|
+
* with refCount === 0 is evicted. A background timer closes pools
|
|
9
|
+
* that have been idle for longer than idleTimeoutMs.
|
|
10
|
+
*/
|
|
11
|
+
export declare class MysqlPoolManager {
|
|
12
|
+
private readonly pools;
|
|
13
|
+
private readonly pending;
|
|
14
|
+
private readonly createPool;
|
|
15
|
+
private readonly baseUri;
|
|
16
|
+
private readonly maxPools;
|
|
17
|
+
private readonly idleTimeoutMs;
|
|
18
|
+
private readonly idleTimer;
|
|
19
|
+
/** Exposed for testing: resolves when the last idle cleanup completes. */
|
|
20
|
+
_lastCleanup: Promise<void>;
|
|
21
|
+
constructor(options: MysqlPoolManagerOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Returns a cached MysqlPoolLike for the given tenant slug, creating one if needed.
|
|
24
|
+
* Increments refCount for the pool. Caller must call releasePool() when done.
|
|
25
|
+
* Evicts the LRU pool (with refCount === 0) if the pool map is at capacity.
|
|
26
|
+
*/
|
|
27
|
+
getPool(slug: string): Promise<MysqlPoolLike>;
|
|
28
|
+
/**
|
|
29
|
+
* Decrements the refCount for the pool associated with the given slug.
|
|
30
|
+
* No-op if the slug is not tracked.
|
|
31
|
+
*/
|
|
32
|
+
releasePool(slug: string): void;
|
|
33
|
+
/** Closes and removes the pool for the given slug. No-op if not found. */
|
|
34
|
+
closePool(slug: string): Promise<void>;
|
|
35
|
+
/** Closes all managed pools and stops the idle timer. Call during application shutdown. */
|
|
36
|
+
closeAll(): Promise<void>;
|
|
37
|
+
/** Returns a snapshot of current pool statistics. */
|
|
38
|
+
getStats(): {
|
|
39
|
+
poolCount: number;
|
|
40
|
+
};
|
|
41
|
+
/** Evicts the pool that has been idle the longest and has no active references. */
|
|
42
|
+
private evictLRU;
|
|
43
|
+
/** Closes pools that have been idle longer than idleTimeoutMs and have no active references. */
|
|
44
|
+
private closeIdlePools;
|
|
45
|
+
/** Builds the MySQL connection URI with the given database name. */
|
|
46
|
+
private buildUri;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=pool-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../src/pool-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AASzE;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAC1E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0D;IACrF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAC3D,0EAA0E;IAC1E,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAqB;gBAEpC,OAAO,EAAE,uBAAuB;IAgB5C;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAwCnD;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQ/B,0EAA0E;IACpE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C,2FAA2F;IACrF,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE;IAIjC,mFAAmF;YACrE,QAAQ;IAgBtB,gGAAgG;YAClF,cAAc;IAa5B,oEAAoE;IACpE,OAAO,CAAC,QAAQ;CAYjB"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MysqlPoolManager = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
/**
|
|
6
|
+
* Manages a collection of per-tenant MySQL connection pools with LRU eviction
|
|
7
|
+
* and active-query-aware reference counting.
|
|
8
|
+
*
|
|
9
|
+
* Pools are keyed by tenant slug and created on first access.
|
|
10
|
+
* When the pool count exceeds maxPools, the least-recently-used pool
|
|
11
|
+
* with refCount === 0 is evicted. A background timer closes pools
|
|
12
|
+
* that have been idle for longer than idleTimeoutMs.
|
|
13
|
+
*/
|
|
14
|
+
class MysqlPoolManager {
|
|
15
|
+
pools = new Map();
|
|
16
|
+
pending = new Map();
|
|
17
|
+
createPool;
|
|
18
|
+
baseUri;
|
|
19
|
+
maxPools;
|
|
20
|
+
idleTimeoutMs;
|
|
21
|
+
idleTimer;
|
|
22
|
+
/** Exposed for testing: resolves when the last idle cleanup completes. */
|
|
23
|
+
_lastCleanup = Promise.resolve();
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.createPool = options.createPool;
|
|
26
|
+
this.baseUri = options.baseUri;
|
|
27
|
+
this.maxPools = options.maxPools ?? 20;
|
|
28
|
+
this.idleTimeoutMs = options.idleTimeoutMs ?? 60_000;
|
|
29
|
+
this.idleTimer = setInterval(() => {
|
|
30
|
+
this._lastCleanup = this.closeIdlePools();
|
|
31
|
+
}, this.idleTimeoutMs);
|
|
32
|
+
// Allow Node.js to exit even if the timer is still running.
|
|
33
|
+
if (typeof this.idleTimer.unref === "function") {
|
|
34
|
+
this.idleTimer.unref();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns a cached MysqlPoolLike for the given tenant slug, creating one if needed.
|
|
39
|
+
* Increments refCount for the pool. Caller must call releasePool() when done.
|
|
40
|
+
* Evicts the LRU pool (with refCount === 0) if the pool map is at capacity.
|
|
41
|
+
*/
|
|
42
|
+
async getPool(slug) {
|
|
43
|
+
(0, core_1.validateSlug)(slug);
|
|
44
|
+
const existing = this.pools.get(slug);
|
|
45
|
+
if (existing) {
|
|
46
|
+
existing.lastUsed = Date.now();
|
|
47
|
+
existing.refCount++;
|
|
48
|
+
return existing.pool;
|
|
49
|
+
}
|
|
50
|
+
// Deduplicate concurrent creation requests for the same slug.
|
|
51
|
+
const inflight = this.pending.get(slug);
|
|
52
|
+
if (inflight) {
|
|
53
|
+
const pool = await inflight;
|
|
54
|
+
const entry = this.pools.get(slug);
|
|
55
|
+
if (entry) {
|
|
56
|
+
entry.refCount++;
|
|
57
|
+
}
|
|
58
|
+
return pool;
|
|
59
|
+
}
|
|
60
|
+
// Evict before adding so we never exceed maxPools.
|
|
61
|
+
if (this.pools.size >= this.maxPools) {
|
|
62
|
+
await this.evictLRU();
|
|
63
|
+
}
|
|
64
|
+
const dbName = `stratum_tenant_${slug}`;
|
|
65
|
+
const uri = this.buildUri(dbName);
|
|
66
|
+
const createPromise = Promise.resolve(this.createPool(uri));
|
|
67
|
+
this.pending.set(slug, createPromise);
|
|
68
|
+
try {
|
|
69
|
+
const pool = await createPromise;
|
|
70
|
+
this.pools.set(slug, { pool, lastUsed: Date.now(), refCount: 1 });
|
|
71
|
+
return pool;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
this.pending.delete(slug);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Decrements the refCount for the pool associated with the given slug.
|
|
79
|
+
* No-op if the slug is not tracked.
|
|
80
|
+
*/
|
|
81
|
+
releasePool(slug) {
|
|
82
|
+
const entry = this.pools.get(slug);
|
|
83
|
+
if (!entry)
|
|
84
|
+
return;
|
|
85
|
+
if (entry.refCount > 0) {
|
|
86
|
+
entry.refCount--;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Closes and removes the pool for the given slug. No-op if not found. */
|
|
90
|
+
async closePool(slug) {
|
|
91
|
+
const entry = this.pools.get(slug);
|
|
92
|
+
if (!entry)
|
|
93
|
+
return;
|
|
94
|
+
this.pools.delete(slug);
|
|
95
|
+
await entry.pool.end();
|
|
96
|
+
}
|
|
97
|
+
/** Closes all managed pools and stops the idle timer. Call during application shutdown. */
|
|
98
|
+
async closeAll() {
|
|
99
|
+
clearInterval(this.idleTimer);
|
|
100
|
+
const entries = Array.from(this.pools.entries());
|
|
101
|
+
this.pools.clear();
|
|
102
|
+
await Promise.all(entries.map(([, entry]) => entry.pool.end()));
|
|
103
|
+
}
|
|
104
|
+
/** Returns a snapshot of current pool statistics. */
|
|
105
|
+
getStats() {
|
|
106
|
+
return { poolCount: this.pools.size };
|
|
107
|
+
}
|
|
108
|
+
/** Evicts the pool that has been idle the longest and has no active references. */
|
|
109
|
+
async evictLRU() {
|
|
110
|
+
let oldestKey;
|
|
111
|
+
let oldestTime = Infinity;
|
|
112
|
+
for (const [key, entry] of this.pools.entries()) {
|
|
113
|
+
if (entry.refCount === 0 && entry.lastUsed < oldestTime) {
|
|
114
|
+
oldestTime = entry.lastUsed;
|
|
115
|
+
oldestKey = key;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (oldestKey !== undefined) {
|
|
119
|
+
await this.closePool(oldestKey);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Closes pools that have been idle longer than idleTimeoutMs and have no active references. */
|
|
123
|
+
async closeIdlePools() {
|
|
124
|
+
const cutoff = Date.now() - this.idleTimeoutMs;
|
|
125
|
+
const toClose = [];
|
|
126
|
+
for (const [key, entry] of this.pools.entries()) {
|
|
127
|
+
if (entry.refCount === 0 && entry.lastUsed < cutoff) {
|
|
128
|
+
toClose.push(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await Promise.all(toClose.map((slug) => this.closePool(slug)));
|
|
132
|
+
}
|
|
133
|
+
/** Builds the MySQL connection URI with the given database name. */
|
|
134
|
+
buildUri(dbName) {
|
|
135
|
+
try {
|
|
136
|
+
const url = new URL(this.baseUri);
|
|
137
|
+
// MySQL URIs use the pathname as the database name: mysql://host/dbname
|
|
138
|
+
url.pathname = `/${dbName}`;
|
|
139
|
+
return url.toString();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Fallback for non-standard URIs: replace trailing path segment.
|
|
143
|
+
const base = this.baseUri.replace(/\/[^/?]*(\?|$)/, `/${dbName}$1`);
|
|
144
|
+
return base;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.MysqlPoolManager = MysqlPoolManager;
|
|
149
|
+
//# sourceMappingURL=pool-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool-manager.js","sourceRoot":"","sources":["../src/pool-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface MysqlConnectionLike {
|
|
2
|
+
query(sql: string, values?: unknown[]): Promise<unknown>;
|
|
3
|
+
execute(sql: string, values?: unknown[]): Promise<unknown>;
|
|
4
|
+
release(): void;
|
|
5
|
+
end(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface MysqlPoolLike {
|
|
8
|
+
getConnection(): Promise<MysqlConnectionLike>;
|
|
9
|
+
query(sql: string, values?: unknown[]): Promise<unknown>;
|
|
10
|
+
end(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface MysqlAdapter {
|
|
13
|
+
purgeTenantData(tenantSlug: string): Promise<PurgeResult>;
|
|
14
|
+
getStats(): AdapterStats;
|
|
15
|
+
}
|
|
16
|
+
export interface PurgeResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
tablesProcessed: number;
|
|
19
|
+
rowsDeleted: number;
|
|
20
|
+
errors: Array<{
|
|
21
|
+
table: string;
|
|
22
|
+
error: Error;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export interface AdapterStats {
|
|
26
|
+
strategy: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface MysqlSharedAdapterOptions {
|
|
30
|
+
/** The MysqlPoolLike instance to use. */
|
|
31
|
+
pool: MysqlPoolLike;
|
|
32
|
+
/** The database name to use. */
|
|
33
|
+
databaseName: string;
|
|
34
|
+
}
|
|
35
|
+
export interface MysqlTableAdapterOptions {
|
|
36
|
+
/** The MysqlPoolLike instance to use. */
|
|
37
|
+
pool: MysqlPoolLike;
|
|
38
|
+
/** The database name to use. */
|
|
39
|
+
databaseName: string;
|
|
40
|
+
}
|
|
41
|
+
export interface MysqlDatabaseAdapterOptions {
|
|
42
|
+
/** Factory function to create a new pool for a connection URI. */
|
|
43
|
+
createPool: (uri: string) => MysqlPoolLike | Promise<MysqlPoolLike>;
|
|
44
|
+
/** Base connection URI (database name will be replaced per-tenant). */
|
|
45
|
+
baseUri: string;
|
|
46
|
+
/** Maximum number of cached pools. Default: 20. */
|
|
47
|
+
maxPools?: number;
|
|
48
|
+
/** Idle timeout in milliseconds. Default: 60000. */
|
|
49
|
+
idleTimeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface MysqlPoolManagerOptions {
|
|
52
|
+
/** Factory function to create a new pool for a connection URI. */
|
|
53
|
+
createPool: (uri: string) => MysqlPoolLike | Promise<MysqlPoolLike>;
|
|
54
|
+
/** Base connection URI. */
|
|
55
|
+
baseUri: string;
|
|
56
|
+
/** Maximum number of cached pools. Default: 20. */
|
|
57
|
+
maxPools?: number;
|
|
58
|
+
/** Idle timeout in milliseconds. Default: 60000. */
|
|
59
|
+
idleTimeoutMs?: number;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,OAAO,IAAI,IAAI,CAAC;IAChB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAID,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,QAAQ,IAAI,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID,MAAM,WAAW,yBAAyB;IACxC,yCAAyC;IACzC,IAAI,EAAE,aAAa,CAAC;IACpB,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,yCAAyC;IACzC,IAAI,EAAE,aAAa,CAAC;IACpB,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,kEAAkE;IAClE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACpE,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,kEAAkE;IAClE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACpE,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,2EAA2E"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PurgeResult } from "./types.js";
|
|
2
|
+
/** Validates that a tenantId is a non-empty string. */
|
|
3
|
+
export declare function assertTenantId(tenantId: string): void;
|
|
4
|
+
/**
|
|
5
|
+
* Backtick-escapes a MySQL identifier (table name, column name, database name).
|
|
6
|
+
* Any backticks inside the name are doubled as defense-in-depth.
|
|
7
|
+
* Note: validateSlug from @stratum-hq/core restricts slugs to [a-z][a-z0-9_]{0,62}
|
|
8
|
+
* so this escaping is an additional safety layer.
|
|
9
|
+
*/
|
|
10
|
+
export declare function escapeIdentifier(name: string): string;
|
|
11
|
+
/** Aggregates Promise.allSettled results into a PurgeResult. */
|
|
12
|
+
export declare function aggregatePurgeResults(results: PromiseSettledResult<{
|
|
13
|
+
table: string;
|
|
14
|
+
rowsDeleted: number;
|
|
15
|
+
}>[]): PurgeResult;
|
|
16
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMrD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,gEAAgE;AAChE,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,GACtE,WAAW,CA4Bb"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertTenantId = assertTenantId;
|
|
4
|
+
exports.escapeIdentifier = escapeIdentifier;
|
|
5
|
+
exports.aggregatePurgeResults = aggregatePurgeResults;
|
|
6
|
+
/** Validates that a tenantId is a non-empty string. */
|
|
7
|
+
function assertTenantId(tenantId) {
|
|
8
|
+
if (!tenantId || typeof tenantId !== "string") {
|
|
9
|
+
throw new Error(`Invalid tenantId: expected a non-empty string, got ${typeof tenantId === "string" ? '""' : String(tenantId)}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Backtick-escapes a MySQL identifier (table name, column name, database name).
|
|
14
|
+
* Any backticks inside the name are doubled as defense-in-depth.
|
|
15
|
+
* Note: validateSlug from @stratum-hq/core restricts slugs to [a-z][a-z0-9_]{0,62}
|
|
16
|
+
* so this escaping is an additional safety layer.
|
|
17
|
+
*/
|
|
18
|
+
function escapeIdentifier(name) {
|
|
19
|
+
return "`" + name.replace(/`/g, "``") + "`";
|
|
20
|
+
}
|
|
21
|
+
/** Aggregates Promise.allSettled results into a PurgeResult. */
|
|
22
|
+
function aggregatePurgeResults(results) {
|
|
23
|
+
let rowsDeleted = 0;
|
|
24
|
+
let tablesProcessed = 0;
|
|
25
|
+
const errors = [];
|
|
26
|
+
for (const result of results) {
|
|
27
|
+
if (result.status === "fulfilled") {
|
|
28
|
+
tablesProcessed++;
|
|
29
|
+
rowsDeleted += result.value.rowsDeleted;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const reason = result.reason;
|
|
33
|
+
if (reason instanceof Error) {
|
|
34
|
+
errors.push({ table: "unknown", error: reason });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
errors.push({
|
|
38
|
+
table: reason.table ?? "unknown",
|
|
39
|
+
error: reason.error ?? new Error(String(reason)),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
success: errors.length === 0,
|
|
46
|
+
tablesProcessed,
|
|
47
|
+
rowsDeleted,
|
|
48
|
+
errors,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;AAGA,wCAMC;AAQD,4CAEC;AAGD,sDA8BC;AAlDD,uDAAuD;AACvD,SAAgB,cAAc,CAAC,QAAgB;IAC7C,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,sDAAsD,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAC/G,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,IAAY;IAC3C,OAAO,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC;AAC9C,CAAC;AAED,gEAAgE;AAChE,SAAgB,qBAAqB,CACnC,OAAuE;IAEvE,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,MAAM,MAAM,GAA0B,EAAE,CAAC;IAEzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,eAAe,EAAE,CAAC;YAClB,WAAW,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,MAAM,CAAC,MAAmD,CAAC;YAC1E,IAAI,MAAM,YAAY,KAAK,EAAE,CAAC;gBAC5B,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC;oBACV,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,SAAS;oBAChC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;iBACjD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QAC5B,eAAe;QACf,WAAW;QACX,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MysqlPoolLike, MysqlConnectionLike } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates or replaces a tenant view that filters rows by a session variable.
|
|
4
|
+
*
|
|
5
|
+
* JSDoc caveat: Views are NOT a security boundary (unlike Postgres RLS).
|
|
6
|
+
* Queries to underlying tables bypass isolation. Views referencing session
|
|
7
|
+
* variables may not use indexes efficiently. Benchmark with >100k rows.
|
|
8
|
+
* Use the shared-table adapter for high-performance workloads.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createTenantView(pool: MysqlPoolLike, tableName: string, viewName?: string): Promise<void>;
|
|
11
|
+
/** Drops a tenant view if it exists. */
|
|
12
|
+
export declare function dropTenantView(pool: MysqlPoolLike, viewName: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Sets the session variable used by tenant views.
|
|
15
|
+
* Pass null to clear the variable after a request completes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setTenantSession(connection: MysqlConnectionLike, tenantId: string | null): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/views/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGtE;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wCAAwC;AACxC,wBAAsB,cAAc,CAClC,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,mBAAmB,EAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTenantView = createTenantView;
|
|
4
|
+
exports.dropTenantView = dropTenantView;
|
|
5
|
+
exports.setTenantSession = setTenantSession;
|
|
6
|
+
const utils_js_1 = require("../utils.js");
|
|
7
|
+
/**
|
|
8
|
+
* Creates or replaces a tenant view that filters rows by a session variable.
|
|
9
|
+
*
|
|
10
|
+
* JSDoc caveat: Views are NOT a security boundary (unlike Postgres RLS).
|
|
11
|
+
* Queries to underlying tables bypass isolation. Views referencing session
|
|
12
|
+
* variables may not use indexes efficiently. Benchmark with >100k rows.
|
|
13
|
+
* Use the shared-table adapter for high-performance workloads.
|
|
14
|
+
*/
|
|
15
|
+
async function createTenantView(pool, tableName, viewName) {
|
|
16
|
+
const resolvedViewName = viewName ?? `${tableName}_tenant_view`;
|
|
17
|
+
const escapedView = (0, utils_js_1.escapeIdentifier)(resolvedViewName);
|
|
18
|
+
const escapedTable = (0, utils_js_1.escapeIdentifier)(tableName);
|
|
19
|
+
const sql = `CREATE OR REPLACE VIEW ${escapedView} AS ` +
|
|
20
|
+
`SELECT * FROM ${escapedTable} WHERE tenant_id = @stratum_tenant_id`;
|
|
21
|
+
await pool.query(sql);
|
|
22
|
+
}
|
|
23
|
+
/** Drops a tenant view if it exists. */
|
|
24
|
+
async function dropTenantView(pool, viewName) {
|
|
25
|
+
const escapedView = (0, utils_js_1.escapeIdentifier)(viewName);
|
|
26
|
+
await pool.query(`DROP VIEW IF EXISTS ${escapedView}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Sets the session variable used by tenant views.
|
|
30
|
+
* Pass null to clear the variable after a request completes.
|
|
31
|
+
*/
|
|
32
|
+
async function setTenantSession(connection, tenantId) {
|
|
33
|
+
if (tenantId === null) {
|
|
34
|
+
await connection.execute(`SET @stratum_tenant_id = NULL`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
await connection.execute(`SET @stratum_tenant_id = ?`, [tenantId]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/views/manager.ts"],"names":[],"mappings":";;AAWA,4CAcC;AAGD,wCAMC;AAMD,4CASC;AAhDD,0CAA+C;AAE/C;;;;;;;GAOG;AACI,KAAK,UAAU,gBAAgB,CACpC,IAAmB,EACnB,SAAiB,EACjB,QAAiB;IAEjB,MAAM,gBAAgB,GAAG,QAAQ,IAAI,GAAG,SAAS,cAAc,CAAC;IAChE,MAAM,WAAW,GAAG,IAAA,2BAAgB,EAAC,gBAAgB,CAAC,CAAC;IACvD,MAAM,YAAY,GAAG,IAAA,2BAAgB,EAAC,SAAS,CAAC,CAAC;IAEjD,MAAM,GAAG,GACP,0BAA0B,WAAW,MAAM;QAC3C,iBAAiB,YAAY,uCAAuC,CAAC;IAEvE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,wCAAwC;AACjC,KAAK,UAAU,cAAc,CAClC,IAAmB,EACnB,QAAgB;IAEhB,MAAM,WAAW,GAAG,IAAA,2BAAgB,EAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,IAAI,CAAC,KAAK,CAAC,uBAAuB,WAAW,EAAE,CAAC,CAAC;AACzD,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,gBAAgB,CACpC,UAA+B,EAC/B,QAAuB;IAEvB,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,UAAU,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;IAC5D,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,CAAC,OAAO,CAAC,4BAA4B,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrE,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stratum-hq/mysql",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MySQL tenant isolation for Stratum — shared table, table-per-tenant, and database-per-tenant strategies",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
23
|
+
"lint": "tsc --noEmit",
|
|
24
|
+
"clean": "rm -rf dist"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@stratum-hq/core": "*",
|
|
28
|
+
"@stratum-hq/sdk": "*"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"mysql2": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"typeorm": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"knex": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"sequelize": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"mysql2": "^3.12.0",
|
|
46
|
+
"typescript": "^5.9.0",
|
|
47
|
+
"vitest": "^1.6.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|