@almatar/branding 1.0.0-beta.3.2 → 1.0.0-beta.3.4
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/lib/index.d.ts +5 -0
- package/lib/index.js +4 -0
- package/lib/lib/BrandIdentifier.d.ts +1 -0
- package/lib/lib/BrandIdentifier.js +46 -0
- package/lib/lib/JunctionTableBrandFilter.d.ts +70 -0
- package/lib/lib/JunctionTableBrandFilter.js +85 -0
- package/lib/lib/Storage.d.ts +1 -0
- package/lib/lib/Storage.js +100 -11
- package/lib/lib/TenantModel/MongooseModel.js +7 -0
- package/lib/lib/TenantModel/TypeORM/JunctionTableEntitySubscriber.d.ts +107 -0
- package/lib/lib/TenantModel/TypeORM/JunctionTableEntitySubscriber.js +175 -0
- package/lib/lib/TenantModel/TypeORM/JunctionTableTenantRepository.d.ts +66 -0
- package/lib/lib/TenantModel/TypeORM/JunctionTableTenantRepository.js +140 -0
- package/package.json +2 -1
package/lib/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { TenantEntitySubscriber } from './lib/TenantModel/TypeORM/TenantEntitySu
|
|
|
8
8
|
import { TenantHttpInterceptor } from './lib/TenantModel/TypeORM/TenantHttpInterceptor';
|
|
9
9
|
import { MongooseTenantHelper } from './lib/TenantModel/MongooseTenantHelper';
|
|
10
10
|
import { TenantMongooseRepository } from './lib/TenantModel/MongooseTenantRepository';
|
|
11
|
+
import { JunctionTableTenantRepository, IJunctionTableBrandConfig } from './lib/TenantModel/TypeORM/JunctionTableTenantRepository';
|
|
12
|
+
import { JunctionTableEntitySubscriber, IJunctionTableSyncConfig, IGenericJunctionTableConfig } from './lib/TenantModel/TypeORM/JunctionTableEntitySubscriber';
|
|
11
13
|
declare const _default: {
|
|
12
14
|
ContextNamespace: typeof ContextNamespace;
|
|
13
15
|
MultiTenant: typeof MultiTenant;
|
|
@@ -19,5 +21,8 @@ declare const _default: {
|
|
|
19
21
|
TenantHttpInterceptor: typeof TenantHttpInterceptor;
|
|
20
22
|
MongooseTenantHelper: typeof MongooseTenantHelper;
|
|
21
23
|
TenantMongooseRepository: typeof TenantMongooseRepository;
|
|
24
|
+
JunctionTableTenantRepository: typeof JunctionTableTenantRepository;
|
|
25
|
+
JunctionTableEntitySubscriber: typeof JunctionTableEntitySubscriber;
|
|
22
26
|
};
|
|
23
27
|
export = _default;
|
|
28
|
+
export type { IJunctionTableBrandConfig, IJunctionTableSyncConfig, IGenericJunctionTableConfig };
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,8 @@ const TenantEntitySubscriber_1 = require("./lib/TenantModel/TypeORM/TenantEntity
|
|
|
12
12
|
const TenantHttpInterceptor_1 = require("./lib/TenantModel/TypeORM/TenantHttpInterceptor");
|
|
13
13
|
const MongooseTenantHelper_1 = require("./lib/TenantModel/MongooseTenantHelper");
|
|
14
14
|
const MongooseTenantRepository_1 = require("./lib/TenantModel/MongooseTenantRepository");
|
|
15
|
+
const JunctionTableTenantRepository_1 = require("./lib/TenantModel/TypeORM/JunctionTableTenantRepository");
|
|
16
|
+
const JunctionTableEntitySubscriber_1 = require("./lib/TenantModel/TypeORM/JunctionTableEntitySubscriber");
|
|
15
17
|
module.exports = {
|
|
16
18
|
ContextNamespace: Storage_1.default,
|
|
17
19
|
MultiTenant: MultiTenant_1.default,
|
|
@@ -23,4 +25,6 @@ module.exports = {
|
|
|
23
25
|
TenantHttpInterceptor: TenantHttpInterceptor_1.TenantHttpInterceptor,
|
|
24
26
|
MongooseTenantHelper: MongooseTenantHelper_1.MongooseTenantHelper,
|
|
25
27
|
TenantMongooseRepository: MongooseTenantRepository_1.TenantMongooseRepository,
|
|
28
|
+
JunctionTableTenantRepository: JunctionTableTenantRepository_1.JunctionTableTenantRepository,
|
|
29
|
+
JunctionTableEntitySubscriber: JunctionTableEntitySubscriber_1.JunctionTableEntitySubscriber,
|
|
26
30
|
};
|
|
@@ -22,12 +22,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.BrandIdentifier = void 0;
|
|
23
23
|
const Boom = __importStar(require("@hapi/boom"));
|
|
24
24
|
const BrandManager_1 = require("./BrandManager");
|
|
25
|
+
const url = __importStar(require("url"));
|
|
25
26
|
class BrandIdentifier {
|
|
26
27
|
constructor(type) {
|
|
27
28
|
this.type = type;
|
|
28
29
|
}
|
|
29
30
|
async getBrand(req) {
|
|
31
|
+
var _a;
|
|
30
32
|
const brandManager = new BrandManager_1.BrandManager(this.type);
|
|
33
|
+
// Priority 1: Query parameter brand (super admin override)
|
|
34
|
+
const queryBrand = this.getQueryBrand(req);
|
|
35
|
+
// Troubleshoot logs (keep while integrating across microservices)
|
|
36
|
+
// tslint:disable-next-line no-console
|
|
37
|
+
console.log('[BrandIdentifier] Query brand detection:', {
|
|
38
|
+
'req.query': req === null || req === void 0 ? void 0 : req.query,
|
|
39
|
+
'req.query.brand': (_a = req === null || req === void 0 ? void 0 : req.query) === null || _a === void 0 ? void 0 : _a.brand,
|
|
40
|
+
'req.url': req === null || req === void 0 ? void 0 : req.url,
|
|
41
|
+
'req.originalUrl': req === null || req === void 0 ? void 0 : req.originalUrl,
|
|
42
|
+
'queryBrand (final)': queryBrand,
|
|
43
|
+
});
|
|
44
|
+
if (queryBrand) {
|
|
45
|
+
// tslint:disable-next-line no-console
|
|
46
|
+
console.log('[BrandIdentifier] Using query parameter brand (overriding token brands):', queryBrand);
|
|
47
|
+
// Return as array so ContextNamespace stores it as employeeBrands (used by filters)
|
|
48
|
+
return [queryBrand];
|
|
49
|
+
}
|
|
31
50
|
if (req.headers['x-brand']) {
|
|
32
51
|
// B2C scenario - return the brand from x-brand header
|
|
33
52
|
return brandManager.getB2CBrand(req);
|
|
@@ -36,12 +55,16 @@ class BrandIdentifier {
|
|
|
36
55
|
// Employee/Console scenario - extract from token or x-employee-brands header
|
|
37
56
|
const brand = await brandManager.getConsoleBrands(req);
|
|
38
57
|
if (brand && brand.length > 0) {
|
|
58
|
+
// tslint:disable-next-line no-console
|
|
59
|
+
console.log('[BrandIdentifier] Using console/token brands:', brand);
|
|
39
60
|
return brand;
|
|
40
61
|
}
|
|
41
62
|
// If no brand found but we have authorization, try extracting from token directly
|
|
42
63
|
if (req.headers.authorization) {
|
|
43
64
|
const tokenBrands = brandManager.extractBrandFromToken(req.headers.authorization);
|
|
44
65
|
if (tokenBrands && tokenBrands.length > 0) {
|
|
66
|
+
// tslint:disable-next-line no-console
|
|
67
|
+
console.log('[BrandIdentifier] Using brands extracted directly from token:', tokenBrands);
|
|
45
68
|
return tokenBrands;
|
|
46
69
|
}
|
|
47
70
|
}
|
|
@@ -78,6 +101,29 @@ class BrandIdentifier {
|
|
|
78
101
|
const brandManager = new BrandManager_1.BrandManager(this.type);
|
|
79
102
|
return brandManager.getDefaultBrand();
|
|
80
103
|
}
|
|
104
|
+
getQueryBrand(req) {
|
|
105
|
+
var _a, _b;
|
|
106
|
+
try {
|
|
107
|
+
// Express/NestJS typically populates req.query
|
|
108
|
+
const fromQuery = (_a = req === null || req === void 0 ? void 0 : req.query) === null || _a === void 0 ? void 0 : _a.brand;
|
|
109
|
+
if (typeof fromQuery === 'string' && fromQuery.trim()) {
|
|
110
|
+
return fromQuery.trim();
|
|
111
|
+
}
|
|
112
|
+
// Fallback to parsing URLs if req.query isn't populated yet
|
|
113
|
+
const urlToParse = (req === null || req === void 0 ? void 0 : req.originalUrl) || (req === null || req === void 0 ? void 0 : req.url);
|
|
114
|
+
if (typeof urlToParse === 'string' && urlToParse.includes('?')) {
|
|
115
|
+
const parsedUrl = url.parse(urlToParse, true);
|
|
116
|
+
const parsedBrand = (_b = parsedUrl.query) === null || _b === void 0 ? void 0 : _b.brand;
|
|
117
|
+
if (typeof parsedBrand === 'string' && parsedBrand.trim()) {
|
|
118
|
+
return parsedBrand.trim();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
// ignore - fall back to other brand sources
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
81
127
|
error(message) {
|
|
82
128
|
switch (this.type) {
|
|
83
129
|
case 'hapi':
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration interface for junction table filtering (internal use only)
|
|
3
|
+
* Not exported - use JunctionTableBrandConfig for automatic repository instead
|
|
4
|
+
*/
|
|
5
|
+
interface IJunctionTableFilterConfig {
|
|
6
|
+
/** Junction table name (e.g., 'role_brands', 'user_brands', 'permission_departments') */
|
|
7
|
+
junctionTableName: string;
|
|
8
|
+
/** Junction table alias for the query (e.g., 'rb', 'ub', 'pd') */
|
|
9
|
+
junctionTableAlias: string;
|
|
10
|
+
/** Foreign key column name in junction table that references the main entity (e.g., 'role_id', 'user_id', 'permission_id') */
|
|
11
|
+
foreignKeyColumn: string;
|
|
12
|
+
/** Primary key column name in the main entity table (usually 'id') */
|
|
13
|
+
primaryKeyColumn?: string;
|
|
14
|
+
/** Filter column name in junction table (e.g., 'brand', 'department_id') */
|
|
15
|
+
filterColumn: string;
|
|
16
|
+
/** Filter values to apply (e.g., brand names array or department IDs array) */
|
|
17
|
+
filterValues: string[] | number[];
|
|
18
|
+
/** Main entity table alias used in the query (e.g., 'role', 'employee', 'permission') */
|
|
19
|
+
entityAlias: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Helper class to apply brand/department filtering using junction tables (tenant scope only).
|
|
23
|
+
*
|
|
24
|
+
* This provides 10-100x faster queries compared to JSON_CONTAINS on JSON columns.
|
|
25
|
+
* Only uses tenant scope for brand filtering - no extra filtering.
|
|
26
|
+
*
|
|
27
|
+
* The tenant scope is automatically handled by ContextNamespace.getBrand() which
|
|
28
|
+
* retrieves brands from the request context (token/headers).
|
|
29
|
+
*
|
|
30
|
+
* This is an internal implementation used by JunctionTableTenantRepository.
|
|
31
|
+
* For automatic brand filtering, use JunctionTableTenantRepository instead.
|
|
32
|
+
*
|
|
33
|
+
* @internal - This class is for internal use only. Use JunctionTableTenantRepository for automatic filtering.
|
|
34
|
+
*/
|
|
35
|
+
export declare class JunctionTableBrandFilter {
|
|
36
|
+
/**
|
|
37
|
+
* Generic method to apply filtering using a junction table.
|
|
38
|
+
* This is the core method that all other methods use internally.
|
|
39
|
+
*
|
|
40
|
+
* @param query - The query builder to apply filtering to
|
|
41
|
+
* @param config - Configuration object specifying junction table structure
|
|
42
|
+
*/
|
|
43
|
+
static applyGenericFilter(query: any, config: IJunctionTableFilterConfig): void;
|
|
44
|
+
/**
|
|
45
|
+
* Generic method to apply brand filtering using a junction table.
|
|
46
|
+
* Automatically gets brands from context (tenant scope).
|
|
47
|
+
*
|
|
48
|
+
* @param query - The query builder to apply filtering to
|
|
49
|
+
* @param config - Configuration object (without filterValues, as it's auto-retrieved from context)
|
|
50
|
+
*/
|
|
51
|
+
static applyGenericBrandFilter(query: any, config: Omit<IJunctionTableFilterConfig, 'filterValues'>): void;
|
|
52
|
+
/**
|
|
53
|
+
* Generic method to apply numeric ID filtering using a junction table.
|
|
54
|
+
* Works for departments, categories, or any numeric ID-based filtering.
|
|
55
|
+
*
|
|
56
|
+
* @param query - The query builder to apply filtering to
|
|
57
|
+
* @param config - Configuration object with filterValues (departmentIds, categoryIds, etc.)
|
|
58
|
+
*/
|
|
59
|
+
static applyGenericNumericFilter(query: any, config: Omit<IJunctionTableFilterConfig, 'filterValues'> & {
|
|
60
|
+
filterValues: number[];
|
|
61
|
+
}): void;
|
|
62
|
+
/**
|
|
63
|
+
* Get brands from context (tenant scope is handled by ContextNamespace).
|
|
64
|
+
* Returns null if no brands are available or if user is almatar admin.
|
|
65
|
+
*
|
|
66
|
+
* @returns Array of brand strings or null if no filtering needed
|
|
67
|
+
*/
|
|
68
|
+
private static getBrandsFromContext;
|
|
69
|
+
}
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
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.JunctionTableBrandFilter = void 0;
|
|
7
|
+
const Storage_1 = __importDefault(require("./Storage"));
|
|
8
|
+
/**
|
|
9
|
+
* Helper class to apply brand/department filtering using junction tables (tenant scope only).
|
|
10
|
+
*
|
|
11
|
+
* This provides 10-100x faster queries compared to JSON_CONTAINS on JSON columns.
|
|
12
|
+
* Only uses tenant scope for brand filtering - no extra filtering.
|
|
13
|
+
*
|
|
14
|
+
* The tenant scope is automatically handled by ContextNamespace.getBrand() which
|
|
15
|
+
* retrieves brands from the request context (token/headers).
|
|
16
|
+
*
|
|
17
|
+
* This is an internal implementation used by JunctionTableTenantRepository.
|
|
18
|
+
* For automatic brand filtering, use JunctionTableTenantRepository instead.
|
|
19
|
+
*
|
|
20
|
+
* @internal - This class is for internal use only. Use JunctionTableTenantRepository for automatic filtering.
|
|
21
|
+
*/
|
|
22
|
+
class JunctionTableBrandFilter {
|
|
23
|
+
/**
|
|
24
|
+
* Generic method to apply filtering using a junction table.
|
|
25
|
+
* This is the core method that all other methods use internally.
|
|
26
|
+
*
|
|
27
|
+
* @param query - The query builder to apply filtering to
|
|
28
|
+
* @param config - Configuration object specifying junction table structure
|
|
29
|
+
*/
|
|
30
|
+
static applyGenericFilter(query, config) {
|
|
31
|
+
const { junctionTableName, junctionTableAlias, foreignKeyColumn, filterColumn, filterValues, entityAlias, primaryKeyColumn = 'id' } = config;
|
|
32
|
+
if (!filterValues || filterValues.length === 0) {
|
|
33
|
+
return; // No filtering needed
|
|
34
|
+
}
|
|
35
|
+
// Build the join condition
|
|
36
|
+
const joinCondition = `${junctionTableAlias}.${foreignKeyColumn} = ${entityAlias}.${primaryKeyColumn} AND ${junctionTableAlias}.${filterColumn} IN (:...filterValues)`;
|
|
37
|
+
// Use junction table for fast filtering
|
|
38
|
+
query.innerJoin(junctionTableName, junctionTableAlias, joinCondition, { filterValues });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generic method to apply brand filtering using a junction table.
|
|
42
|
+
* Automatically gets brands from context (tenant scope).
|
|
43
|
+
*
|
|
44
|
+
* @param query - The query builder to apply filtering to
|
|
45
|
+
* @param config - Configuration object (without filterValues, as it's auto-retrieved from context)
|
|
46
|
+
*/
|
|
47
|
+
static applyGenericBrandFilter(query, config) {
|
|
48
|
+
const brands = this.getBrandsFromContext();
|
|
49
|
+
if (!brands) {
|
|
50
|
+
return; // No brand filtering needed (no brands or almatar admin)
|
|
51
|
+
}
|
|
52
|
+
// Apply generic filter with brands from context
|
|
53
|
+
this.applyGenericFilter(query, Object.assign(Object.assign({}, config), { filterValues: brands }));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generic method to apply numeric ID filtering using a junction table.
|
|
57
|
+
* Works for departments, categories, or any numeric ID-based filtering.
|
|
58
|
+
*
|
|
59
|
+
* @param query - The query builder to apply filtering to
|
|
60
|
+
* @param config - Configuration object with filterValues (departmentIds, categoryIds, etc.)
|
|
61
|
+
*/
|
|
62
|
+
static applyGenericNumericFilter(query, config) {
|
|
63
|
+
this.applyGenericFilter(query, config);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get brands from context (tenant scope is handled by ContextNamespace).
|
|
67
|
+
* Returns null if no brands are available or if user is almatar admin.
|
|
68
|
+
*
|
|
69
|
+
* @returns Array of brand strings or null if no filtering needed
|
|
70
|
+
*/
|
|
71
|
+
static getBrandsFromContext() {
|
|
72
|
+
const contextEmployeeBrands = Storage_1.default.getEmployeeBrands();
|
|
73
|
+
const contextBrand = Storage_1.default.getBrand();
|
|
74
|
+
const brands = contextEmployeeBrands || (contextBrand ? [contextBrand] : null);
|
|
75
|
+
if (!brands || brands.length === 0) {
|
|
76
|
+
return null; // No brand filtering needed
|
|
77
|
+
}
|
|
78
|
+
// Check if user is almatar admin - bypass filtering
|
|
79
|
+
if (brands.includes('almatar')) {
|
|
80
|
+
return null; // Almatar admin can see all brands
|
|
81
|
+
}
|
|
82
|
+
return brands;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.JunctionTableBrandFilter = JunctionTableBrandFilter;
|
package/lib/lib/Storage.d.ts
CHANGED
package/lib/lib/Storage.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
2
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
22
|
const BrandIdentifier_1 = require("./BrandIdentifier");
|
|
23
|
+
const url = __importStar(require("url"));
|
|
4
24
|
const createNamespace = require('cls-hooked').createNamespace;
|
|
5
25
|
const namespaceName = 'request';
|
|
6
26
|
const ns = createNamespace(namespaceName);
|
|
@@ -16,19 +36,52 @@ class ContextNamespace {
|
|
|
16
36
|
});
|
|
17
37
|
}
|
|
18
38
|
static async setEBrand(req, res, next) {
|
|
39
|
+
var _a, _b, _c;
|
|
19
40
|
const brandIdentifier = new BrandIdentifier_1.BrandIdentifier('express');
|
|
20
41
|
const brand = await brandIdentifier.getBrand(req);
|
|
42
|
+
// Check if query parameter brand was used (allows super admin to query specific brand)
|
|
43
|
+
const queryBrand = ContextNamespace.getQueryBrand(req);
|
|
44
|
+
const isQueryParameterBrand = !!queryBrand;
|
|
45
|
+
// Troubleshoot logs (keep while integrating across microservices)
|
|
46
|
+
// tslint:disable-next-line no-console
|
|
47
|
+
console.log('[ContextNamespace] setEBrand request snapshot:', {
|
|
48
|
+
'req.url': req === null || req === void 0 ? void 0 : req.url,
|
|
49
|
+
'req.originalUrl': req === null || req === void 0 ? void 0 : req.originalUrl,
|
|
50
|
+
'req.query': req === null || req === void 0 ? void 0 : req.query,
|
|
51
|
+
'queryBrand (final)': queryBrand,
|
|
52
|
+
'hasAuthorization': !!((_a = req === null || req === void 0 ? void 0 : req.headers) === null || _a === void 0 ? void 0 : _a.authorization),
|
|
53
|
+
'incoming x-brand': (_b = req === null || req === void 0 ? void 0 : req.headers) === null || _b === void 0 ? void 0 : _b['x-brand'],
|
|
54
|
+
'incoming x-employee-brands': (_c = req === null || req === void 0 ? void 0 : req.headers) === null || _c === void 0 ? void 0 : _c['x-employee-brands'],
|
|
55
|
+
});
|
|
21
56
|
// Check if this is a B2C request (x-brand header) - don't set x-employee-brands for B2C
|
|
22
|
-
const isB2CRequest = !!req.headers['x-brand'];
|
|
23
|
-
// Check if this is an employee/console request (authorization token)
|
|
24
|
-
const isEmployeeRequest = !!req.headers.authorization || !!req.headers['x-employee-brands'];
|
|
57
|
+
const isB2CRequest = !!req.headers['x-brand'] && !isQueryParameterBrand;
|
|
58
|
+
// Check if this is an employee/console request (authorization token or query parameter brand)
|
|
59
|
+
const isEmployeeRequest = !!req.headers.authorization || !!req.headers['x-employee-brands'] || isQueryParameterBrand;
|
|
25
60
|
if (Array.isArray(brand)) {
|
|
26
|
-
// Employee/Console scenario with multiple brands
|
|
61
|
+
// Employee/Console scenario with multiple brands (or query parameter brand)
|
|
27
62
|
// tslint:disable-next-line no-console
|
|
28
|
-
console.log('[ContextNamespace] Setting employeeBrands in context:', brand);
|
|
63
|
+
console.log('[ContextNamespace] Setting employeeBrands in context:', brand, isQueryParameterBrand ? '(from query parameter)' : '');
|
|
64
|
+
// IMPORTANT: If query parameter brand is used, clear any existing brand context first
|
|
65
|
+
// This ensures query parameter brand takes absolute priority
|
|
66
|
+
if (isQueryParameterBrand) {
|
|
67
|
+
// Clear any existing brand context to ensure query parameter brand is used
|
|
68
|
+
ns.set('brand', null);
|
|
69
|
+
// tslint:disable-next-line no-console
|
|
70
|
+
console.log('[ContextNamespace] Cleared existing brand context to prioritize query parameter brand');
|
|
71
|
+
}
|
|
72
|
+
// IMPORTANT: Set employeeBrands in context - this is what MongooseModel pre-hooks will read
|
|
29
73
|
ns.set('employeeBrands', brand);
|
|
30
|
-
//
|
|
31
|
-
|
|
74
|
+
// Troubleshoot: verify it was set
|
|
75
|
+
// tslint:disable-next-line no-console
|
|
76
|
+
console.log('[ContextNamespace] employeeBrands in context now:', ns.get('employeeBrands'));
|
|
77
|
+
// If query parameter brand is used with authorization token, always set x-employee-brands header
|
|
78
|
+
if (isQueryParameterBrand && req.headers.authorization && brand.length > 0) {
|
|
79
|
+
req.headers['x-employee-brands'] = brand.join(',');
|
|
80
|
+
// tslint:disable-next-line no-console
|
|
81
|
+
console.log('[ContextNamespace] Set x-employee-brands from query brand (with token):', req.headers['x-employee-brands']);
|
|
82
|
+
}
|
|
83
|
+
else if (isEmployeeRequest && brand.length > 0) {
|
|
84
|
+
// Set x-employee-brands header for employee requests (normal flow)
|
|
32
85
|
req.headers['x-employee-brands'] = brand.join(',');
|
|
33
86
|
// tslint:disable-next-line no-console
|
|
34
87
|
console.log('[ContextNamespace] Setting x-employee-brands header on request:', req.headers['x-employee-brands']);
|
|
@@ -49,10 +102,18 @@ class ContextNamespace {
|
|
|
49
102
|
// tslint:disable-next-line no-console
|
|
50
103
|
console.log('[ContextNamespace] Setting brand in context (Employee):', brand);
|
|
51
104
|
ns.set('brand', brand);
|
|
52
|
-
//
|
|
53
|
-
req.headers
|
|
54
|
-
|
|
55
|
-
|
|
105
|
+
// If query parameter brand is used with authorization token, always set x-employee-brands header
|
|
106
|
+
if (isQueryParameterBrand && req.headers.authorization) {
|
|
107
|
+
req.headers['x-employee-brands'] = brand;
|
|
108
|
+
// tslint:disable-next-line no-console
|
|
109
|
+
console.log('[ContextNamespace] Set x-employee-brands from query brand (with token):', req.headers['x-employee-brands']);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Set x-employee-brands header for employee requests (normal flow)
|
|
113
|
+
req.headers['x-employee-brands'] = brand;
|
|
114
|
+
// tslint:disable-next-line no-console
|
|
115
|
+
console.log('[ContextNamespace] Setting x-employee-brands header on request:', req.headers['x-employee-brands']);
|
|
116
|
+
}
|
|
56
117
|
next();
|
|
57
118
|
}
|
|
58
119
|
else {
|
|
@@ -83,9 +144,16 @@ class ContextNamespace {
|
|
|
83
144
|
});
|
|
84
145
|
}
|
|
85
146
|
static async setHBrand(req, reply) {
|
|
147
|
+
var _a;
|
|
86
148
|
const brandIdentifier = new BrandIdentifier_1.BrandIdentifier('hapi');
|
|
87
149
|
const brand = await brandIdentifier.getBrand(req);
|
|
150
|
+
// Check if query parameter brand was used
|
|
151
|
+
const queryBrand = (_a = req.query) === null || _a === void 0 ? void 0 : _a.brand;
|
|
152
|
+
const isQueryParameterBrand = !!queryBrand;
|
|
88
153
|
if (Array.isArray(brand)) {
|
|
154
|
+
// Employee/Console scenario with multiple brands (or query parameter brand)
|
|
155
|
+
// tslint:disable-next-line no-console
|
|
156
|
+
console.log('[ContextNamespace] Setting employeeBrands in context (Hapi):', brand, isQueryParameterBrand ? '(from query parameter)' : '');
|
|
89
157
|
ns.set('employeeBrands', brand);
|
|
90
158
|
reply();
|
|
91
159
|
}
|
|
@@ -137,5 +205,26 @@ class ContextNamespace {
|
|
|
137
205
|
const brand = (_a = ns.get('brandsList')) === null || _a === void 0 ? void 0 : _a.filter((b) => b.key === brandKey)[0];
|
|
138
206
|
return (_b = brand.products) === null || _b === void 0 ? void 0 : _b.includes(product);
|
|
139
207
|
}
|
|
208
|
+
static getQueryBrand(req) {
|
|
209
|
+
var _a, _b;
|
|
210
|
+
try {
|
|
211
|
+
const fromQuery = (_a = req === null || req === void 0 ? void 0 : req.query) === null || _a === void 0 ? void 0 : _a.brand;
|
|
212
|
+
if (typeof fromQuery === 'string' && fromQuery.trim()) {
|
|
213
|
+
return fromQuery.trim();
|
|
214
|
+
}
|
|
215
|
+
const urlToParse = (req === null || req === void 0 ? void 0 : req.originalUrl) || (req === null || req === void 0 ? void 0 : req.url);
|
|
216
|
+
if (typeof urlToParse === 'string' && urlToParse.includes('?')) {
|
|
217
|
+
const parsedUrl = url.parse(urlToParse, true);
|
|
218
|
+
const parsedBrand = (_b = parsedUrl.query) === null || _b === void 0 ? void 0 : _b.brand;
|
|
219
|
+
if (typeof parsedBrand === 'string' && parsedBrand.trim()) {
|
|
220
|
+
return parsedBrand.trim();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
// ignore
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
140
229
|
}
|
|
141
230
|
exports.default = ContextNamespace;
|
|
@@ -38,6 +38,13 @@ class MongooseModel {
|
|
|
38
38
|
const employeeBrands = Storage_1.default.getEmployeeBrands();
|
|
39
39
|
const brand = Storage_1.default.getBrand();
|
|
40
40
|
const contextBrands = employeeBrands || (brand ? [brand] : null);
|
|
41
|
+
// Troubleshoot logs (keep while integrating across microservices)
|
|
42
|
+
// tslint:disable-next-line no-console
|
|
43
|
+
console.log('[MongooseModel] addPreCondition - Context:', {
|
|
44
|
+
employeeBrands,
|
|
45
|
+
brand,
|
|
46
|
+
contextBrands,
|
|
47
|
+
});
|
|
41
48
|
// Only apply filter if brands are explicitly set (not null/undefined)
|
|
42
49
|
if (contextBrands && contextBrands.length > 0) {
|
|
43
50
|
// Use $in to check if entity's brand is in the user's brands array
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for a generic junction table sync (for numeric IDs like departments, categories, etc.)
|
|
4
|
+
*/
|
|
5
|
+
export interface IGenericJunctionTableConfig {
|
|
6
|
+
/** Junction table name (e.g., 'user_departments', 'permission_categories') */
|
|
7
|
+
junctionTableName: string;
|
|
8
|
+
/** Foreign key column name in junction table (e.g., 'user_id', 'permission_id') */
|
|
9
|
+
foreignKeyColumn: string;
|
|
10
|
+
/** Filter column name in junction table (e.g., 'department_id', 'category_id') */
|
|
11
|
+
filterColumn: string;
|
|
12
|
+
/** Entity property name that contains the filter values (e.g., 'departmentId', 'categoryId') */
|
|
13
|
+
entityPropertyName: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for automatic junction table syncing
|
|
17
|
+
*/
|
|
18
|
+
export interface IJunctionTableSyncConfig {
|
|
19
|
+
/** Entity table name (e.g., 'users', 'roles') */
|
|
20
|
+
entityTableName: string;
|
|
21
|
+
/** Junction table name for brands (e.g., 'user_brands', 'role_brands') */
|
|
22
|
+
brandJunctionTable?: string;
|
|
23
|
+
/** Foreign key column name in brand junction table (e.g., 'user_id', 'role_id') */
|
|
24
|
+
brandForeignKeyColumn?: string;
|
|
25
|
+
/** Generic junction table configurations for numeric ID filters (departments, categories, etc.) */
|
|
26
|
+
genericJunctionTables?: IGenericJunctionTableConfig[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* TypeORM EntitySubscriber that AUTOMATICALLY syncs junction tables when entities are saved.
|
|
30
|
+
*
|
|
31
|
+
* This ensures that whenever you create or update a role/employee/user, the junction tables
|
|
32
|
+
* are automatically updated based on the brand/departmentId properties you provide.
|
|
33
|
+
*
|
|
34
|
+
* Configuration is done via a static map that maps entity table names to junction table configs.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { JunctionTableEntitySubscriber } from '@almatar/branding';
|
|
39
|
+
*
|
|
40
|
+
* // Configure junction tables for your entities
|
|
41
|
+
* JunctionTableEntitySubscriber.configure({
|
|
42
|
+
* entityTableName: 'roles',
|
|
43
|
+
* brandJunctionTable: 'role_brands',
|
|
44
|
+
* brandForeignKeyColumn: 'role_id',
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* JunctionTableEntitySubscriber.configure({
|
|
48
|
+
* entityTableName: 'users',
|
|
49
|
+
* brandJunctionTable: 'user_brands',
|
|
50
|
+
* brandForeignKeyColumn: 'user_id',
|
|
51
|
+
* genericJunctionTables: [
|
|
52
|
+
* {
|
|
53
|
+
* junctionTableName: 'user_departments',
|
|
54
|
+
* foreignKeyColumn: 'user_id',
|
|
55
|
+
* filterColumn: 'department_id',
|
|
56
|
+
* entityPropertyName: 'departmentId',
|
|
57
|
+
* },
|
|
58
|
+
* ],
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Register in DataSource
|
|
62
|
+
* export const dataSourceOptions: DataSourceOptions = {
|
|
63
|
+
* subscribers: [JunctionTableEntitySubscriber],
|
|
64
|
+
* };
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare class JunctionTableEntitySubscriber implements EntitySubscriberInterface {
|
|
68
|
+
/**
|
|
69
|
+
* Configure junction table syncing for an entity
|
|
70
|
+
*/
|
|
71
|
+
static configure(config: IJunctionTableSyncConfig): void;
|
|
72
|
+
/**
|
|
73
|
+
* Static configuration map: entity table name -> junction table config
|
|
74
|
+
*/
|
|
75
|
+
private static configMap;
|
|
76
|
+
/**
|
|
77
|
+
* Get configuration for an entity table
|
|
78
|
+
*/
|
|
79
|
+
private static getConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Called after an entity is inserted
|
|
82
|
+
* Automatically syncs junction tables if entity has brand/departmentId properties
|
|
83
|
+
*/
|
|
84
|
+
afterInsert(event: InsertEvent<any>): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Called after an entity is updated
|
|
87
|
+
* Automatically syncs junction tables if entity has brand/departmentId properties
|
|
88
|
+
*/
|
|
89
|
+
afterUpdate(event: UpdateEvent<any>): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Called after an entity is removed
|
|
92
|
+
* Automatically cleans up junction table entries
|
|
93
|
+
*/
|
|
94
|
+
afterRemove(event: RemoveEvent<any>): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Sync junction tables based on entity properties
|
|
97
|
+
*/
|
|
98
|
+
private syncJunctionTables;
|
|
99
|
+
/**
|
|
100
|
+
* Sync brand junction table
|
|
101
|
+
*/
|
|
102
|
+
private syncBrandJunctionTable;
|
|
103
|
+
/**
|
|
104
|
+
* Sync generic junction table for numeric ID filters (departments, categories, etc.)
|
|
105
|
+
*/
|
|
106
|
+
private syncGenericJunctionTable;
|
|
107
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var JunctionTableEntitySubscriber_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.JunctionTableEntitySubscriber = void 0;
|
|
11
|
+
const typeorm_1 = require("typeorm");
|
|
12
|
+
/**
|
|
13
|
+
* TypeORM EntitySubscriber that AUTOMATICALLY syncs junction tables when entities are saved.
|
|
14
|
+
*
|
|
15
|
+
* This ensures that whenever you create or update a role/employee/user, the junction tables
|
|
16
|
+
* are automatically updated based on the brand/departmentId properties you provide.
|
|
17
|
+
*
|
|
18
|
+
* Configuration is done via a static map that maps entity table names to junction table configs.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { JunctionTableEntitySubscriber } from '@almatar/branding';
|
|
23
|
+
*
|
|
24
|
+
* // Configure junction tables for your entities
|
|
25
|
+
* JunctionTableEntitySubscriber.configure({
|
|
26
|
+
* entityTableName: 'roles',
|
|
27
|
+
* brandJunctionTable: 'role_brands',
|
|
28
|
+
* brandForeignKeyColumn: 'role_id',
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* JunctionTableEntitySubscriber.configure({
|
|
32
|
+
* entityTableName: 'users',
|
|
33
|
+
* brandJunctionTable: 'user_brands',
|
|
34
|
+
* brandForeignKeyColumn: 'user_id',
|
|
35
|
+
* genericJunctionTables: [
|
|
36
|
+
* {
|
|
37
|
+
* junctionTableName: 'user_departments',
|
|
38
|
+
* foreignKeyColumn: 'user_id',
|
|
39
|
+
* filterColumn: 'department_id',
|
|
40
|
+
* entityPropertyName: 'departmentId',
|
|
41
|
+
* },
|
|
42
|
+
* ],
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Register in DataSource
|
|
46
|
+
* export const dataSourceOptions: DataSourceOptions = {
|
|
47
|
+
* subscribers: [JunctionTableEntitySubscriber],
|
|
48
|
+
* };
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
let JunctionTableEntitySubscriber = JunctionTableEntitySubscriber_1 = class JunctionTableEntitySubscriber {
|
|
52
|
+
/**
|
|
53
|
+
* Configure junction table syncing for an entity
|
|
54
|
+
*/
|
|
55
|
+
static configure(config) {
|
|
56
|
+
this.configMap.set(config.entityTableName, config);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get configuration for an entity table
|
|
60
|
+
*/
|
|
61
|
+
static getConfig(tableName) {
|
|
62
|
+
return this.configMap.get(tableName);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Called after an entity is inserted
|
|
66
|
+
* Automatically syncs junction tables if entity has brand/departmentId properties
|
|
67
|
+
*/
|
|
68
|
+
async afterInsert(event) {
|
|
69
|
+
await this.syncJunctionTables(event.entity, event.metadata.tableName, event.manager);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Called after an entity is updated
|
|
73
|
+
* Automatically syncs junction tables if entity has brand/departmentId properties
|
|
74
|
+
*/
|
|
75
|
+
async afterUpdate(event) {
|
|
76
|
+
if (event.entity) {
|
|
77
|
+
await this.syncJunctionTables(event.entity, event.metadata.tableName, event.manager);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Called after an entity is removed
|
|
82
|
+
* Automatically cleans up junction table entries
|
|
83
|
+
*/
|
|
84
|
+
async afterRemove(event) {
|
|
85
|
+
var _a, _b;
|
|
86
|
+
const config = JunctionTableEntitySubscriber_1.getConfig(event.metadata.tableName);
|
|
87
|
+
if (!config) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const entityId = ((_a = event.entity) === null || _a === void 0 ? void 0 : _a.id) || ((_b = event.databaseEntity) === null || _b === void 0 ? void 0 : _b.id);
|
|
91
|
+
if (!entityId) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Clean up brand junction table
|
|
95
|
+
if (config.brandJunctionTable && config.brandForeignKeyColumn) {
|
|
96
|
+
await event.manager.query(`DELETE FROM ${config.brandJunctionTable} WHERE ${config.brandForeignKeyColumn} = ?`, [entityId]);
|
|
97
|
+
}
|
|
98
|
+
// Clean up generic junction tables
|
|
99
|
+
if (config.genericJunctionTables) {
|
|
100
|
+
for (const genericConfig of config.genericJunctionTables) {
|
|
101
|
+
await event.manager.query(`DELETE FROM ${genericConfig.junctionTableName} WHERE ${genericConfig.foreignKeyColumn} = ?`, [entityId]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Sync junction tables based on entity properties
|
|
107
|
+
*/
|
|
108
|
+
async syncJunctionTables(entity, tableName, manager) {
|
|
109
|
+
const config = JunctionTableEntitySubscriber_1.getConfig(tableName);
|
|
110
|
+
if (!config) {
|
|
111
|
+
return; // No configuration for this entity
|
|
112
|
+
}
|
|
113
|
+
const entityId = entity === null || entity === void 0 ? void 0 : entity.id;
|
|
114
|
+
if (!entityId) {
|
|
115
|
+
return; // Entity doesn't have an ID yet
|
|
116
|
+
}
|
|
117
|
+
// Sync brand junction table if entity has brand property
|
|
118
|
+
if (config.brandJunctionTable && config.brandForeignKeyColumn && entity.brand) {
|
|
119
|
+
await this.syncBrandJunctionTable(manager, entityId, entity.brand, config.brandJunctionTable, config.brandForeignKeyColumn);
|
|
120
|
+
}
|
|
121
|
+
// Sync generic junction tables (departments, categories, etc.)
|
|
122
|
+
if (config.genericJunctionTables) {
|
|
123
|
+
for (const genericConfig of config.genericJunctionTables) {
|
|
124
|
+
const filterValues = entity[genericConfig.entityPropertyName];
|
|
125
|
+
if (filterValues) {
|
|
126
|
+
await this.syncGenericJunctionTable(manager, entityId, filterValues, genericConfig.junctionTableName, genericConfig.foreignKeyColumn, genericConfig.filterColumn);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Sync brand junction table
|
|
133
|
+
*/
|
|
134
|
+
async syncBrandJunctionTable(manager, entityId, brands, junctionTableName, foreignKeyColumn) {
|
|
135
|
+
// Normalize to array
|
|
136
|
+
const brandArray = Array.isArray(brands) ? brands : [brands].filter(Boolean);
|
|
137
|
+
// Delete existing entries
|
|
138
|
+
await manager.query(`DELETE FROM ${junctionTableName} WHERE ${foreignKeyColumn} = ?`, [entityId]);
|
|
139
|
+
// Insert new entries
|
|
140
|
+
if (brandArray.length > 0) {
|
|
141
|
+
const placeholders = brandArray.map(() => '(?, ?)').join(',');
|
|
142
|
+
const values = [];
|
|
143
|
+
brandArray.forEach((brand) => {
|
|
144
|
+
values.push(entityId, brand);
|
|
145
|
+
});
|
|
146
|
+
await manager.query(`INSERT INTO ${junctionTableName} (${foreignKeyColumn}, brand) VALUES ${placeholders}`, values);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sync generic junction table for numeric ID filters (departments, categories, etc.)
|
|
151
|
+
*/
|
|
152
|
+
async syncGenericJunctionTable(manager, entityId, filterValues, junctionTableName, foreignKeyColumn, filterColumn) {
|
|
153
|
+
// Normalize to array
|
|
154
|
+
const filterArray = Array.isArray(filterValues) ? filterValues : [filterValues].filter(Boolean);
|
|
155
|
+
// Delete existing entries
|
|
156
|
+
await manager.query(`DELETE FROM ${junctionTableName} WHERE ${foreignKeyColumn} = ?`, [entityId]);
|
|
157
|
+
// Insert new entries
|
|
158
|
+
if (filterArray.length > 0) {
|
|
159
|
+
const placeholders = filterArray.map(() => '(?, ?)').join(',');
|
|
160
|
+
const values = [];
|
|
161
|
+
filterArray.forEach((filterValue) => {
|
|
162
|
+
values.push(entityId, filterValue);
|
|
163
|
+
});
|
|
164
|
+
await manager.query(`INSERT INTO ${junctionTableName} (${foreignKeyColumn}, ${filterColumn}) VALUES ${placeholders}`, values);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Static configuration map: entity table name -> junction table config
|
|
170
|
+
*/
|
|
171
|
+
JunctionTableEntitySubscriber.configMap = new Map();
|
|
172
|
+
JunctionTableEntitySubscriber = JunctionTableEntitySubscriber_1 = __decorate([
|
|
173
|
+
typeorm_1.EventSubscriber()
|
|
174
|
+
], JunctionTableEntitySubscriber);
|
|
175
|
+
exports.JunctionTableEntitySubscriber = JunctionTableEntitySubscriber;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SelectQueryBuilder, ObjectLiteral } from 'typeorm';
|
|
2
|
+
import { TenantRepository } from './TenantRepository';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for junction table brand filtering
|
|
5
|
+
*/
|
|
6
|
+
export interface IJunctionTableBrandConfig {
|
|
7
|
+
/** Junction table name (e.g., 'role_brands', 'user_brands', 'hotel_brands') */
|
|
8
|
+
junctionTableName: string;
|
|
9
|
+
/** Junction table alias for the query (e.g., 'rb', 'ub', 'hb') */
|
|
10
|
+
junctionTableAlias: string;
|
|
11
|
+
/** Foreign key column name in junction table that references the main entity (e.g., 'role_id', 'user_id', 'hotel_id') */
|
|
12
|
+
foreignKeyColumn: string;
|
|
13
|
+
/** Primary key column name in the main entity table (usually 'id', defaults to 'id') */
|
|
14
|
+
primaryKeyColumn?: string;
|
|
15
|
+
/** Property name on entity to store brand array (defaults to 'brand') */
|
|
16
|
+
brandPropertyName?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Base Repository class for TypeORM that AUTOMATICALLY applies brand filtering using junction tables.
|
|
20
|
+
*
|
|
21
|
+
* This extends TenantRepository and automatically uses junction tables for 10-100x faster queries
|
|
22
|
+
* compared to JSON_CONTAINS. No code changes needed in service layer - just configure once!
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { JunctionTableTenantRepository } from '@almatar/branding';
|
|
27
|
+
*
|
|
28
|
+
* export class RoleRepository extends JunctionTableTenantRepository<RoleEntity> {
|
|
29
|
+
* constructor(@InjectRepository(RoleEntity) repository: Repository<RoleEntity>) {
|
|
30
|
+
* super(repository.target, repository.manager, repository.queryRunner, {
|
|
31
|
+
* junctionTableName: 'role_brands',
|
|
32
|
+
* junctionTableAlias: 'rb',
|
|
33
|
+
* foreignKeyColumn: 'role_id'
|
|
34
|
+
* });
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Brand filtering is automatically applied to ALL queries:
|
|
40
|
+
* - createQueryBuilder() - automatically adds junction table filter
|
|
41
|
+
* - find() - automatically filters by brand via junction table
|
|
42
|
+
* - findOne() - automatically filters by brand via junction table
|
|
43
|
+
* - findOneBy() - automatically filters by brand via junction table
|
|
44
|
+
* - findAndCount() - automatically filters by brand via junction table
|
|
45
|
+
*
|
|
46
|
+
* No need to call any methods - it just works automatically!
|
|
47
|
+
*/
|
|
48
|
+
export declare class JunctionTableTenantRepository<T extends ObjectLiteral> extends TenantRepository<T> {
|
|
49
|
+
protected readonly junctionTableConfig: IJunctionTableBrandConfig | null;
|
|
50
|
+
constructor(target: any, manager: any, queryRunner?: any, junctionTableConfig?: IJunctionTableBrandConfig);
|
|
51
|
+
/**
|
|
52
|
+
* Override createQueryBuilder to automatically apply junction table brand filtering
|
|
53
|
+
* Bypasses parent's JSON_CONTAINS filter since brand column doesn't exist (it's in junction tables)
|
|
54
|
+
* Also wraps getOne() and getMany() to automatically load brands from junction table
|
|
55
|
+
*/
|
|
56
|
+
createQueryBuilder(alias?: string): SelectQueryBuilder<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Apply brand filtering using junction table (automatic, no service code needed)
|
|
59
|
+
*/
|
|
60
|
+
private applyJunctionTableBrandFilter;
|
|
61
|
+
/**
|
|
62
|
+
* Automatically load brand data from junction table for entities
|
|
63
|
+
* This is called automatically after fetching entities via query builder - no manual code needed!
|
|
64
|
+
*/
|
|
65
|
+
private loadBrandsFromJunctionTable;
|
|
66
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JunctionTableTenantRepository = void 0;
|
|
4
|
+
const typeorm_1 = require("typeorm");
|
|
5
|
+
const TenantRepository_1 = require("./TenantRepository");
|
|
6
|
+
const JunctionTableBrandFilter_1 = require("../../JunctionTableBrandFilter");
|
|
7
|
+
/**
|
|
8
|
+
* Base Repository class for TypeORM that AUTOMATICALLY applies brand filtering using junction tables.
|
|
9
|
+
*
|
|
10
|
+
* This extends TenantRepository and automatically uses junction tables for 10-100x faster queries
|
|
11
|
+
* compared to JSON_CONTAINS. No code changes needed in service layer - just configure once!
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { JunctionTableTenantRepository } from '@almatar/branding';
|
|
16
|
+
*
|
|
17
|
+
* export class RoleRepository extends JunctionTableTenantRepository<RoleEntity> {
|
|
18
|
+
* constructor(@InjectRepository(RoleEntity) repository: Repository<RoleEntity>) {
|
|
19
|
+
* super(repository.target, repository.manager, repository.queryRunner, {
|
|
20
|
+
* junctionTableName: 'role_brands',
|
|
21
|
+
* junctionTableAlias: 'rb',
|
|
22
|
+
* foreignKeyColumn: 'role_id'
|
|
23
|
+
* });
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Brand filtering is automatically applied to ALL queries:
|
|
29
|
+
* - createQueryBuilder() - automatically adds junction table filter
|
|
30
|
+
* - find() - automatically filters by brand via junction table
|
|
31
|
+
* - findOne() - automatically filters by brand via junction table
|
|
32
|
+
* - findOneBy() - automatically filters by brand via junction table
|
|
33
|
+
* - findAndCount() - automatically filters by brand via junction table
|
|
34
|
+
*
|
|
35
|
+
* No need to call any methods - it just works automatically!
|
|
36
|
+
*/
|
|
37
|
+
class JunctionTableTenantRepository extends TenantRepository_1.TenantRepository {
|
|
38
|
+
constructor(target, manager, queryRunner, junctionTableConfig) {
|
|
39
|
+
super(target, manager, queryRunner);
|
|
40
|
+
this.junctionTableConfig = junctionTableConfig || null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Override createQueryBuilder to automatically apply junction table brand filtering
|
|
44
|
+
* Bypasses parent's JSON_CONTAINS filter since brand column doesn't exist (it's in junction tables)
|
|
45
|
+
* Also wraps getOne() and getMany() to automatically load brands from junction table
|
|
46
|
+
*/
|
|
47
|
+
createQueryBuilder(alias) {
|
|
48
|
+
// Call base TypeORM Repository.createQueryBuilder() directly to bypass parent's JSON_CONTAINS filter
|
|
49
|
+
// The parent's applyBrandFilter() uses JSON_CONTAINS on 'brand' column which doesn't exist anymore
|
|
50
|
+
const query = typeorm_1.Repository.prototype.createQueryBuilder.call(this, alias);
|
|
51
|
+
const tableAlias = alias || this.tableName;
|
|
52
|
+
// If junction table is configured, use it for fast filtering
|
|
53
|
+
if (this.junctionTableConfig) {
|
|
54
|
+
this.applyJunctionTableBrandFilter(query, tableAlias);
|
|
55
|
+
}
|
|
56
|
+
// Otherwise, no filtering (backward compatible - but junction table should always be configured)
|
|
57
|
+
// Wrap getOne() and getMany() to automatically load brands
|
|
58
|
+
if (this.junctionTableConfig) {
|
|
59
|
+
const originalGetOne = query.getOne.bind(query);
|
|
60
|
+
const originalGetMany = query.getMany.bind(query);
|
|
61
|
+
query.getOne = async () => {
|
|
62
|
+
const entity = await originalGetOne();
|
|
63
|
+
if (entity) {
|
|
64
|
+
await this.loadBrandsFromJunctionTable([entity]);
|
|
65
|
+
}
|
|
66
|
+
return entity;
|
|
67
|
+
};
|
|
68
|
+
query.getMany = async () => {
|
|
69
|
+
const entities = await originalGetMany();
|
|
70
|
+
if (entities.length > 0) {
|
|
71
|
+
await this.loadBrandsFromJunctionTable(entities);
|
|
72
|
+
}
|
|
73
|
+
return entities;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return query;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Apply brand filtering using junction table (automatic, no service code needed)
|
|
80
|
+
*/
|
|
81
|
+
applyJunctionTableBrandFilter(query, tableAlias) {
|
|
82
|
+
if (!this.junctionTableConfig) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { junctionTableName, junctionTableAlias, foreignKeyColumn, primaryKeyColumn = 'id', } = this.junctionTableConfig;
|
|
86
|
+
// Use the generic brand filter from JunctionTableBrandFilter
|
|
87
|
+
// This automatically gets brands from context (tenant scope)
|
|
88
|
+
JunctionTableBrandFilter_1.JunctionTableBrandFilter.applyGenericBrandFilter(query, {
|
|
89
|
+
junctionTableName,
|
|
90
|
+
junctionTableAlias,
|
|
91
|
+
foreignKeyColumn,
|
|
92
|
+
filterColumn: 'brand',
|
|
93
|
+
entityAlias: tableAlias,
|
|
94
|
+
primaryKeyColumn,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Automatically load brand data from junction table for entities
|
|
99
|
+
* This is called automatically after fetching entities via query builder - no manual code needed!
|
|
100
|
+
*/
|
|
101
|
+
async loadBrandsFromJunctionTable(entities) {
|
|
102
|
+
if (!this.junctionTableConfig || entities.length === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const { junctionTableName, foreignKeyColumn, primaryKeyColumn = 'id', brandPropertyName = 'brand', } = this.junctionTableConfig;
|
|
106
|
+
// Get all entity IDs
|
|
107
|
+
const entityIds = entities
|
|
108
|
+
.map((entity) => entity[primaryKeyColumn])
|
|
109
|
+
.filter((id) => id !== undefined && id !== null);
|
|
110
|
+
if (entityIds.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Load brands from junction table in a single query
|
|
114
|
+
const brandMappings = await this.manager
|
|
115
|
+
.createQueryBuilder()
|
|
116
|
+
.select(foreignKeyColumn, foreignKeyColumn)
|
|
117
|
+
.addSelect('brand', 'brand')
|
|
118
|
+
.from(junctionTableName, 'jt')
|
|
119
|
+
.where(`jt.${foreignKeyColumn} IN (:...entityIds)`, { entityIds })
|
|
120
|
+
.getRawMany();
|
|
121
|
+
// Group brands by entity ID
|
|
122
|
+
const brandsByEntityId = new Map();
|
|
123
|
+
brandMappings.forEach((mapping) => {
|
|
124
|
+
const entityId = mapping[foreignKeyColumn];
|
|
125
|
+
const brand = mapping.brand;
|
|
126
|
+
if (entityId && brand) {
|
|
127
|
+
if (!brandsByEntityId.has(entityId)) {
|
|
128
|
+
brandsByEntityId.set(entityId, []);
|
|
129
|
+
}
|
|
130
|
+
brandsByEntityId.get(entityId).push(brand);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// Assign brands to entities
|
|
134
|
+
entities.forEach((entity) => {
|
|
135
|
+
const entityId = entity[primaryKeyColumn];
|
|
136
|
+
entity[brandPropertyName] = brandsByEntityId.get(entityId) || [];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.JunctionTableTenantRepository = JunctionTableTenantRepository;
|