@quiqflow-org/quiqflow-multi-tenants-utils 1.0.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.
@@ -0,0 +1,114 @@
1
+ ---
2
+ noteId: "4752778074a811f081017bbaf3f56380"
3
+ tags: []
4
+
5
+ ---
6
+
7
+ # Contributing to Quiqflow common orm
8
+
9
+ We are thrilled that you'd like to contribute to this project! This document provides guidelines for contributing effectively and collaboratively.
10
+
11
+ ---
12
+
13
+ ## How to Contribute
14
+
15
+ ### 1. Report Issues
16
+
17
+ If you encounter a bug, have a feature request, or need clarification, please [open an issue](https://github.com/[REPO]/issues).
18
+
19
+ **When creating an issue:**
20
+
21
+ - Use the provided issue template.
22
+ - Provide a clear, descriptive title.
23
+ - Include steps to reproduce, if applicable.
24
+ - Add screenshots or logs if necessary.
25
+
26
+ ---
27
+
28
+ ### 2. Create Pull Requests (PRs)
29
+
30
+ #### Steps for Submitting a PR
31
+
32
+ 1. **Fork the repository** and create a new branch:
33
+
34
+ ```bash
35
+ git checkout -b feature/your-feature-name
36
+ ```
37
+
38
+ 2. **Make your changes** while following the project's coding standards.
39
+ 3. **Test your changes** thoroughly.
40
+ 4. **Commit your changes**:
41
+
42
+ ```bash
43
+ git commit -m "feat: Add description of the feature"
44
+ ```
45
+
46
+ Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format.
47
+ 5. **Push your branch** to your fork:
48
+
49
+ ```bash
50
+ git push origin feature/your-feature-name
51
+ ```
52
+
53
+ 6. **Open a Pull Request**:
54
+ - Provide a descriptive title and summary.
55
+ - Link any related issues (e.g., `Fixes #123`).
56
+ - Follow the PR template.
57
+
58
+ #### Code Review
59
+
60
+ - PRs will be reviewed by maintainers.
61
+ - Address requested changes promptly.
62
+ - Once approved, your changes will be merged.
63
+
64
+ ---
65
+
66
+ ## Code Guidelines
67
+
68
+ - Follow the coding standards defined in the [STYLE_GUIDE.md](./STYLE_GUIDE.md).
69
+ - Use meaningful variable and function names.
70
+ - Write comments where necessary to explain complex logic.
71
+ - Ensure that your code is tested and linted:
72
+
73
+ ```bash
74
+ yarn lint
75
+ yarn test
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Branching Strategy
81
+
82
+ - `main`: Stable production-ready code.
83
+ - `develop`: Latest development code.
84
+ - `feature/*`: Features or enhancements.
85
+ - `bugfix/*`: Bug fixes.
86
+
87
+ ---
88
+
89
+ ## Commit Message Format
90
+
91
+ Use the following structure for commit messages:
92
+
93
+ ```
94
+ <type>: <short description>
95
+
96
+ [Optional body explaining what and why.]
97
+ ```
98
+
99
+ **Types:**
100
+
101
+ - `feat`: New features
102
+ - `fix`: Bug fixes
103
+ - `docs`: Documentation updates
104
+ - `style`: Code style improvements
105
+ - `refactor`: Code changes without changing functionality
106
+ - `test`: Adding or updating tests
107
+
108
+ ---
109
+
110
+ ## Questions
111
+
112
+ If you have any questions, feel free to reach out by [creating an issue](https://github.com/quiqflow/quiqflow-multi-tenants-utils/issues) or contacting [mohammad@quiqflow.com].
113
+
114
+ Thank you for contributing to [Quiqflow]!
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Quiqflow-2.0
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ import { TenantCache } from "./cache";
2
+ import { AdminConnectionLike } from "./types";
3
+ export declare class SchemaResolutionService {
4
+ private admin;
5
+ private cache;
6
+ private static readonly MAX_RETRIES;
7
+ constructor(adminConnection: AdminConnectionLike, cache?: TenantCache);
8
+ preloadAllTenants(): Promise<void>;
9
+ private isActive;
10
+ resolveSchemaForTenant(tenantId: number): Promise<string>;
11
+ }
12
+ //# sourceMappingURL=SchemaResolutionService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchemaResolutionService.d.ts","sourceRoot":"","sources":["../src/SchemaResolutionService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAgB,MAAM,SAAS,CAAC;AAE5D,qBAAa,uBAAuB;IAClC,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAK;gBAGtC,eAAe,EAAE,mBAAmB,EACpC,KAAK,GAAE,WAAkC;IAMrC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAMxC,OAAO,CAAC,QAAQ;IAMV,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAsBhE"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchemaResolutionService = void 0;
4
+ const cache_1 = require("./cache");
5
+ class SchemaResolutionService {
6
+ constructor(adminConnection, cache = cache_1.TenantCache.instance) {
7
+ this.admin = adminConnection;
8
+ this.cache = cache;
9
+ }
10
+ async preloadAllTenants() {
11
+ const tenants = await this.admin.listAllTenants();
12
+ for (const t of tenants)
13
+ if (t.schemaName)
14
+ this.cache.set(String(t.tenantId), t.schemaName);
15
+ }
16
+ isActive(tenant) {
17
+ if (!tenant)
18
+ return false;
19
+ if (!tenant.status)
20
+ return true;
21
+ return tenant.status.toLowerCase() === "active";
22
+ }
23
+ async resolveSchemaForTenant(tenantId) {
24
+ const cacheKey = String(tenantId);
25
+ const cached = this.cache.get(cacheKey);
26
+ if (cached)
27
+ return cached;
28
+ let attempt = 0;
29
+ let lastError;
30
+ while (attempt <= SchemaResolutionService.MAX_RETRIES) {
31
+ try {
32
+ const record = await this.admin.getTenantById(tenantId);
33
+ if (!record || !record.schemaName)
34
+ throw new Error("Tenant not found");
35
+ if (!this.isActive(record))
36
+ throw new Error("Tenant inactive");
37
+ this.cache.set(cacheKey, record.schemaName);
38
+ return record.schemaName;
39
+ }
40
+ catch (e) {
41
+ lastError = e;
42
+ attempt++;
43
+ if (attempt > SchemaResolutionService.MAX_RETRIES)
44
+ break;
45
+ await new Promise((r) => setTimeout(r, 50 * attempt));
46
+ }
47
+ }
48
+ throw lastError || new Error("Failed resolving tenant schema");
49
+ }
50
+ }
51
+ exports.SchemaResolutionService = SchemaResolutionService;
52
+ SchemaResolutionService.MAX_RETRIES = 2;
@@ -0,0 +1,18 @@
1
+ import { TenantConnectionManagerOptions } from "./types";
2
+ export declare class TenantConnectionManager {
3
+ private static _instance;
4
+ private connections;
5
+ private factory;
6
+ private authenticate?;
7
+ private constructor();
8
+ static init(opts: TenantConnectionManagerOptions): TenantConnectionManager;
9
+ static getInstance(): TenantConnectionManager;
10
+ getConnection(schemaName: string): Promise<any>;
11
+ /**
12
+ * Efficiently ensure schema isolation for reused connections
13
+ * Fixes Sequelize model schema caching issues in multi-tenant environment
14
+ */
15
+ private ensureSchemaIsolation;
16
+ closeAll(): Promise<void>;
17
+ }
18
+ //# sourceMappingURL=TenantConnectionManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TenantConnectionManager.d.ts","sourceRoot":"","sources":["../src/TenantConnectionManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,SAAS,CAAC;AAEzD,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;IAMvC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAgBrD;;;OAGG;YACW,qBAAqB;IAgB7B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAUhC"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TenantConnectionManager = void 0;
4
+ class TenantConnectionManager {
5
+ constructor(opts) {
6
+ this.connections = new Map();
7
+ this.factory = opts.factory;
8
+ this.authenticate = opts.authenticate;
9
+ }
10
+ static init(opts) {
11
+ if (!this._instance)
12
+ this._instance = new TenantConnectionManager(opts);
13
+ return this._instance;
14
+ }
15
+ static getInstance() {
16
+ if (!this._instance)
17
+ throw new Error("TenantConnectionManager not initialized");
18
+ return this._instance;
19
+ }
20
+ async getConnection(schemaName) {
21
+ if (!this.connections.has(schemaName)) {
22
+ const orm = await this.factory({ schemaName });
23
+ if (this.authenticate)
24
+ await this.authenticate(orm);
25
+ this.connections.set(schemaName, orm);
26
+ }
27
+ const orm = this.connections.get(schemaName);
28
+ // CRITICAL: Ensure schema isolation when reusing connections
29
+ // This fixes Sequelize model schema caching issues in multi-tenant environment
30
+ await this.ensureSchemaIsolation(orm, schemaName);
31
+ return orm;
32
+ }
33
+ /**
34
+ * Efficiently ensure schema isolation for reused connections
35
+ * Fixes Sequelize model schema caching issues in multi-tenant environment
36
+ */
37
+ async ensureSchemaIsolation(orm, expectedSchema) {
38
+ // Set search_path for this connection (lightweight operation)
39
+ await orm.sequelize.query(`SET search_path TO "${expectedSchema}", public`);
40
+ // Force all models to use the correct schema
41
+ // Sequelize caches schema names in model definitions, so we need to update them
42
+ Object.values(orm.sequelize.models).forEach((model) => {
43
+ if (model._schema !== expectedSchema) {
44
+ model._schema = expectedSchema;
45
+ }
46
+ });
47
+ }
48
+ async closeAll() {
49
+ for (const [, orm] of this.connections) {
50
+ if (orm && typeof orm.close === "function") {
51
+ try {
52
+ await orm.close();
53
+ }
54
+ catch { }
55
+ }
56
+ }
57
+ this.connections.clear();
58
+ }
59
+ }
60
+ exports.TenantConnectionManager = TenantConnectionManager;
61
+ TenantConnectionManager._instance = null;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview Authentication and authorization middleware helpers
3
+ * @author QuiqFlow Team
4
+ */
5
+ import { Response } from "express";
6
+ import { TenantContextAugmentedRequest } from "./types";
7
+ /**
8
+ * Ensure user has admin role or send error response
9
+ * @param req Express request with tenant context
10
+ * @param res Express response
11
+ * @returns boolean indicating if validation passed
12
+ */
13
+ export declare const ensureAdmin: (req: TenantContextAugmentedRequest, res: Response) => boolean;
14
+ /**
15
+ * Ensure user has specific role or send error response
16
+ * @param req Express request with tenant context
17
+ * @param res Express response
18
+ * @param requiredRole Required role string
19
+ * @returns boolean indicating if validation passed
20
+ */
21
+ export declare const ensureRole: (req: TenantContextAugmentedRequest, res: Response, requiredRole: string) => boolean;
22
+ /**
23
+ * Ensure user has any of the specified roles
24
+ * @param req Express request with tenant context
25
+ * @param res Express response
26
+ * @param allowedRoles Array of allowed role strings
27
+ * @returns boolean indicating if validation passed
28
+ */
29
+ export declare const ensureAnyRole: (req: TenantContextAugmentedRequest, res: Response, allowedRoles: string[]) => boolean;
30
+ /**
31
+ * Ensure user is authenticated (has user object)
32
+ * @param req Express request with tenant context
33
+ * @param res Express response
34
+ * @returns boolean indicating if validation passed
35
+ */
36
+ export declare const ensureAuthenticated: (req: TenantContextAugmentedRequest, res: Response) => boolean;
37
+ /**
38
+ * Ensure tenant context is properly set
39
+ * @param req Express request with tenant context
40
+ * @param res Express response
41
+ * @returns boolean indicating if validation passed
42
+ */
43
+ export declare const ensureTenantContext: (req: TenantContextAugmentedRequest, res: Response) => boolean;
44
+ //# sourceMappingURL=authHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authHelpers.d.ts","sourceRoot":"","sources":["../src/authHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,6BAA6B,EAAE,MAAM,SAAS,CAAC;AAGxD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,KAAK,6BAA6B,EAClC,KAAK,QAAQ,KACZ,OAaF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,KAAK,6BAA6B,EAClC,KAAK,QAAQ,EACb,cAAc,MAAM,KACnB,OAeF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GACxB,KAAK,6BAA6B,EAClC,KAAK,QAAQ,EACb,cAAc,MAAM,EAAE,KACrB,OAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,KAAK,6BAA6B,EAClC,KAAK,QAAQ,KACZ,OAOF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,KAAK,6BAA6B,EAClC,KAAK,QAAQ,KACZ,OAcF,CAAC"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Authentication and authorization middleware helpers
4
+ * @author QuiqFlow Team
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ensureTenantContext = exports.ensureAuthenticated = exports.ensureAnyRole = exports.ensureRole = exports.ensureAdmin = void 0;
8
+ const tenantHelpers_1 = require("./tenantHelpers");
9
+ /**
10
+ * Ensure user has admin role or send error response
11
+ * @param req Express request with tenant context
12
+ * @param res Express response
13
+ * @returns boolean indicating if validation passed
14
+ */
15
+ const ensureAdmin = (req, res) => {
16
+ if (!req.user) {
17
+ res.status(401).json({ message: "Authentication required" });
18
+ return false;
19
+ }
20
+ if (!(0, tenantHelpers_1.isAdmin)(req)) {
21
+ res.status(403).json({ message: "Admin access required" });
22
+ return false;
23
+ }
24
+ console.log("User is admin:", req.user.role);
25
+ return true;
26
+ };
27
+ exports.ensureAdmin = ensureAdmin;
28
+ /**
29
+ * Ensure user has specific role or send error response
30
+ * @param req Express request with tenant context
31
+ * @param res Express response
32
+ * @param requiredRole Required role string
33
+ * @returns boolean indicating if validation passed
34
+ */
35
+ const ensureRole = (req, res, requiredRole) => {
36
+ if (!req.user) {
37
+ res.status(401).json({ message: "Authentication required" });
38
+ return false;
39
+ }
40
+ if (!(0, tenantHelpers_1.hasRole)(req, requiredRole)) {
41
+ res.status(403).json({
42
+ message: `${requiredRole} access required`,
43
+ userRole: req.user.role,
44
+ });
45
+ return false;
46
+ }
47
+ return true;
48
+ };
49
+ exports.ensureRole = ensureRole;
50
+ /**
51
+ * Ensure user has any of the specified roles
52
+ * @param req Express request with tenant context
53
+ * @param res Express response
54
+ * @param allowedRoles Array of allowed role strings
55
+ * @returns boolean indicating if validation passed
56
+ */
57
+ const ensureAnyRole = (req, res, allowedRoles) => {
58
+ if (!req.user) {
59
+ res.status(401).json({ message: "Authentication required" });
60
+ return false;
61
+ }
62
+ if (!(0, tenantHelpers_1.hasAnyRole)(req, allowedRoles)) {
63
+ res.status(403).json({
64
+ message: `Access denied. Required roles: ${allowedRoles.join(", ")}`,
65
+ userRole: req.user.role,
66
+ allowedRoles,
67
+ });
68
+ return false;
69
+ }
70
+ return true;
71
+ };
72
+ exports.ensureAnyRole = ensureAnyRole;
73
+ /**
74
+ * Ensure user is authenticated (has user object)
75
+ * @param req Express request with tenant context
76
+ * @param res Express response
77
+ * @returns boolean indicating if validation passed
78
+ */
79
+ const ensureAuthenticated = (req, res) => {
80
+ if (!req.user || !req.user.id) {
81
+ res.status(401).json({ message: "Authentication required" });
82
+ return false;
83
+ }
84
+ return true;
85
+ };
86
+ exports.ensureAuthenticated = ensureAuthenticated;
87
+ /**
88
+ * Ensure tenant context is properly set
89
+ * @param req Express request with tenant context
90
+ * @param res Express response
91
+ * @returns boolean indicating if validation passed
92
+ */
93
+ const ensureTenantContext = (req, res) => {
94
+ if (!req.schemaName) {
95
+ res.status(400).json({ message: "Tenant context missing" });
96
+ return false;
97
+ }
98
+ if (!req.tenantDb) {
99
+ res
100
+ .status(500)
101
+ .json({ message: "Tenant database connection not available" });
102
+ return false;
103
+ }
104
+ return true;
105
+ };
106
+ exports.ensureTenantContext = ensureTenantContext;
@@ -0,0 +1,10 @@
1
+ export declare class TenantCache {
2
+ private static _instance;
3
+ private cache;
4
+ private constructor();
5
+ static get instance(): TenantCache;
6
+ get(key: string): string | undefined;
7
+ set(key: string, value: string): void;
8
+ keys(): string[];
9
+ }
10
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA4B;IACpD,OAAO,CAAC,KAAK,CAAY;IAEzB,OAAO;IAIP,MAAM,KAAK,QAAQ,IAAI,WAAW,CAGjC;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,IAAI,IAAI,MAAM,EAAE;CAGjB"}
package/dist/cache.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TenantCache = void 0;
7
+ const node_cache_1 = __importDefault(require("node-cache"));
8
+ class TenantCache {
9
+ constructor() {
10
+ this.cache = new node_cache_1.default({ stdTTL: 0, useClones: false, checkperiod: 0 });
11
+ }
12
+ static get instance() {
13
+ if (!this._instance)
14
+ this._instance = new TenantCache();
15
+ return this._instance;
16
+ }
17
+ get(key) {
18
+ return this.cache.get(key);
19
+ }
20
+ set(key, value) {
21
+ this.cache.set(key, value);
22
+ }
23
+ keys() {
24
+ return this.cache.keys();
25
+ }
26
+ }
27
+ exports.TenantCache = TenantCache;
28
+ TenantCache._instance = null;
@@ -0,0 +1,8 @@
1
+ export * from "./types";
2
+ export * from "./cache";
3
+ export * from "./TenantConnectionManager";
4
+ export * from "./SchemaResolutionService";
5
+ export * from "./middleware";
6
+ export * from "./tenantHelpers";
7
+ export * from "./authHelpers";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./cache"), exports);
19
+ __exportStar(require("./TenantConnectionManager"), exports);
20
+ __exportStar(require("./SchemaResolutionService"), exports);
21
+ __exportStar(require("./middleware"), exports);
22
+ __exportStar(require("./tenantHelpers"), exports);
23
+ __exportStar(require("./authHelpers"), exports);
@@ -0,0 +1,4 @@
1
+ import { NextFunction, Response } from "express";
2
+ import { CreateTenantMiddlewareOptions, TenantContextAugmentedRequest } from "./types";
3
+ export declare const createTenantMiddleware: (options: CreateTenantMiddlewareOptions) => (req: TenantContextAugmentedRequest, res: Response, next: NextFunction) => Promise<void>;
4
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,SAAS,CAAC;AAIjB,eAAO,MAAM,sBAAsB,GACjC,SAAS,6BAA6B,MAWpC,KAAK,6BAA6B,EAClC,KAAK,QAAQ,EACb,MAAM,YAAY,KACjB,OAAO,CAAC,IAAI,CA2BhB,CAAC"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createTenantMiddleware = void 0;
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const defaultDecode = (token) => jsonwebtoken_1.default.decode(token);
9
+ const createTenantMiddleware = (options) => {
10
+ const { schemaResolutionService, connectionManager, decodeJwt = defaultDecode, headerTenantKey = "x-tenant-id", allowOptional = false, } = options;
11
+ return async (req, res, next) => {
12
+ try {
13
+ const tenantData = extractTenant(req, decodeJwt, headerTenantKey);
14
+ if (!(tenantData === null || tenantData === void 0 ? void 0 : tenantData.tenantId)) {
15
+ if (allowOptional)
16
+ return next();
17
+ res.status(400).json({ error: "Tenant ID is required." });
18
+ return;
19
+ }
20
+ const numericTenantId = parseInt(tenantData.tenantId, 10);
21
+ if (isNaN(numericTenantId) || numericTenantId <= 0) {
22
+ res.status(400).json({ error: "Invalid tenant ID format." });
23
+ return;
24
+ }
25
+ const schemaName = await schemaResolutionService.resolveSchemaForTenant(numericTenantId);
26
+ const tenantDb = await connectionManager.getConnection(schemaName);
27
+ req.tenantId = tenantData.tenantId;
28
+ req.schemaName = schemaName;
29
+ if (tenantData.user)
30
+ req.user = tenantData.user;
31
+ req.tenantDb = tenantDb;
32
+ next();
33
+ }
34
+ catch (e) {
35
+ console.error("[createTenantMiddleware] error", e);
36
+ res.status(400).json({ error: "Failed to resolve tenant schema." });
37
+ }
38
+ };
39
+ };
40
+ exports.createTenantMiddleware = createTenantMiddleware;
41
+ const extractTenant = (req, decodeJwt, headerTenantKey) => {
42
+ var _a, _b, _c, _d;
43
+ const authHeader = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.authorization;
44
+ if (authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith("Bearer ")) {
45
+ const token = authHeader.substring(7);
46
+ const decoded = decodeJwt(token);
47
+ if (decoded === null || decoded === void 0 ? void 0 : decoded.tenantId) {
48
+ return {
49
+ tenantId: String(decoded.tenantId),
50
+ user: {
51
+ tenantId: decoded.tenantId,
52
+ userId: decoded.userId || decoded.id,
53
+ ...decoded,
54
+ },
55
+ };
56
+ }
57
+ }
58
+ const headerTenant = (_b = req.headers) === null || _b === void 0 ? void 0 : _b[headerTenantKey];
59
+ if (headerTenant)
60
+ return { tenantId: String(headerTenant) };
61
+ if ((_c = req.query) === null || _c === void 0 ? void 0 : _c.tenantId)
62
+ return { tenantId: String(req.query.tenantId) };
63
+ if ((_d = req.body) === null || _d === void 0 ? void 0 : _d.tenantId)
64
+ return { tenantId: String(req.body.tenantId) };
65
+ return null;
66
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @fileoverview Tenant context helper functions for multi-tenant applications
3
+ * @author QuiqFlow Team
4
+ */
5
+ import { TenantContextAugmentedRequest } from "./types";
6
+ /**
7
+ * Returns resolved schema name from tenant middleware context.
8
+ */
9
+ export declare const getSchemaName: (req: TenantContextAugmentedRequest) => string;
10
+ /**
11
+ * Extract authenticated user id from request (supports id, sub, userId fields).
12
+ */
13
+ export declare const getTenantUserId: (req: TenantContextAugmentedRequest) => number;
14
+ /**
15
+ * Get tenant ID from request context
16
+ */
17
+ export declare const getTenantId: (req: TenantContextAugmentedRequest) => string | number;
18
+ /**
19
+ * Simple no-op validator kept for compatibility with previous validateUserSchema guard.
20
+ * Always returns true now that schema resolution is mandatory via middleware.
21
+ * For response-aware validation, use ensureTenantContext from authHelpers.
22
+ */
23
+ export declare const validateTenantContext: (_req: TenantContextAugmentedRequest) => boolean;
24
+ /**
25
+ * Simple boolean tenant context check (for compatibility)
26
+ * @param req Express request with tenant context
27
+ * @returns boolean indicating if context is valid
28
+ */
29
+ export declare const ensureTenantContextBoolean: (req: TenantContextAugmentedRequest) => boolean;
30
+ /**
31
+ * Check if the user has admin role
32
+ */
33
+ export declare const isAdmin: (req: TenantContextAugmentedRequest) => boolean;
34
+ /**
35
+ * Check if user has specific role
36
+ */
37
+ export declare const hasRole: (req: TenantContextAugmentedRequest, role: string) => boolean;
38
+ /**
39
+ * Get user roles as array (supports both single role string and roles array)
40
+ */
41
+ export declare const getUserRoles: (req: TenantContextAugmentedRequest) => string[];
42
+ /**
43
+ * Check if user has any of the specified roles
44
+ */
45
+ export declare const hasAnyRole: (req: TenantContextAugmentedRequest, roles: string[]) => boolean;
46
+ //# sourceMappingURL=tenantHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenantHelpers.d.ts","sourceRoot":"","sources":["../src/tenantHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,6BAA6B,EAAE,MAAM,SAAS,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,6BAA6B,KAAG,MAKlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,6BAA6B,KAAG,MAUpE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GACtB,KAAK,6BAA6B,KACjC,MAAM,GAAG,MAIX,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,6BAA6B,KAClC,OAAe,CAAC;AAEnB;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,GACrC,KAAK,6BAA6B,KACjC,OAEF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,6BAA6B,KAAG,OAClB,CAAC;AAE5C;;GAEG;AACH,eAAO,MAAM,OAAO,GAClB,KAAK,6BAA6B,EAClC,MAAM,MAAM,KACX,OAAiD,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,KAAK,6BAA6B,KAAG,MAAM,EAOvE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,UAAU,GACrB,KAAK,6BAA6B,EAClC,OAAO,MAAM,EAAE,KACd,OAGF,CAAC"}
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Tenant context helper functions for multi-tenant applications
4
+ * @author QuiqFlow Team
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.hasAnyRole = exports.getUserRoles = exports.hasRole = exports.isAdmin = exports.ensureTenantContextBoolean = exports.validateTenantContext = exports.getTenantId = exports.getTenantUserId = exports.getSchemaName = void 0;
8
+ /**
9
+ * Returns resolved schema name from tenant middleware context.
10
+ */
11
+ const getSchemaName = (req) => {
12
+ if (!req.schemaName)
13
+ throw new Error("Schema name missing from request context");
14
+ return req.schemaName;
15
+ };
16
+ exports.getSchemaName = getSchemaName;
17
+ /**
18
+ * Extract authenticated user id from request (supports id, sub, userId fields).
19
+ */
20
+ const getTenantUserId = (req) => {
21
+ var _a, _b, _c;
22
+ const raw = ((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) ||
23
+ ((_b = req.user) === null || _b === void 0 ? void 0 : _b.userId) ||
24
+ ((_c = req.user) === null || _c === void 0 ? void 0 : _c.sub);
25
+ if (!raw)
26
+ throw new Error("User id missing in request user payload");
27
+ const n = typeof raw === "string" ? parseInt(raw, 10) : raw;
28
+ if (isNaN(n))
29
+ throw new Error("User id is not numeric");
30
+ return n;
31
+ };
32
+ exports.getTenantUserId = getTenantUserId;
33
+ /**
34
+ * Get tenant ID from request context
35
+ */
36
+ const getTenantId = (req) => {
37
+ var _a;
38
+ if (req.tenantId)
39
+ return req.tenantId;
40
+ if ((_a = req.user) === null || _a === void 0 ? void 0 : _a.tenantId)
41
+ return req.user.tenantId;
42
+ throw new Error("Tenant ID missing from request context");
43
+ };
44
+ exports.getTenantId = getTenantId;
45
+ /**
46
+ * Simple no-op validator kept for compatibility with previous validateUserSchema guard.
47
+ * Always returns true now that schema resolution is mandatory via middleware.
48
+ * For response-aware validation, use ensureTenantContext from authHelpers.
49
+ */
50
+ const validateTenantContext = (_req) => true;
51
+ exports.validateTenantContext = validateTenantContext;
52
+ /**
53
+ * Simple boolean tenant context check (for compatibility)
54
+ * @param req Express request with tenant context
55
+ * @returns boolean indicating if context is valid
56
+ */
57
+ const ensureTenantContextBoolean = (req) => {
58
+ return !!(req.schemaName && req.tenantDb);
59
+ };
60
+ exports.ensureTenantContextBoolean = ensureTenantContextBoolean;
61
+ /**
62
+ * Check if the user has admin role
63
+ */
64
+ const isAdmin = (req) => !!(req.user && req.user.role === "admin");
65
+ exports.isAdmin = isAdmin;
66
+ /**
67
+ * Check if user has specific role
68
+ */
69
+ const hasRole = (req, role) => !!(req.user && req.user.role === role);
70
+ exports.hasRole = hasRole;
71
+ /**
72
+ * Get user roles as array (supports both single role string and roles array)
73
+ */
74
+ const getUserRoles = (req) => {
75
+ if (!req.user)
76
+ return [];
77
+ if (Array.isArray(req.user.roles))
78
+ return req.user.roles;
79
+ if (req.user.role)
80
+ return [req.user.role];
81
+ return [];
82
+ };
83
+ exports.getUserRoles = getUserRoles;
84
+ /**
85
+ * Check if user has any of the specified roles
86
+ */
87
+ const hasAnyRole = (req, roles) => {
88
+ const userRoles = (0, exports.getUserRoles)(req);
89
+ return roles.some((role) => userRoles.includes(role));
90
+ };
91
+ exports.hasAnyRole = hasAnyRole;
@@ -0,0 +1,52 @@
1
+ import { Request } from "express";
2
+ import { NextFunction, Response } from "express";
3
+ export interface TenantAwareUser {
4
+ id?: number | string;
5
+ userId?: number | string;
6
+ tenantId?: number | string;
7
+ role?: string;
8
+ [key: string]: any;
9
+ }
10
+ export interface TenantContextAugmentedRequest extends Request {
11
+ tenantId?: string | number;
12
+ schemaName?: string;
13
+ user?: TenantAwareUser;
14
+ tenantDb?: any;
15
+ }
16
+ export interface SchemaRecord {
17
+ tenantId: number;
18
+ schemaName: string;
19
+ status?: string;
20
+ [key: string]: any;
21
+ }
22
+ export interface SchemaResolutionAdapter {
23
+ listAllTenants(): Promise<SchemaRecord[]>;
24
+ getTenantById(tenantId: number): Promise<SchemaRecord | null>;
25
+ }
26
+ export interface AdminConnectionLike {
27
+ getTenantById(tenantId: number): Promise<SchemaRecord | null>;
28
+ listAllTenants(): Promise<SchemaRecord[]>;
29
+ }
30
+ export interface TenantConnectionFactoryArgs {
31
+ schemaName: string;
32
+ }
33
+ export type TenantOrmFactory = (args: TenantConnectionFactoryArgs) => Promise<any>;
34
+ export interface TenantConnectionManagerOptions {
35
+ factory: TenantOrmFactory;
36
+ authenticate?(orm: any): Promise<void>;
37
+ }
38
+ export interface CreateTenantMiddlewareOptions {
39
+ schemaResolutionService: SchemaResolutionServiceLike;
40
+ connectionManager: TenantConnectionManagerLike;
41
+ decodeJwt?(token: string): any;
42
+ headerTenantKey?: string;
43
+ allowOptional?: boolean;
44
+ }
45
+ export type TenantMiddleware = (req: TenantContextAugmentedRequest, res: Response, next: NextFunction) => Promise<void>;
46
+ export interface TenantConnectionManagerLike {
47
+ getConnection(schemaName: string): Promise<any>;
48
+ }
49
+ export interface SchemaResolutionServiceLike {
50
+ resolveSchemaForTenant(tenantId: number): Promise<string>;
51
+ }
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,6BAA8B,SAAQ,OAAO;IAC5D,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC1C,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC9D,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,2BAA2B,KAC9B,OAAO,CAAC,GAAG,CAAC,CAAC;AAElB,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,6BAA6B;IAC5C,uBAAuB,EAAE,2BAA2B,CAAC;IACrD,iBAAiB,EAAE,2BAA2B,CAAC;IAC/C,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,GAAG,EAAE,6BAA6B,EAClC,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,WAAW,2BAA2B;IAC1C,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,2BAA2B;IAC1C,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3D"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@quiqflow-org/quiqflow-multi-tenants-utils",
3
+ "version": "1.0.0",
4
+ "description": "Shared multi-tenant helpers (schema resolution, connection management, middleware) for Quiqflow services.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "lint": "eslint 'src/**/*.{ts,js}' || true",
11
+ "prepare": "npm run build",
12
+ "semantic-release": "semantic-release",
13
+ "test": "echo 'No tests yet'"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@github.com/Quiqflow-2-0/quiqflow-multi-tenants-utils.git"
21
+ },
22
+ "keywords": [
23
+ "multi-tenant",
24
+ "schema",
25
+ "middleware",
26
+ "quiqflow"
27
+ ],
28
+ "dependencies": {
29
+ "jsonwebtoken": "^9.0.2",
30
+ "@semantic-release/github": "^11.0.1",
31
+ "node-cache": "^5.1.2"
32
+ },
33
+ "peerDependencies": {
34
+ "express": ">=4",
35
+ "typescript": ">=5.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/express": "^4.17.21",
39
+ "@types/jsonwebtoken": "^9.0.7",
40
+ "@semantic-release/changelog": "^6.0.3",
41
+ "@semantic-release/exec": "^6.0.3",
42
+ "@semantic-release/git": "^10.0.1",
43
+ "@semantic-release/npm": "^11.0.2",
44
+ "semantic-release": "^22.0.12",
45
+ "@types/node": "^22.10.1",
46
+ "express": "^4.21.2",
47
+ "typescript": "^5.3.3"
48
+ }
49
+ }