@stratum-hq/mysql 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,12 @@ await adapter.purgeTenantData("tenant-a");
43
43
  ```typescript
44
44
  import { MysqlTableAdapter } from "@stratum-hq/mysql";
45
45
 
46
- const adapter = new MysqlTableAdapter({ pool, databaseName: "myapp" });
46
+ const adapter = new MysqlTableAdapter({
47
+ pool,
48
+ databaseName: "myapp",
49
+ // Every base table that has a per-tenant copy. Required by purgeTenantData.
50
+ baseTables: ["users", "orders"],
51
+ });
47
52
 
48
53
  // Returns escaped table name: `users_tenanta`
49
54
  const tableName = adapter.scopedTable("tenanta", "users");
@@ -79,12 +84,14 @@ await adapter.closeAll();
79
84
  ```typescript
80
85
  import { StratumTypeOrmSubscriber } from "@stratum-hq/mysql";
81
86
 
82
- // Add to your TypeORM data source subscribers
83
- const dataSource = new DataSource({
84
- subscribers: [StratumTypeOrmSubscriber],
85
- });
87
+ // Register an instance once the data source is initialized. (TypeORM's
88
+ // `subscribers` option only loads @EventSubscriber()-decorated classes.)
89
+ await dataSource.initialize();
90
+ dataSource.subscribers.push(new StratumTypeOrmSubscriber());
86
91
  ```
87
92
 
93
+ Inserts get the current tenant's `tenant_id`. Updates never change `tenant_id`: `save()` keeps the loaded value, and `update()` / query builder updates drop it from the SET values.
94
+
88
95
  **Limitation:** TypeORM subscribers can intercept writes but not reads. Use the shared-table adapter's structured methods for tenant-scoped reads.
89
96
 
90
97
  ### Knex Helper
@@ -93,49 +100,35 @@ const dataSource = new DataSource({
93
100
  import { withTenantScope } from "@stratum-hq/mysql";
94
101
 
95
102
  const tenantKnex = withTenantScope(knex, "tenant-a");
96
- const users = await tenantKnex("users").select("*");
97
- // Automatically adds: WHERE tenant_id = 'tenant-a'
103
+ const users = await tenantKnex("users").where("name", "like", q).orWhere("email", "like", q);
104
+ // Compiles to: WHERE tenant_id = 'tenant-a' AND (name LIKE ? OR email LIKE ?)
98
105
  ```
99
106
 
107
+ Your where clauses are always grouped after the tenant filter, including on clones and when the builder is used as a subquery. `insert()` sets `tenant_id`, `update()` never changes it, and `onConflict().merge()`, `upsert()` and `truncate()` throw.
108
+
100
109
  ### Sequelize Adapter
101
110
 
102
111
  ```typescript
103
112
  import { withMysqlTenantScope } from "@stratum-hq/mysql";
104
113
 
105
- await withMysqlTenantScope(sequelize, "tenant-a", async (scoped) => {
106
- // Session variable @stratum_tenant_id is set for this scope
114
+ await withMysqlTenantScope(sequelize, "tenant-a", async (scoped, transaction) => {
115
+ // @stratum_tenant_id is set on the transaction's connection only.
116
+ // Pass the transaction to every query, or it runs on another connection.
107
117
  // Guaranteed cleanup via try/finally, even on errors
108
- const users = await scoped.query("SELECT * FROM users_view");
118
+ const [rows] = await scoped.query("SELECT @stratum_tenant_id", { transaction });
109
119
  });
110
120
  ```
111
121
 
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
- ```
122
+ ## MySQL Views (not supported)
130
123
 
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.
124
+ `createTenantView()` is deprecated and always throws. MySQL does not allow a view to read a session variable (`ER_VIEW_SELECT_VARIABLE`), so a view filtered on `@stratum_tenant_id` cannot be created. Use the shared-table adapter's scoped methods. `setTenantSession()` and `dropTenantView()` remain available.
132
125
 
133
126
  ## GDPR Compliance
134
127
 
135
128
  All three adapters implement `purgeTenantData(tenantSlug)`:
136
129
 
137
130
  - **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`
131
+ - **Table-per-tenant**: `DROP TABLE` for exactly `{base}_{slug}` for each entry in the `baseTables` option, which it requires
139
132
  - **Database-per-tenant**: `DROP DATABASE stratum_tenant_slug`
140
133
 
141
134
  ## License
@@ -14,7 +14,11 @@ export declare class MysqlSharedAdapter implements MysqlAdapter {
14
14
  scopedSelect(tenantId: string, table: string, conditions?: Record<string, unknown>, columns?: string[]): Promise<unknown[]>;
15
15
  /** INSERT with tenant_id added to data. */
16
16
  scopedInsert(tenantId: string, table: string, data: Record<string, unknown>): Promise<unknown>;
17
- /** UPDATE SET ... WHERE tenant_id = ? AND conditions. */
17
+ /**
18
+ * UPDATE SET ... WHERE tenant_id = ? AND conditions.
19
+ * tenant_id (in any letter case) is dropped from data, so an update can never
20
+ * move a row to another tenant.
21
+ */
18
22
  scopedUpdate(tenantId: string, table: string, data: Record<string, unknown>, conditions: Record<string, unknown>): Promise<unknown>;
19
23
  /** DELETE WHERE tenant_id = ? AND optional conditions. */
20
24
  scopedDelete(tenantId: string, table: string, conditions?: Record<string, unknown>): Promise<unknown>;
@@ -1 +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;;;;;;OAMG;IACG,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAInE,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAuB7D,kCAAkC;IAClC,QAAQ,IAAI,YAAY;CAGzB"}
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;;;;OAIG;IACG,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;IA4BnB,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;;;;;;OAMG;IACG,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAInE,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAuB7D,kCAAkC;IAClC,QAAQ,IAAI,YAAY;CAGzB"}
@@ -44,9 +44,17 @@ class MysqlSharedAdapter {
44
44
  const sql = `INSERT INTO ${qualifiedTable} (${cols}) VALUES (${placeholders})`;
45
45
  return this.pool.query(sql, values);
46
46
  }
47
- /** UPDATE SET ... WHERE tenant_id = ? AND conditions. */
47
+ /**
48
+ * UPDATE SET ... WHERE tenant_id = ? AND conditions.
49
+ * tenant_id (in any letter case) is dropped from data, so an update can never
50
+ * move a row to another tenant.
51
+ */
48
52
  async scopedUpdate(tenantId, table, data, conditions) {
49
53
  (0, utils_js_1.assertTenantId)(tenantId);
54
+ data = Object.fromEntries(Object.entries(data).filter(([col]) => col.toLowerCase() !== "tenant_id"));
55
+ if (Object.keys(data).length === 0) {
56
+ throw new Error("scopedUpdate: data has no columns to update other than tenant_id");
57
+ }
50
58
  const qualifiedTable = `${(0, utils_js_1.escapeIdentifier)(this.databaseName)}.${(0, utils_js_1.escapeIdentifier)(table)}`;
51
59
  const setClauses = Object.keys(data)
52
60
  .map((col) => `${(0, utils_js_1.escapeIdentifier)(col)} = ?`)
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ import type { MysqlAdapter, MysqlTableAdapterOptions, MysqlPoolLike, PurgeResult
8
8
  export declare class MysqlTableAdapter implements MysqlAdapter {
9
9
  private readonly pool;
10
10
  private readonly databaseName;
11
+ private readonly baseTables;
11
12
  constructor(options: MysqlTableAdapterOptions);
12
13
  /**
13
14
  * Validates slug and returns the escaped tenant-scoped table name
@@ -16,6 +17,11 @@ export declare class MysqlTableAdapter implements MysqlAdapter {
16
17
  scopedTable(tenantSlug: string, baseTableName: string): string;
17
18
  /** Returns the underlying pool for raw queries against tenant tables. */
18
19
  getPool(): MysqlPoolLike;
20
+ /**
21
+ * Drops `{base}_{tenantSlug}` for every base in the baseTables option.
22
+ * Throws when baseTables is not set: discovering tables by name suffix would
23
+ * also match other tenants whose slug ends with this one.
24
+ */
19
25
  purgeTenantData(tenantSlug: string): Promise<PurgeResult>;
20
26
  /** Returns adapter statistics. */
21
27
  getStats(): AdapterStats;
@@ -1 +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"}
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;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;gBAEtC,OAAO,EAAE,wBAAwB;IAkB7C;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM;IAS9D,yEAAyE;IACzE,OAAO,IAAI,aAAa;IAIxB;;;;OAIG;IACG,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAgC/D,kCAAkC;IAClC,QAAQ,IAAI,YAAY;CAGzB"}
@@ -12,9 +12,21 @@ const utils_js_1 = require("../utils.js");
12
12
  class MysqlTableAdapter {
13
13
  pool;
14
14
  databaseName;
15
+ baseTables;
15
16
  constructor(options) {
16
17
  this.pool = options.pool;
17
18
  this.databaseName = options.databaseName;
19
+ if (options.baseTables) {
20
+ for (const a of options.baseTables) {
21
+ for (const b of options.baseTables) {
22
+ if (b.toLowerCase().startsWith(`${a.toLowerCase()}_`)) {
23
+ throw new Error(`MysqlTableAdapter: base tables "${a}" and "${b}" are ambiguous, ` +
24
+ `because "${b}_{slug}" can also be read as "${a}_{slug}"`);
25
+ }
26
+ }
27
+ }
28
+ this.baseTables = [...options.baseTables];
29
+ }
18
30
  }
19
31
  /**
20
32
  * Validates slug and returns the escaped tenant-scoped table name
@@ -22,6 +34,9 @@ class MysqlTableAdapter {
22
34
  */
23
35
  scopedTable(tenantSlug, baseTableName) {
24
36
  (0, core_1.validateSlug)(tenantSlug);
37
+ if (this.baseTables && !this.baseTables.includes(baseTableName)) {
38
+ throw new Error(`MysqlTableAdapter: "${baseTableName}" is not in baseTables`);
39
+ }
25
40
  const tableName = `${baseTableName}_${tenantSlug}`;
26
41
  return (0, utils_js_1.escapeIdentifier)(tableName);
27
42
  }
@@ -29,23 +44,23 @@ class MysqlTableAdapter {
29
44
  getPool() {
30
45
  return this.pool;
31
46
  }
47
+ /**
48
+ * Drops `{base}_{tenantSlug}` for every base in the baseTables option.
49
+ * Throws when baseTables is not set: discovering tables by name suffix would
50
+ * also match other tenants whose slug ends with this one.
51
+ */
32
52
  async purgeTenantData(tenantSlug) {
33
53
  (0, utils_js_1.assertTenantId)(tenantSlug);
34
54
  (0, core_1.validateSlug)(tenantSlug);
55
+ if (!this.baseTables) {
56
+ throw new Error("MysqlTableAdapter: purgeTenantData requires the baseTables option, " +
57
+ "listing every base table name that has a per-tenant copy");
58
+ }
35
59
  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
60
  const errors = [];
40
61
  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;
62
+ for (const base of this.baseTables) {
63
+ const tableName = `${base}_${tenantSlug}`;
49
64
  try {
50
65
  await this.pool.query(`DROP TABLE IF EXISTS ${escapedDb}.${(0, utils_js_1.escapeIdentifier)(tableName)}`);
51
66
  tablesProcessed++;
@@ -1 +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"}
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;IACrB,UAAU,CAAuB;IAElD,YAAY,OAAiC;QAC3C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;gBACnC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;oBACnC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC;wBACtD,MAAM,IAAI,KAAK,CACb,mCAAmC,CAAC,UAAU,CAAC,mBAAmB;4BAChE,YAAY,CAAC,iCAAiC,CAAC,UAAU,CAC5D,CAAC;oBACJ,CAAC;gBACH,CAAC;YACH,CAAC;YACD,IAAI,CAAC,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,UAAkB,EAAE,aAAqB;QACnD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QACzB,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,uBAAuB,aAAa,wBAAwB,CAAC,CAAC;QAChF,CAAC;QACD,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;;;;OAIG;IACH,KAAK,CAAC,eAAe,CAAC,UAAkB;QACtC,IAAA,yBAAc,EAAC,UAAU,CAAC,CAAC;QAC3B,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CACb,qEAAqE;gBACnE,0DAA0D,CAC7D,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,IAAA,2BAAgB,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACtD,MAAM,MAAM,GAA0B,EAAE,CAAC;QACzC,IAAI,eAAe,GAAG,CAAC,CAAC;QAExB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACnC,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC;YAC1C,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;AAlFD,8CAkFC"}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { MysqlDatabaseAdapter } from "./adapters/database.js";
5
5
  export { MysqlPoolManager } from "./pool-manager.js";
6
6
  export { createTenantView, dropTenantView, setTenantSession } from "./views/manager.js";
7
7
  export { StratumTypeOrmSubscriber } from "./integrations/typeorm-subscriber.js";
8
- export type { InsertEvent, EntitySubscriberInterface } from "./integrations/typeorm-subscriber.js";
8
+ export type { InsertEvent, UpdateEvent, EntitySubscriberInterface } from "./integrations/typeorm-subscriber.js";
9
9
  export { withTenantScope } from "./integrations/knex.js";
10
10
  export type { KnexLike, KnexQueryBuilderLike } from "./integrations/knex.js";
11
11
  export { withMysqlTenantScope } from "./integrations/sequelize.js";
@@ -1 +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"}
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,WAAW,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAChH,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"}
@@ -11,11 +11,21 @@ export interface KnexLike {
11
11
  }
12
12
  /**
13
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.
14
+ * Call the returned function with a table name to get a builder whose query
15
+ * always compiles to `WHERE tenant_id = ? AND (<your where clauses>)`.
16
+ *
17
+ * The caller's where clauses (including orWhere, whereRaw and clearWhere) are
18
+ * kept together in one parenthesized group, so they cannot widen the query
19
+ * past the tenant. This also holds for clones and for the builder used as a
20
+ * subquery.
15
21
  *
16
22
  * INSERT operations automatically inject tenant_id into the data object, so
17
23
  * you do not need to include it yourself:
18
24
  * await scoped("users").insert({ name: "Alice" });
25
+ *
26
+ * UPDATE never changes tenant_id: the column is dropped from the update data.
27
+ * onConflict().merge(), upsert() and truncate() throw, because MySQL applies
28
+ * none of them through the WHERE clause. onConflict().ignore() is allowed.
19
29
  */
20
30
  export declare function withTenantScope(knex: KnexLike, tenantId: string): (tableName: string) => KnexQueryBuilderLike;
21
31
  //# sourceMappingURL=knex.d.ts.map
@@ -1 +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,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpF,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,CAW7C"}
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,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpF,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;AA8BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,MAAM,GACf,CAAC,SAAS,EAAE,MAAM,KAAK,oBAAoB,CAwI7C"}
@@ -2,24 +2,162 @@
2
2
  // ─── Structural type for Knex (no hard dependency on knex) ───
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.withTenantScope = withTenantScope;
5
+ const TENANT_COLUMN = "tenant_id";
6
+ /** Builder methods that would write or destroy rows without the tenant filter. */
7
+ const REFUSED_METHODS = new Set(["upsert", "truncate"]);
8
+ function isTenantColumn(name) {
9
+ return name.toLowerCase() === TENANT_COLUMN;
10
+ }
11
+ function withoutTenantColumn(row) {
12
+ const copy = {};
13
+ for (const [key, value] of Object.entries(row)) {
14
+ if (!isTenantColumn(key))
15
+ copy[key] = value;
16
+ }
17
+ return copy;
18
+ }
5
19
  /**
6
20
  * 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.
21
+ * Call the returned function with a table name to get a builder whose query
22
+ * always compiles to `WHERE tenant_id = ? AND (<your where clauses>)`.
23
+ *
24
+ * The caller's where clauses (including orWhere, whereRaw and clearWhere) are
25
+ * kept together in one parenthesized group, so they cannot widen the query
26
+ * past the tenant. This also holds for clones and for the builder used as a
27
+ * subquery.
8
28
  *
9
29
  * INSERT operations automatically inject tenant_id into the data object, so
10
30
  * you do not need to include it yourself:
11
31
  * await scoped("users").insert({ name: "Alice" });
32
+ *
33
+ * UPDATE never changes tenant_id: the column is dropped from the update data.
34
+ * onConflict().merge(), upsert() and truncate() throw, because MySQL applies
35
+ * none of them through the WHERE clause. onConflict().ignore() is allowed.
12
36
  */
13
37
  function withTenantScope(knex, tenantId) {
14
- return (tableName) => {
15
- const builder = knex(tableName).where("tenant_id", tenantId);
16
- const originalInsert = builder.insert.bind(builder);
17
- builder.insert = (data) => {
18
- const inject = (row) => ({ ...row, tenant_id: tenantId });
19
- const scoped = Array.isArray(data) ? data.map(inject) : inject(data);
20
- return originalInsert(scoped);
38
+ // Statement objects this module created, so they can be recognized again on
39
+ // clones, which copy the statement array but share the statement objects.
40
+ const tenantStatements = new WeakSet();
41
+ const groupedStatements = new WeakMap();
42
+ function normalize(builder) {
43
+ const statements = builder._statements;
44
+ const others = [];
45
+ let userWheres = [];
46
+ for (const stmt of statements) {
47
+ if (stmt.grouping !== "where") {
48
+ others.push(stmt);
49
+ }
50
+ else if (tenantStatements.has(stmt)) {
51
+ continue;
52
+ }
53
+ else if (groupedStatements.has(stmt)) {
54
+ userWheres = [...userWheres, ...groupedStatements.get(stmt)];
55
+ }
56
+ else {
57
+ userWheres = [...userWheres, stmt];
58
+ }
59
+ }
60
+ const tenantStmt = {
61
+ grouping: "where",
62
+ type: "whereBasic",
63
+ column: TENANT_COLUMN,
64
+ operator: "=",
65
+ value: tenantId,
66
+ not: false,
67
+ bool: "and",
68
+ asColumn: false,
21
69
  };
22
- return builder;
23
- };
70
+ tenantStatements.add(tenantStmt);
71
+ const next = [...others, tenantStmt];
72
+ if (userWheres.length > 0) {
73
+ const captured = userWheres;
74
+ const groupStmt = {
75
+ grouping: "where",
76
+ type: "whereWrapped",
77
+ value: function () {
78
+ this._statements.push(...captured);
79
+ },
80
+ not: false,
81
+ bool: "and",
82
+ };
83
+ groupedStatements.set(groupStmt, captured);
84
+ next.push(groupStmt);
85
+ }
86
+ builder._statements = next;
87
+ }
88
+ function scope(target) {
89
+ // Knex methods call other methods on `this`. Only the outermost call is
90
+ // intercepted and normalized, so a statement array is never swapped out
91
+ // while a Knex method is still writing to it.
92
+ let depth = 0;
93
+ const proxy = new Proxy(target, {
94
+ get(obj, prop, receiver) {
95
+ const value = Reflect.get(obj, prop, receiver);
96
+ if (typeof value !== "function" || typeof prop !== "string" || prop === "constructor") {
97
+ return value;
98
+ }
99
+ if (REFUSED_METHODS.has(prop)) {
100
+ return () => {
101
+ throw new Error(`withTenantScope: ${prop}() is not allowed on a tenant-scoped builder, ` +
102
+ `because MySQL does not apply the tenant filter to it`);
103
+ };
104
+ }
105
+ if (depth > 0) {
106
+ return (...args) => value.apply(proxy, args);
107
+ }
108
+ if (prop === "onConflict") {
109
+ return (...args) => {
110
+ const onConflict = value.apply(proxy, args);
111
+ return {
112
+ ignore: () => onConflict.ignore(),
113
+ merge: () => {
114
+ throw new Error("withTenantScope: onConflict().merge() is not allowed on a tenant-scoped builder, " +
115
+ "because MySQL does not apply the tenant filter to it");
116
+ },
117
+ };
118
+ };
119
+ }
120
+ return (...args) => {
121
+ if (prop === "insert") {
122
+ const data = args[0];
123
+ const inject = (row) => ({ ...row, tenant_id: tenantId });
124
+ args[0] = Array.isArray(data) ? data.map(inject) : inject(data);
125
+ }
126
+ else if (prop === "update") {
127
+ if (typeof args[0] === "string") {
128
+ if (isTenantColumn(args[0])) {
129
+ throw new Error(`withTenantScope: update() cannot change ${TENANT_COLUMN}`);
130
+ }
131
+ }
132
+ else if (args[0] && typeof args[0] === "object") {
133
+ args[0] = withoutTenantColumn(args[0]);
134
+ }
135
+ }
136
+ else if (prop === "increment" || prop === "decrement") {
137
+ const cols = typeof args[0] === "string" ? [args[0]] : Object.keys((args[0] ?? {}));
138
+ if (cols.some(isTenantColumn)) {
139
+ throw new Error(`withTenantScope: ${prop}() cannot change ${TENANT_COLUMN}`);
140
+ }
141
+ }
142
+ normalize(obj);
143
+ let result;
144
+ depth++;
145
+ try {
146
+ result = value.apply(proxy, args);
147
+ }
148
+ finally {
149
+ depth--;
150
+ }
151
+ normalize(obj);
152
+ if (prop === "clone")
153
+ return scope(result);
154
+ return result === obj ? proxy : result;
155
+ };
156
+ },
157
+ });
158
+ normalize(target);
159
+ return proxy;
160
+ }
161
+ return (tableName) => scope(knex(tableName));
24
162
  }
25
163
  //# sourceMappingURL=knex.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"knex.js","sourceRoot":"","sources":["../../src/integrations/knex.ts"],"names":[],"mappings":";AAAA,gEAAgE;;AAuBhE,0CAcC;AAtBD;;;;;;;GAOG;AACH,SAAgB,eAAe,CAC7B,IAAc,EACd,QAAgB;IAEhB,OAAO,CAAC,SAAiB,EAAE,EAAE;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAC7D,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpD,OAAO,CAAC,MAAM,GAAG,CAAC,IAAyD,EAAE,EAAE;YAC7E,MAAM,MAAM,GAAG,CAAC,GAA4B,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;YACnF,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACrE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC,CAAC;QACF,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"knex.js","sourceRoot":"","sources":["../../src/integrations/knex.ts"],"names":[],"mappings":"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"}
@@ -4,9 +4,13 @@ export interface SequelizeLike {
4
4
  }
5
5
  /**
6
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.
7
+ * Sets @stratum_tenant_id inside a Sequelize transaction and clears it in a
8
+ * finally block, even if fn throws.
9
+ *
10
+ * The variable exists only on the transaction's connection. fn receives that
11
+ * transaction as its second argument, and every query inside fn must pass it
12
+ * (`{ transaction }`); a query without it runs on another pooled connection
13
+ * where the variable is not set.
10
14
  */
11
- export declare function withMysqlTenantScope<T>(sequelize: SequelizeLike, tenantId: string, fn: (sequelize: SequelizeLike) => Promise<T>): Promise<T>;
15
+ export declare function withMysqlTenantScope<T>(sequelize: SequelizeLike, tenantId: string, fn: (sequelize: SequelizeLike, transaction: unknown) => Promise<T>): Promise<T>;
12
16
  //# sourceMappingURL=sequelize.d.ts.map
@@ -1 +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"}
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;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,SAAS,EAAE,aAAa,EACxB,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GACjE,OAAO,CAAC,CAAC,CAAC,CAYZ"}
@@ -4,9 +4,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.withMysqlTenantScope = withMysqlTenantScope;
5
5
  /**
6
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.
7
+ * Sets @stratum_tenant_id inside a Sequelize transaction and clears it in a
8
+ * finally block, even if fn throws.
9
+ *
10
+ * The variable exists only on the transaction's connection. fn receives that
11
+ * transaction as its second argument, and every query inside fn must pass it
12
+ * (`{ transaction }`); a query without it runs on another pooled connection
13
+ * where the variable is not set.
10
14
  */
11
15
  async function withMysqlTenantScope(sequelize, tenantId, fn) {
12
16
  return sequelize.transaction(async (transaction) => {
@@ -15,7 +19,7 @@ async function withMysqlTenantScope(sequelize, tenantId, fn) {
15
19
  transaction,
16
20
  });
17
21
  try {
18
- return await fn(sequelize);
22
+ return await fn(sequelize, transaction);
19
23
  }
20
24
  finally {
21
25
  await sequelize.query("SET @stratum_tenant_id = NULL", { transaction });
@@ -1 +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"}
1
+ {"version":3,"file":"sequelize.js","sourceRoot":"","sources":["../../src/integrations/sequelize.ts"],"names":[],"mappings":";AAAA,6DAA6D;;AAiB7D,oDAgBC;AA1BD;;;;;;;;;GASG;AACI,KAAK,UAAU,oBAAoB,CACxC,SAAwB,EACxB,QAAgB,EAChB,EAAkE;IAElE,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,EAAE,WAAW,CAAC,CAAC;QAC1C,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"}
@@ -1,11 +1,29 @@
1
1
  export interface InsertEvent {
2
2
  entity: Record<string, unknown>;
3
3
  }
4
+ export interface UpdateEvent {
5
+ /** The entity being saved, or the SET values of a query builder / repository update. */
6
+ entity?: Record<string, unknown> | null;
7
+ /** The row as loaded from the database, when TypeORM has it (save()). */
8
+ databaseEntity?: Record<string, unknown> | null;
9
+ metadata?: {
10
+ columns: {
11
+ propertyName: string;
12
+ databaseName: string;
13
+ }[];
14
+ };
15
+ }
4
16
  export interface EntitySubscriberInterface {
5
17
  beforeInsert?(event: InsertEvent): void | Promise<void>;
18
+ beforeUpdate?(event: UpdateEvent): void | Promise<void>;
6
19
  }
7
20
  /**
8
- * TypeORM subscriber that injects tenant_id into inserted entities via ALS.
21
+ * TypeORM subscriber that injects tenant_id into inserted entities via ALS and
22
+ * never lets an update change tenant_id.
23
+ *
24
+ * Register an instance after the data source is initialized:
25
+ * `dataSource.subscribers.push(new StratumTypeOrmSubscriber())`. TypeORM's
26
+ * `subscribers` option only loads classes decorated with `@EventSubscriber()`.
9
27
  *
10
28
  * Known limitation: TypeORM subscribers cannot intercept query filtering.
11
29
  * This subscriber handles writes only. Use the shared-table adapter's
@@ -14,5 +32,12 @@ export interface EntitySubscriberInterface {
14
32
  export declare class StratumTypeOrmSubscriber implements EntitySubscriberInterface {
15
33
  /** Injects the current tenant's ID into the entity before insert. */
16
34
  beforeInsert(event: InsertEvent): void;
35
+ /**
36
+ * Keeps tenant_id out of an update. On save() the loaded value is restored,
37
+ * so the column is unchanged; on a repository or query builder update the
38
+ * column is dropped from the SET values (MySQL column names are
39
+ * case-insensitive, so any letter case is dropped).
40
+ */
41
+ beforeUpdate(event: UpdateEvent): void;
17
42
  }
18
43
  //# sourceMappingURL=typeorm-subscriber.d.ts.map
@@ -1 +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"}
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,WAAW;IAC1B,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CAC1E;AAED,MAAM,WAAW,yBAAyB;IACxC,YAAY,CAAC,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,YAAY,CAAC,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,YAAW,yBAAyB;IACxE,qEAAqE;IACrE,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAMtC;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;CAsBvC"}
@@ -4,7 +4,12 @@ exports.StratumTypeOrmSubscriber = void 0;
4
4
  const sdk_1 = require("@stratum-hq/sdk");
5
5
  const utils_js_1 = require("../utils.js");
6
6
  /**
7
- * TypeORM subscriber that injects tenant_id into inserted entities via ALS.
7
+ * TypeORM subscriber that injects tenant_id into inserted entities via ALS and
8
+ * never lets an update change tenant_id.
9
+ *
10
+ * Register an instance after the data source is initialized:
11
+ * `dataSource.subscribers.push(new StratumTypeOrmSubscriber())`. TypeORM's
12
+ * `subscribers` option only loads classes decorated with `@EventSubscriber()`.
8
13
  *
9
14
  * Known limitation: TypeORM subscribers cannot intercept query filtering.
10
15
  * This subscriber handles writes only. Use the shared-table adapter's
@@ -17,6 +22,32 @@ class StratumTypeOrmSubscriber {
17
22
  (0, utils_js_1.assertTenantId)(context.tenant_id);
18
23
  event.entity["tenant_id"] = context.tenant_id;
19
24
  }
25
+ /**
26
+ * Keeps tenant_id out of an update. On save() the loaded value is restored,
27
+ * so the column is unchanged; on a repository or query builder update the
28
+ * column is dropped from the SET values (MySQL column names are
29
+ * case-insensitive, so any letter case is dropped).
30
+ */
31
+ beforeUpdate(event) {
32
+ const entity = event.entity;
33
+ if (!entity)
34
+ return;
35
+ const tenantProps = new Set(Object.keys(entity).filter((key) => key.toLowerCase() === "tenant_id"));
36
+ for (const column of event.metadata?.columns ?? []) {
37
+ if (column.databaseName.toLowerCase() === "tenant_id" && column.propertyName in entity) {
38
+ tenantProps.add(column.propertyName);
39
+ }
40
+ }
41
+ for (const prop of tenantProps) {
42
+ const loaded = event.databaseEntity;
43
+ if (loaded && prop in loaded) {
44
+ entity[prop] = loaded[prop];
45
+ }
46
+ else {
47
+ delete entity[prop];
48
+ }
49
+ }
50
+ }
20
51
  }
21
52
  exports.StratumTypeOrmSubscriber = StratumTypeOrmSubscriber;
22
53
  //# sourceMappingURL=typeorm-subscriber.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"typeorm-subscriber.js","sourceRoot":"","sources":["../../src/integrations/typeorm-subscriber.ts"],"names":[],"mappings":";;;AAAA,yCAAmD;AACnD,0CAA6C;AAqB7C;;;;;;;;;;;GAWG;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;IAED;;;;;OAKG;IACH,YAAY,CAAC,KAAkB;QAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,MAAM,WAAW,GAAG,IAAI,GAAG,CACzB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,WAAW,CAAC,CACvE,CAAC;QACF,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;YACnD,IAAI,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE,KAAK,WAAW,IAAI,MAAM,CAAC,YAAY,IAAI,MAAM,EAAE,CAAC;gBACvF,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,CAAC;YACpC,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;gBAC7B,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,CAAC;iBAAM,CAAC;gBACN,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;CACF;AApCD,4DAoCC"}
package/dist/types.d.ts CHANGED
@@ -37,6 +37,14 @@ export interface MysqlTableAdapterOptions {
37
37
  pool: MysqlPoolLike;
38
38
  /** The database name to use. */
39
39
  databaseName: string;
40
+ /**
41
+ * Every base table name that has a per-tenant copy. Required by
42
+ * purgeTenantData, which drops exactly `{base}_{slug}` for each entry and
43
+ * nothing else. When set, scopedTable only accepts these base names.
44
+ * No entry may be another entry followed by `_`, since `{base}_{slug}` would
45
+ * then be ambiguous between two tenants.
46
+ */
47
+ baseTables?: string[];
40
48
  }
41
49
  export interface MysqlDatabaseAdapterOptions {
42
50
  /** Factory function to create a new pool for a connection URI. */
@@ -1 +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"}
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;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;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"}
@@ -1,13 +1,12 @@
1
1
  import type { MysqlPoolLike, MysqlConnectionLike } from "../types.js";
2
2
  /**
3
- * Creates or replaces a tenant view that filters rows by a session variable.
3
+ * @deprecated Not supported on MySQL, and always throws.
4
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.
5
+ * MySQL rejects a view whose SELECT reads a user variable
6
+ * (ER_VIEW_SELECT_VARIABLE), so a view filtered on `@stratum_tenant_id` cannot
7
+ * be created. Use the shared-table adapter's scoped methods instead.
9
8
  */
10
- export declare function createTenantView(pool: MysqlPoolLike, tableName: string, viewName?: string): Promise<void>;
9
+ export declare function createTenantView(_pool: MysqlPoolLike, _tableName: string, _viewName?: string): Promise<void>;
11
10
  /** Drops a tenant view if it exists. */
12
11
  export declare function dropTenantView(pool: MysqlPoolLike, viewName: string): Promise<void>;
13
12
  /**
@@ -1 +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"}
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;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAKf;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"}
@@ -5,20 +5,15 @@ exports.dropTenantView = dropTenantView;
5
5
  exports.setTenantSession = setTenantSession;
6
6
  const utils_js_1 = require("../utils.js");
7
7
  /**
8
- * Creates or replaces a tenant view that filters rows by a session variable.
8
+ * @deprecated Not supported on MySQL, and always throws.
9
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.
10
+ * MySQL rejects a view whose SELECT reads a user variable
11
+ * (ER_VIEW_SELECT_VARIABLE), so a view filtered on `@stratum_tenant_id` cannot
12
+ * be created. Use the shared-table adapter's scoped methods instead.
14
13
  */
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);
14
+ async function createTenantView(_pool, _tableName, _viewName) {
15
+ throw new Error("createTenantView is not supported: MySQL does not allow a view to read the " +
16
+ "@stratum_tenant_id session variable. Use MysqlSharedAdapter's scoped methods instead.");
22
17
  }
23
18
  /** Drops a tenant view if it exists. */
24
19
  async function dropTenantView(pool, viewName) {
@@ -1 +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"}
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/views/manager.ts"],"names":[],"mappings":";;AAUA,4CASC;AAGD,wCAMC;AAMD,4CASC;AA1CD,0CAA+C;AAE/C;;;;;;GAMG;AACI,KAAK,UAAU,gBAAgB,CACpC,KAAoB,EACpB,UAAkB,EAClB,SAAkB;IAElB,MAAM,IAAI,KAAK,CACb,6EAA6E;QAC3E,uFAAuF,CAC1F,CAAC;AACJ,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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratum-hq/mysql",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "MySQL tenant isolation for Stratum — shared table, table-per-tenant, and database-per-tenant strategies",
5
5
  "keywords": [
6
6
  "multi-tenancy",
@@ -42,7 +42,8 @@
42
42
  "@stratum-hq/sdk": "*"
43
43
  },
44
44
  "peerDependencies": {
45
- "mysql2": "^3.0.0"
45
+ "mysql2": "^3.0.0",
46
+ "knex": ">=3.0.0"
46
47
  },
47
48
  "peerDependenciesMeta": {
48
49
  "typeorm": {
@@ -56,9 +57,11 @@
56
57
  }
57
58
  },
58
59
  "devDependencies": {
59
- "mysql2": "^3.12.0",
60
+ "knex": "^3.3.0",
61
+ "mysql2": "^3.24.4",
62
+ "typeorm": "^1.1.1",
60
63
  "typescript": "^5.9.0",
61
- "vitest": "^1.6.0"
64
+ "vitest": "^4.1.11"
62
65
  },
63
66
  "publishConfig": {
64
67
  "access": "public"