@quiqflow-org/quiqflow-multi-tenants-utils 1.0.1 → 1.0.3
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.
|
@@ -11,6 +11,14 @@ export declare class TenantConnectionManager {
|
|
|
11
11
|
* Get a connection for a schema. Options:
|
|
12
12
|
* - forceNew: Close existing connection and create a fresh one (new DB pool)
|
|
13
13
|
* - skipCache: Don't use cached connection, always create new (but still cache result)
|
|
14
|
+
*
|
|
15
|
+
* CRITICAL FIX: ORM caching is DISABLED to prevent cross-tenant model contamination.
|
|
16
|
+
* The issue: Sequelize models are bound to a specific Sequelize instance. When ORMs
|
|
17
|
+
* are cached, models from one tenant's ORM can get used by another tenant, causing
|
|
18
|
+
* queries to go through the wrong Sequelize instance.
|
|
19
|
+
*
|
|
20
|
+
* By always creating fresh ORMs, each request gets models bound to the correct Sequelize.
|
|
21
|
+
* Connection pool management is handled at the Sequelize level (each Sequelize has its own pool).
|
|
14
22
|
*/
|
|
15
23
|
getConnection(schemaName: string, options?: {
|
|
16
24
|
forceNew?: boolean;
|
|
@@ -35,6 +43,12 @@ export declare class TenantConnectionManager {
|
|
|
35
43
|
/**
|
|
36
44
|
* Efficiently ensure schema isolation for reused connections
|
|
37
45
|
* Fixes Sequelize model schema caching issues in multi-tenant environment
|
|
46
|
+
*
|
|
47
|
+
* CRITICAL: We DO NOT set model._schema here because that causes Sequelize
|
|
48
|
+
* to generate fully qualified table names (schema.table) which bypasses
|
|
49
|
+
* PostgreSQL's search_path mechanism. Instead, we rely on search_path
|
|
50
|
+
* being set on the connection, and we REMOVE schema from models to force
|
|
51
|
+
* Sequelize to use unqualified names that resolve via search_path.
|
|
38
52
|
*/
|
|
39
53
|
private ensureSchemaIsolation;
|
|
40
54
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantConnectionManager.d.ts","sourceRoot":"","sources":["../src/TenantConnectionManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,SAAS,CAAC;AAIzD,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAwC;IAChE,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,YAAY,CAAC,CAAiD;IAEtE,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,8BAA8B,GAAG,uBAAuB;IAK1E,MAAM,CAAC,WAAW,IAAI,uBAAuB;IAM7C
|
|
1
|
+
{"version":3,"file":"TenantConnectionManager.d.ts","sourceRoot":"","sources":["../src/TenantConnectionManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,SAAS,CAAC;AAIzD,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAwC;IAChE,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,YAAY,CAAC,CAAiD;IAEtE,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,8BAA8B,GAAG,uBAAuB;IAK1E,MAAM,CAAC,WAAW,IAAI,uBAAuB;IAM7C;;;;;;;;;;;;OAYG;IACG,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GACpD,OAAO,CAAC,GAAG,CAAC;IA0Ef;;;OAGG;IACG,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAOzD;;;OAGG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5C;;;;OAIG;YACW,kBAAkB;IAmChC;;;;;;;;;OASG;YACW,qBAAqB;IA4CnC;;;OAGG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B5D;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C;;OAEG;IACH,gBAAgB,IAAI,MAAM,EAAE;IAI5B;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAwBhC"}
|
|
@@ -22,15 +22,26 @@ class TenantConnectionManager {
|
|
|
22
22
|
* Get a connection for a schema. Options:
|
|
23
23
|
* - forceNew: Close existing connection and create a fresh one (new DB pool)
|
|
24
24
|
* - skipCache: Don't use cached connection, always create new (but still cache result)
|
|
25
|
+
*
|
|
26
|
+
* CRITICAL FIX: ORM caching is DISABLED to prevent cross-tenant model contamination.
|
|
27
|
+
* The issue: Sequelize models are bound to a specific Sequelize instance. When ORMs
|
|
28
|
+
* are cached, models from one tenant's ORM can get used by another tenant, causing
|
|
29
|
+
* queries to go through the wrong Sequelize instance.
|
|
30
|
+
*
|
|
31
|
+
* By always creating fresh ORMs, each request gets models bound to the correct Sequelize.
|
|
32
|
+
* Connection pool management is handled at the Sequelize level (each Sequelize has its own pool).
|
|
25
33
|
*/
|
|
26
34
|
async getConnection(schemaName, options) {
|
|
27
|
-
const { forceNew = false
|
|
35
|
+
const { forceNew = false } = options || {};
|
|
36
|
+
// CRITICAL FIX: Always create fresh ORM to prevent model cross-contamination
|
|
37
|
+
// Previously, cached ORMs shared models that could be bound to wrong Sequelize instance
|
|
38
|
+
const skipCache = true; // Force skip cache for ALL requests
|
|
28
39
|
// If forceNew, close existing connection first to get fresh DB pool
|
|
29
40
|
if (forceNew && this.connections.has(schemaName)) {
|
|
30
41
|
console.log(`${LOG_PREFIX} Force closing existing connection for "${schemaName}" to create fresh DB pool`);
|
|
31
42
|
await this.removeConnection(schemaName);
|
|
32
43
|
}
|
|
33
|
-
// Check if we have a cached connection
|
|
44
|
+
// Check if we have a cached connection (only used for cleanup, not retrieval)
|
|
34
45
|
if (!skipCache && !forceNew && this.connections.has(schemaName)) {
|
|
35
46
|
const cachedOrm = this.connections.get(schemaName);
|
|
36
47
|
// Validate the cached connection - check if schema still exists
|
|
@@ -47,10 +58,35 @@ class TenantConnectionManager {
|
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
// Create a new connection with fresh DB pool
|
|
50
|
-
console.log(`${LOG_PREFIX} Creating
|
|
61
|
+
console.log(`${LOG_PREFIX} Creating FRESH ORM for "${schemaName}" (no caching - prevents model contamination)`);
|
|
51
62
|
const orm = await this.factory({ schemaName });
|
|
52
63
|
if (this.authenticate)
|
|
53
64
|
await this.authenticate(orm);
|
|
65
|
+
// CRITICAL: Store schema on Sequelize instance and connection pool immediately after creation
|
|
66
|
+
// This ensures that afterConnect hooks can access the correct schema
|
|
67
|
+
const sequelize = orm.sequelize;
|
|
68
|
+
sequelize._currentTenantSchema = schemaName;
|
|
69
|
+
// Store schema on connection pool so new connections use correct schema
|
|
70
|
+
const connectionManager = sequelize.connectionManager;
|
|
71
|
+
const pool = connectionManager === null || connectionManager === void 0 ? void 0 : connectionManager.pool;
|
|
72
|
+
if (pool) {
|
|
73
|
+
pool._tenantSchema = schemaName;
|
|
74
|
+
}
|
|
75
|
+
// Ensure schema isolation for the newly created connection
|
|
76
|
+
await this.ensureSchemaIsolation(orm, schemaName);
|
|
77
|
+
// Still track connections for cleanup purposes (but not for retrieval)
|
|
78
|
+
// Close any existing ORM for this schema to prevent connection pool buildup
|
|
79
|
+
if (this.connections.has(schemaName)) {
|
|
80
|
+
const oldOrm = this.connections.get(schemaName);
|
|
81
|
+
try {
|
|
82
|
+
if (oldOrm && oldOrm.sequelize && typeof oldOrm.sequelize.close === 'function') {
|
|
83
|
+
await oldOrm.sequelize.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.warn(`${LOG_PREFIX} Error closing old ORM for "${schemaName}":`, err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
54
90
|
this.connections.set(schemaName, orm);
|
|
55
91
|
return orm;
|
|
56
92
|
}
|
|
@@ -105,15 +141,45 @@ class TenantConnectionManager {
|
|
|
105
141
|
/**
|
|
106
142
|
* Efficiently ensure schema isolation for reused connections
|
|
107
143
|
* Fixes Sequelize model schema caching issues in multi-tenant environment
|
|
144
|
+
*
|
|
145
|
+
* CRITICAL: We DO NOT set model._schema here because that causes Sequelize
|
|
146
|
+
* to generate fully qualified table names (schema.table) which bypasses
|
|
147
|
+
* PostgreSQL's search_path mechanism. Instead, we rely on search_path
|
|
148
|
+
* being set on the connection, and we REMOVE schema from models to force
|
|
149
|
+
* Sequelize to use unqualified names that resolve via search_path.
|
|
108
150
|
*/
|
|
109
151
|
async ensureSchemaIsolation(orm, expectedSchema) {
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
152
|
+
// CRITICAL: Store schema on Sequelize instance and connection pool
|
|
153
|
+
// This allows hooks (afterConnect, beforeQuery) to access the correct schema
|
|
154
|
+
const sequelize = orm.sequelize;
|
|
155
|
+
sequelize._currentTenantSchema = expectedSchema;
|
|
156
|
+
// Store schema on connection pool so new connections use correct schema
|
|
157
|
+
const connectionManager = sequelize.connectionManager;
|
|
158
|
+
const pool = connectionManager === null || connectionManager === void 0 ? void 0 : connectionManager.pool;
|
|
159
|
+
if (pool) {
|
|
160
|
+
pool._tenantSchema = expectedSchema;
|
|
161
|
+
}
|
|
162
|
+
// Set search_path for this connection (PRIMARY mechanism for schema isolation)
|
|
163
|
+
await sequelize.query(`SET search_path TO "${expectedSchema}", public`);
|
|
164
|
+
// CRITICAL: REMOVE schema from models to force Sequelize to use search_path
|
|
165
|
+
// When model._schema is set, Sequelize ALWAYS generates fully qualified names
|
|
166
|
+
// (schema.table) which bypasses search_path. By removing it, Sequelize generates
|
|
167
|
+
// unqualified names that resolve via search_path.
|
|
114
168
|
Object.values(orm.sequelize.models).forEach((model) => {
|
|
115
|
-
|
|
116
|
-
|
|
169
|
+
// Remove schema from model to force Sequelize to rely on search_path
|
|
170
|
+
if (model._schema) {
|
|
171
|
+
delete model._schema;
|
|
172
|
+
}
|
|
173
|
+
// Also remove from model.options
|
|
174
|
+
if (model.options) {
|
|
175
|
+
model.options.schema = null;
|
|
176
|
+
}
|
|
177
|
+
// Clear any cached fully qualified table names
|
|
178
|
+
if (model.tableName && typeof model.tableName === 'string' && model.tableName.includes('.')) {
|
|
179
|
+
const parts = model.tableName.split('.');
|
|
180
|
+
if (parts.length === 2) {
|
|
181
|
+
model.tableName = parts[1];
|
|
182
|
+
}
|
|
117
183
|
}
|
|
118
184
|
});
|
|
119
185
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quiqflow-org/quiqflow-multi-tenants-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Shared multi-tenant helpers (schema resolution, connection management, middleware) for Quiqflow services.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|