@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;;;;OAIG;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;IA0Cf;;;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;;;OAGG;YACW,qBAAqB;IAgBnC;;;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"}
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, skipCache = false } = options || {};
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 new connection for "${schemaName}" (fresh DB pool)`);
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
- // Set search_path for this connection (lightweight operation)
111
- await orm.sequelize.query(`SET search_path TO "${expectedSchema}", public`);
112
- // Force all models to use the correct schema
113
- // Sequelize caches schema names in model definitions, so we need to update them
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
- if (model._schema !== expectedSchema) {
116
- model._schema = expectedSchema;
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.1",
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",