@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 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
  };
@@ -4,5 +4,6 @@ export declare class BrandIdentifier {
4
4
  getBrand(req: any): Promise<string | string[] | null>;
5
5
  getBrands(): Promise<string[] | null>;
6
6
  getDefaultBrand(): string;
7
+ private getQueryBrand;
7
8
  private error;
8
9
  }
@@ -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;
@@ -14,4 +14,5 @@ export default class ContextNamespace {
14
14
  static getDefaultBrand(): string | null;
15
15
  static isValidBrand(brand: string): boolean;
16
16
  static isBrandIncludesProduct(brandKey: string, product: string): boolean;
17
+ private static getQueryBrand;
17
18
  }
@@ -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
- // Only set x-employee-brands header for employee requests (not B2C)
31
- if (isEmployeeRequest && brand.length > 0) {
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
- // Set x-employee-brands header for employee requests
53
- req.headers['x-employee-brands'] = brand;
54
- // tslint:disable-next-line no-console
55
- console.log('[ContextNamespace] Setting x-employee-brands header on request:', req.headers['x-employee-brands']);
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;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@almatar/branding",
3
- "version": "1.0.0-beta.3.2",
3
+ "version": "1.0.0-beta.3.4",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
+ "type": "commonjs",
7
8
  "files": [
8
9
  "lib/**/*"
9
10
  ],