@almatar/branding 1.0.0-beta.3.3 → 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
  };
@@ -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;
@@ -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.3",
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
  ],