@vritti/api-sdk 0.0.1 → 0.0.2

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/dist/index.cjs CHANGED
@@ -21,14 +21,833 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
- getHello: () => getHello
24
+ DatabaseModule: () => DatabaseModule,
25
+ MessageTenantContextInterceptor: () => MessageTenantContextInterceptor,
26
+ PrimaryDatabaseService: () => PrimaryDatabaseService,
27
+ Tenant: () => Tenant,
28
+ TenantContextInterceptor: () => TenantContextInterceptor,
29
+ TenantContextService: () => TenantContextService,
30
+ TenantDatabaseService: () => TenantDatabaseService,
31
+ extractSubdomain: () => extractSubdomain
25
32
  });
26
33
  module.exports = __toCommonJS(index_exports);
27
- var getHello = /* @__PURE__ */ __name(() => {
28
- return "Hello, World!";
29
- }, "getHello");
34
+
35
+ // src/database/database.module.ts
36
+ var import_common4 = require("@nestjs/common");
37
+
38
+ // src/database/constants.ts
39
+ var DATABASE_MODULE_OPTIONS = Symbol("DATABASE_MODULE_OPTIONS");
40
+
41
+ // src/database/services/primary-database.service.ts
42
+ var import_common = require("@nestjs/common");
43
+ function _ts_decorate(decorators, target, key, desc) {
44
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
45
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
46
+ 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;
47
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
48
+ }
49
+ __name(_ts_decorate, "_ts_decorate");
50
+ function _ts_metadata(k, v) {
51
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
52
+ }
53
+ __name(_ts_metadata, "_ts_metadata");
54
+ function _ts_param(paramIndex, decorator) {
55
+ return function(target, key) {
56
+ decorator(target, key, paramIndex);
57
+ };
58
+ }
59
+ __name(_ts_param, "_ts_param");
60
+ var PrimaryDatabaseService = class _PrimaryDatabaseService {
61
+ static {
62
+ __name(this, "PrimaryDatabaseService");
63
+ }
64
+ options;
65
+ logger = new import_common.Logger(_PrimaryDatabaseService.name);
66
+ /** Primary database client for querying tenant registry */
67
+ primaryDbClient;
68
+ /** In-memory cache: Map<tenantIdentifier, TenantConfig> */
69
+ tenantConfigCache = /* @__PURE__ */ new Map();
70
+ /** Cache TTL in milliseconds */
71
+ cacheTTL;
72
+ constructor(options) {
73
+ this.options = options;
74
+ this.cacheTTL = options.connectionCacheTTL || 3e5;
75
+ }
76
+ async onModuleInit() {
77
+ if (this.options.primaryDb) {
78
+ await this.initializePrimaryDbClient();
79
+ }
80
+ }
81
+ /**
82
+ * Initialize connection to primary database
83
+ */
84
+ async initializePrimaryDbClient() {
85
+ try {
86
+ const PrimaryDbClient = this.options.prismaClientConstructor;
87
+ const databaseUrl = this.buildPrimaryDbUrl();
88
+ this.primaryDbClient = new PrimaryDbClient({
89
+ datasources: {
90
+ db: {
91
+ url: databaseUrl
92
+ }
93
+ },
94
+ log: [
95
+ "error",
96
+ "warn"
97
+ ]
98
+ });
99
+ await this.primaryDbClient.$connect();
100
+ this.logger.log("Connected to primary database (tenant registry)");
101
+ } catch (error) {
102
+ this.logger.error("Failed to connect to primary database", error);
103
+ throw new import_common.InternalServerErrorException("Failed to initialize tenant registry");
104
+ }
105
+ }
106
+ /**
107
+ * Build connection URL from primary database properties
108
+ */
109
+ buildPrimaryDbUrl() {
110
+ if (!this.options.primaryDb) {
111
+ throw new Error("Primary database configuration not provided");
112
+ }
113
+ const { host, port = 5432, username, password, database, schema = "public", sslMode = "require" } = this.options.primaryDb;
114
+ let url = `postgresql://${username}:${password}@${host}:${port}/${database}`;
115
+ const params = new URLSearchParams();
116
+ if (schema) {
117
+ params.set("schema", schema);
118
+ }
119
+ params.set("sslmode", sslMode);
120
+ const queryString = params.toString();
121
+ if (queryString) {
122
+ url += `?${queryString}`;
123
+ }
124
+ this.logger.debug(`Primary DB connection URL: ${this.maskPassword(url)}`);
125
+ return url;
126
+ }
127
+ /**
128
+ * Mask password in connection URL for logging
129
+ */
130
+ maskPassword(url) {
131
+ return url.replace(/:([^@]+)@/, ":****@");
132
+ }
133
+ /**
134
+ * Get tenant configuration by identifier (ID or slug)
135
+ *
136
+ * @param tenantIdentifier Tenant ID or slug
137
+ * @returns Tenant configuration or null if not found
138
+ */
139
+ async getTenantInfo(tenantIdentifier) {
140
+ const cached = this.tenantConfigCache.get(tenantIdentifier);
141
+ if (cached) {
142
+ this.logger.debug(`Cache hit for tenant: ${tenantIdentifier}`);
143
+ return cached;
144
+ }
145
+ try {
146
+ if (!this.primaryDbClient) {
147
+ throw new Error("Primary database client not initialized");
148
+ }
149
+ this.logger.debug(`Querying primary database for tenant: ${tenantIdentifier}`);
150
+ const tenant = await this.primaryDbClient.tenant.findFirst({
151
+ where: {
152
+ OR: [
153
+ {
154
+ id: tenantIdentifier
155
+ },
156
+ {
157
+ subDomain: tenantIdentifier
158
+ }
159
+ ],
160
+ status: "ACTIVE"
161
+ }
162
+ });
163
+ if (!tenant) {
164
+ this.logger.warn(`Tenant not found: ${tenantIdentifier}`);
165
+ return null;
166
+ }
167
+ const info = {
168
+ id: tenant.id,
169
+ subdomain: tenant.subdomain,
170
+ type: tenant.type,
171
+ status: tenant.status,
172
+ schemaName: tenant.schemaName || void 0,
173
+ databaseName: tenant.databaseName || void 0,
174
+ databaseHost: tenant.databaseHost || void 0,
175
+ databasePort: tenant.databasePort || void 0,
176
+ databaseUsername: tenant.databaseUsername ? this.decrypt(tenant.databaseUsername) : void 0,
177
+ databasePassword: tenant.databasePassword ? this.decrypt(tenant.databasePassword) : void 0,
178
+ databaseSslMode: tenant.databaseSslMode || void 0,
179
+ connectionPoolSize: tenant.connectionPoolSize || void 0
180
+ };
181
+ this.cacheInfo(info);
182
+ return info;
183
+ } catch (error) {
184
+ this.logger.error(`Failed to fetch tenant info: ${tenantIdentifier}`, error);
185
+ throw new import_common.InternalServerErrorException("Failed to resolve tenant");
186
+ }
187
+ }
188
+ /**
189
+ * Cache tenant information with TTL
190
+ */
191
+ cacheInfo(info) {
192
+ this.tenantConfigCache.set(info.id, info);
193
+ this.tenantConfigCache.set(info.subdomain, info);
194
+ setTimeout(() => {
195
+ this.tenantConfigCache.delete(info.id);
196
+ this.tenantConfigCache.delete(info.subdomain);
197
+ this.logger.debug(`Cache expired for tenant: ${info.subdomain}`);
198
+ }, this.cacheTTL);
199
+ }
200
+ /**
201
+ * Clear cached tenant information
202
+ *
203
+ * Useful when tenant settings are updated and cache needs to be invalidated
204
+ *
205
+ * @param tenantIdentifier Tenant ID or slug
206
+ */
207
+ clearTenantCache(tenantIdentifier) {
208
+ const config = this.tenantConfigCache.get(tenantIdentifier);
209
+ if (config) {
210
+ this.tenantConfigCache.delete(config.id);
211
+ this.tenantConfigCache.delete(config.subdomain);
212
+ this.logger.log(`Cleared cache for tenant: ${tenantIdentifier}`);
213
+ }
214
+ }
215
+ /**
216
+ * Clear all cached tenant configurations
217
+ */
218
+ clearAllCaches() {
219
+ const size = this.tenantConfigCache.size;
220
+ this.tenantConfigCache.clear();
221
+ this.logger.log(`Cleared ${size} cached tenant configs`);
222
+ }
223
+ /**
224
+ * Get primary database client for direct database access
225
+ *
226
+ * This is useful for platform admin operations (creating tenants, billing, etc.)
227
+ *
228
+ * @returns Primary database client instance
229
+ * @throws Error if primary database client is not initialized
230
+ */
231
+ getPrimaryDbClient() {
232
+ if (!this.primaryDbClient) {
233
+ throw new Error("Primary database client not initialized. Are you in gateway mode?");
234
+ }
235
+ return this.primaryDbClient;
236
+ }
237
+ /**
238
+ * Decrypt database credentials
239
+ *
240
+ * Override this method to implement your encryption strategy
241
+ *
242
+ * @param encrypted Encrypted value
243
+ * @returns Decrypted value
244
+ */
245
+ decrypt(encrypted) {
246
+ return encrypted;
247
+ }
248
+ async onModuleDestroy() {
249
+ if (this.primaryDbClient) {
250
+ await this.primaryDbClient.$disconnect();
251
+ this.logger.log("Disconnected from primary database");
252
+ }
253
+ }
254
+ };
255
+ PrimaryDatabaseService = _ts_decorate([
256
+ (0, import_common.Injectable)(),
257
+ _ts_param(0, (0, import_common.Inject)(DATABASE_MODULE_OPTIONS)),
258
+ _ts_metadata("design:type", Function),
259
+ _ts_metadata("design:paramtypes", [
260
+ typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions
261
+ ])
262
+ ], PrimaryDatabaseService);
263
+
264
+ // src/database/services/tenant-context.service.ts
265
+ var import_common2 = require("@nestjs/common");
266
+ function _ts_decorate2(decorators, target, key, desc) {
267
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
268
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
269
+ 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;
270
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
271
+ }
272
+ __name(_ts_decorate2, "_ts_decorate");
273
+ var TenantContextService = class {
274
+ static {
275
+ __name(this, "TenantContextService");
276
+ }
277
+ tenantInfo = null;
278
+ /**
279
+ * Set tenant information for this request/message
280
+ *
281
+ * This is typically called by:
282
+ * - TenantContextInterceptor (for HTTP requests in gateway)
283
+ * - MessageTenantContextInterceptor (for RabbitMQ messages in microservices)
284
+ * - Manual context setup in message handlers
285
+ *
286
+ * @param tenantInfo Complete tenant information
287
+ * @throws Error if tenant context is already set (prevents accidental overwrites)
288
+ */
289
+ setTenant(tenantInfo) {
290
+ if (this.tenantInfo) {
291
+ throw new Error("Tenant context already set for this request");
292
+ }
293
+ this.tenantInfo = tenantInfo;
294
+ }
295
+ /**
296
+ * Get tenant information for this request/message
297
+ *
298
+ * @returns Tenant information
299
+ * @throws UnauthorizedException if tenant context hasn't been set
300
+ */
301
+ getTenant() {
302
+ if (!this.tenantInfo) {
303
+ throw new import_common2.UnauthorizedException("Tenant context not set");
304
+ }
305
+ return this.tenantInfo;
306
+ }
307
+ /**
308
+ * Check if tenant context has been set
309
+ *
310
+ * @returns true if tenant context is available
311
+ */
312
+ hasTenant() {
313
+ return this.tenantInfo !== null;
314
+ }
315
+ /**
316
+ * Clear tenant context
317
+ *
318
+ * This is useful for cleanup in RabbitMQ message handlers
319
+ * after the message has been processed.
320
+ *
321
+ * HTTP requests don't need manual cleanup as the service
322
+ * instance is destroyed when the request ends.
323
+ */
324
+ clearTenant() {
325
+ this.tenantInfo = null;
326
+ }
327
+ /**
328
+ * Get tenant ID safely (returns null if not set)
329
+ *
330
+ * @returns Tenant ID or null
331
+ */
332
+ getTenantIdSafe() {
333
+ return this.tenantInfo?.id ?? null;
334
+ }
335
+ /**
336
+ * Get tenant subdomain safely (returns null if not set)
337
+ *
338
+ * @returns Tenant subdomain or null
339
+ */
340
+ getTenantSubdomainSafe() {
341
+ return this.tenantInfo?.subdomain ?? null;
342
+ }
343
+ };
344
+ TenantContextService = _ts_decorate2([
345
+ (0, import_common2.Injectable)({
346
+ scope: import_common2.Scope.REQUEST
347
+ })
348
+ ], TenantContextService);
349
+
350
+ // src/database/services/tenant-database.service.ts
351
+ var import_common3 = require("@nestjs/common");
352
+ function _ts_decorate3(decorators, target, key, desc) {
353
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
354
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
355
+ 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;
356
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
357
+ }
358
+ __name(_ts_decorate3, "_ts_decorate");
359
+ function _ts_metadata2(k, v) {
360
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
361
+ }
362
+ __name(_ts_metadata2, "_ts_metadata");
363
+ function _ts_param2(paramIndex, decorator) {
364
+ return function(target, key) {
365
+ decorator(target, key, paramIndex);
366
+ };
367
+ }
368
+ __name(_ts_param2, "_ts_param");
369
+ var TenantDatabaseService = class _TenantDatabaseService {
370
+ static {
371
+ __name(this, "TenantDatabaseService");
372
+ }
373
+ options;
374
+ tenantContext;
375
+ logger = new import_common3.Logger(_TenantDatabaseService.name);
376
+ /** Connection pool: Map<cacheKey, DbClient> */
377
+ clients = /* @__PURE__ */ new Map();
378
+ /** Track last usage time for idle connection cleanup */
379
+ clientLastUsed = /* @__PURE__ */ new Map();
380
+ /** Cleanup interval timer */
381
+ cleanupInterval;
382
+ constructor(options, tenantContext) {
383
+ this.options = options;
384
+ this.tenantContext = tenantContext;
385
+ this.startConnectionCleaner();
386
+ }
387
+ /**
388
+ * Get tenant-scoped database client for the current request/message
389
+ *
390
+ * This method:
391
+ * 1. Gets tenant info from TenantContextService
392
+ * 2. Builds a connection URL based on tenant type
393
+ * 3. Returns cached client if exists, otherwise creates new one
394
+ *
395
+ * @returns Promise<Database client instance>
396
+ * @throws UnauthorizedException if tenant context not set
397
+ * @throws InternalServerErrorException if connection fails
398
+ *
399
+ * @example
400
+ * const dbClient = await tenantDatabase.getDbClient<PrismaClient>();
401
+ * const users = await dbClient.user.findMany();
402
+ */
403
+ async getDbClient() {
404
+ const tenant = this.tenantContext.getTenant();
405
+ const cacheKey = this.buildCacheKey(tenant);
406
+ if (this.clients.has(cacheKey)) {
407
+ this.clientLastUsed.set(cacheKey, Date.now());
408
+ this.logger.debug(`Reusing cached connection: ${cacheKey}`);
409
+ return this.clients.get(cacheKey);
410
+ }
411
+ this.logger.log(`Creating new database connection: ${cacheKey}`);
412
+ const client = await this.createDbClient(tenant);
413
+ this.clients.set(cacheKey, client);
414
+ this.clientLastUsed.set(cacheKey, Date.now());
415
+ return client;
416
+ }
417
+ /**
418
+ * Create a new database client for the given tenant
419
+ */
420
+ async createDbClient(tenant) {
421
+ try {
422
+ const databaseUrl = this.buildTenantDbUrl(tenant);
423
+ const PrismaClient = await this.options.prismaClientConstructor;
424
+ const client = new PrismaClient({
425
+ datasources: {
426
+ db: {
427
+ url: databaseUrl
428
+ }
429
+ },
430
+ log: [
431
+ "error",
432
+ "warn"
433
+ ]
434
+ });
435
+ await client.$connect();
436
+ this.logger.log(`Connected to database for tenant: ${tenant.subdomain}`);
437
+ return client;
438
+ } catch (error) {
439
+ this.logger.error(`Failed to create database connection for tenant: ${tenant.subdomain}`, error);
440
+ throw new import_common3.InternalServerErrorException("Failed to connect to tenant database");
441
+ }
442
+ }
443
+ /**
444
+ * Build connection URL for enterprise tenant (dedicated database)
445
+ */
446
+ buildTenantDbUrl(tenant) {
447
+ const { databaseHost, databasePort, databaseName, databaseUsername, databasePassword, databaseSslMode } = tenant;
448
+ if (!databaseHost || !databaseName || !databaseUsername) {
449
+ throw new Error(`Enterprise tenant ${tenant.subdomain} missing database configuration`);
450
+ }
451
+ const port = databasePort || 5432;
452
+ const sslMode = databaseSslMode || "require";
453
+ const connectionUrl = `postgresql://${databaseUsername}:${databasePassword}@${databaseHost}:${port}/${databaseName}?sslmode=${sslMode}`;
454
+ this.logger.debug(`Enterprise connection URL: ${this.maskPassword(connectionUrl)}`);
455
+ return connectionUrl;
456
+ }
457
+ /**
458
+ * Build cache key for connection pooling
459
+ */
460
+ buildCacheKey(tenant) {
461
+ return `${tenant.type}:${tenant.databaseName}@${tenant.databaseHost}`;
462
+ }
463
+ /**
464
+ * Start periodic cleanup of idle connections
465
+ */
466
+ startConnectionCleaner() {
467
+ const interval = this.options.connectionCacheTTL || 3e5;
468
+ this.cleanupInterval = setInterval(() => {
469
+ this.cleanupIdleConnections();
470
+ }, interval);
471
+ this.logger.log(`Connection cleanup scheduled every ${interval / 1e3} seconds`);
472
+ }
473
+ /**
474
+ * Clean up idle connections that haven't been used recently
475
+ */
476
+ cleanupIdleConnections() {
477
+ const now = Date.now();
478
+ const maxIdle = this.options.connectionCacheTTL || 3e5;
479
+ let cleaned = 0;
480
+ for (const [key, lastUsed] of this.clientLastUsed.entries()) {
481
+ if (now - lastUsed > maxIdle) {
482
+ const client = this.clients.get(key);
483
+ if (client) {
484
+ client.$disconnect().then(() => {
485
+ this.logger.debug(`Cleaned up idle connection: ${key}`);
486
+ }).catch((error) => {
487
+ this.logger.error(`Error disconnecting idle client: ${key}`, error);
488
+ });
489
+ this.clients.delete(key);
490
+ this.clientLastUsed.delete(key);
491
+ cleaned++;
492
+ }
493
+ }
494
+ }
495
+ if (cleaned > 0) {
496
+ this.logger.log(`Cleaned up ${cleaned} idle connections`);
497
+ }
498
+ }
499
+ /**
500
+ * Get current connection pool statistics
501
+ */
502
+ getPoolStats() {
503
+ return {
504
+ activeConnections: this.clients.size,
505
+ tenants: Array.from(this.clients.keys())
506
+ };
507
+ }
508
+ /**
509
+ * Mask password in connection URL for logging
510
+ */
511
+ maskPassword(url) {
512
+ return url.replace(/:([^@]+)@/, ":****@");
513
+ }
514
+ async onModuleDestroy() {
515
+ if (this.cleanupInterval) {
516
+ clearInterval(this.cleanupInterval);
517
+ }
518
+ this.logger.log(`Disconnecting ${this.clients.size} database connections`);
519
+ const disconnectPromises = Array.from(this.clients.entries()).map(async ([key, client]) => {
520
+ try {
521
+ await client.$disconnect();
522
+ this.logger.debug(`Disconnected: ${key}`);
523
+ } catch (error) {
524
+ this.logger.error(`Error disconnecting client: ${key}`, error);
525
+ }
526
+ });
527
+ await Promise.all(disconnectPromises);
528
+ this.logger.log("All database connections closed");
529
+ }
530
+ };
531
+ TenantDatabaseService = _ts_decorate3([
532
+ (0, import_common3.Injectable)(),
533
+ _ts_param2(0, (0, import_common3.Inject)(DATABASE_MODULE_OPTIONS)),
534
+ _ts_metadata2("design:type", Function),
535
+ _ts_metadata2("design:paramtypes", [
536
+ typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions,
537
+ typeof TenantContextService === "undefined" ? Object : TenantContextService
538
+ ])
539
+ ], TenantDatabaseService);
540
+
541
+ // src/database/database.module.ts
542
+ function _ts_decorate4(decorators, target, key, desc) {
543
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
544
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
545
+ 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;
546
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
547
+ }
548
+ __name(_ts_decorate4, "_ts_decorate");
549
+ var DatabaseModule = class _DatabaseModule {
550
+ static {
551
+ __name(this, "DatabaseModule");
552
+ }
553
+ /**
554
+ * Synchronous configuration
555
+ *
556
+ * @param options Module configuration options
557
+ * @returns Dynamic module configuration
558
+ */
559
+ static forRoot(options) {
560
+ const providers = [
561
+ {
562
+ provide: DATABASE_MODULE_OPTIONS,
563
+ useValue: options
564
+ },
565
+ TenantDatabaseService,
566
+ TenantContextService,
567
+ PrimaryDatabaseService
568
+ ];
569
+ return {
570
+ module: _DatabaseModule,
571
+ providers,
572
+ exports: [
573
+ TenantDatabaseService,
574
+ TenantContextService,
575
+ PrimaryDatabaseService
576
+ ]
577
+ };
578
+ }
579
+ /**
580
+ * Asynchronous configuration (recommended)
581
+ *
582
+ * Allows injecting ConfigService or other dependencies
583
+ *
584
+ * @param options Async configuration options
585
+ * @returns Dynamic module configuration
586
+ *
587
+ * @example
588
+ * DatabaseModule.forRootAsync({
589
+ * imports: [ConfigModule],
590
+ * useFactory: async (config: ConfigService) => ({
591
+ * cloudDatabaseUrl: config.get('CLOUD_DATABASE_URL'),
592
+ * prismaClientConstructor: PrismaClient,
593
+ * tenantResolver: 'subdomain',
594
+ * }),
595
+ * inject: [ConfigService],
596
+ * })
597
+ */
598
+ static forRootAsync(options) {
599
+ const asyncProvider = {
600
+ provide: DATABASE_MODULE_OPTIONS,
601
+ useFactory: options.useFactory,
602
+ inject: options.inject || []
603
+ };
604
+ return {
605
+ module: _DatabaseModule,
606
+ providers: [
607
+ asyncProvider,
608
+ TenantContextService,
609
+ PrimaryDatabaseService,
610
+ TenantDatabaseService
611
+ ],
612
+ exports: [
613
+ TenantDatabaseService,
614
+ TenantContextService,
615
+ PrimaryDatabaseService,
616
+ asyncProvider
617
+ ]
618
+ };
619
+ }
620
+ };
621
+ DatabaseModule = _ts_decorate4([
622
+ (0, import_common4.Global)(),
623
+ (0, import_common4.Module)({})
624
+ ], DatabaseModule);
625
+
626
+ // src/database/interceptors/message-tenant-context.interceptor.ts
627
+ var import_common5 = require("@nestjs/common");
628
+ var import_operators = require("rxjs/operators");
629
+ function _ts_decorate5(decorators, target, key, desc) {
630
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
631
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
632
+ 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;
633
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
634
+ }
635
+ __name(_ts_decorate5, "_ts_decorate");
636
+ function _ts_metadata3(k, v) {
637
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
638
+ }
639
+ __name(_ts_metadata3, "_ts_metadata");
640
+ var MessageTenantContextInterceptor = class _MessageTenantContextInterceptor {
641
+ static {
642
+ __name(this, "MessageTenantContextInterceptor");
643
+ }
644
+ tenantContext;
645
+ logger = new import_common5.Logger(_MessageTenantContextInterceptor.name);
646
+ constructor(tenantContext) {
647
+ this.tenantContext = tenantContext;
648
+ }
649
+ intercept(context, next) {
650
+ const contextType = context.getType();
651
+ if (contextType === "rpc") {
652
+ const rpcContext = context.switchToRpc();
653
+ const payload = rpcContext.getData();
654
+ if (payload && payload.tenant) {
655
+ const tenant = payload.tenant;
656
+ this.logger.debug(`Setting tenant context from message: ${tenant.subdomain}`);
657
+ try {
658
+ this.tenantContext.setTenant(tenant);
659
+ this.logger.log(`Tenant context set: ${tenant.subdomain} (${tenant.type})`);
660
+ } catch (error) {
661
+ this.logger.error("Failed to set tenant context from message", error);
662
+ }
663
+ } else {
664
+ this.logger.warn("Message payload missing tenant information");
665
+ }
666
+ }
667
+ return next.handle().pipe((0, import_operators.tap)({
668
+ next: /* @__PURE__ */ __name(() => {
669
+ this.cleanupContext();
670
+ }, "next"),
671
+ error: /* @__PURE__ */ __name(() => {
672
+ this.cleanupContext();
673
+ }, "error"),
674
+ complete: /* @__PURE__ */ __name(() => {
675
+ this.cleanupContext();
676
+ }, "complete")
677
+ }));
678
+ }
679
+ /**
680
+ * Clean up tenant context after message is processed
681
+ */
682
+ cleanupContext() {
683
+ if (this.tenantContext.hasTenant()) {
684
+ const tenant = this.tenantContext.getTenantIdSafe();
685
+ this.tenantContext.clearTenant();
686
+ this.logger.debug(`Cleaned up tenant context: ${tenant}`);
687
+ }
688
+ }
689
+ };
690
+ MessageTenantContextInterceptor = _ts_decorate5([
691
+ (0, import_common5.Injectable)({
692
+ scope: import_common5.Scope.REQUEST
693
+ }),
694
+ _ts_metadata3("design:type", Function),
695
+ _ts_metadata3("design:paramtypes", [
696
+ typeof TenantContextService === "undefined" ? Object : TenantContextService
697
+ ])
698
+ ], MessageTenantContextInterceptor);
699
+
700
+ // src/database/interceptors/tenant-context.interceptor.ts
701
+ var import_common6 = require("@nestjs/common");
702
+
703
+ // src/database/utils/subdomain-parser.util.ts
704
+ function extractSubdomain(host) {
705
+ if (!host) {
706
+ return null;
707
+ }
708
+ const hostname = host.split(":")[0];
709
+ if (!hostname) {
710
+ return null;
711
+ }
712
+ const parts = hostname.split(".");
713
+ if (parts.length < 3) {
714
+ return null;
715
+ }
716
+ return parts[0] ?? null;
717
+ }
718
+ __name(extractSubdomain, "extractSubdomain");
719
+
720
+ // src/database/interceptors/tenant-context.interceptor.ts
721
+ function _ts_decorate6(decorators, target, key, desc) {
722
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
723
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
724
+ 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;
725
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
726
+ }
727
+ __name(_ts_decorate6, "_ts_decorate");
728
+ function _ts_metadata4(k, v) {
729
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
730
+ }
731
+ __name(_ts_metadata4, "_ts_metadata");
732
+ function _ts_param3(paramIndex, decorator) {
733
+ return function(target, key) {
734
+ decorator(target, key, paramIndex);
735
+ };
736
+ }
737
+ __name(_ts_param3, "_ts_param");
738
+ var TenantContextInterceptor = class _TenantContextInterceptor {
739
+ static {
740
+ __name(this, "TenantContextInterceptor");
741
+ }
742
+ tenantContext;
743
+ primaryDatabase;
744
+ options;
745
+ logger = new import_common6.Logger(_TenantContextInterceptor.name);
746
+ constructor(tenantContext, primaryDatabase, options) {
747
+ this.tenantContext = tenantContext;
748
+ this.primaryDatabase = primaryDatabase;
749
+ this.options = options;
750
+ }
751
+ async intercept(context, next) {
752
+ const request = context.switchToHttp().getRequest();
753
+ this.logger.debug(`Processing request: ${request.method} ${request.url}`);
754
+ try {
755
+ const tenantIdentifier = this.extractTenantIdentifier(request);
756
+ if (!tenantIdentifier) {
757
+ throw new import_common6.UnauthorizedException("Tenant identifier not found in request");
758
+ }
759
+ this.logger.debug(`Tenant identifier extracted: ${tenantIdentifier}`);
760
+ if (tenantIdentifier === "cloud") {
761
+ this.logger.log("Cloud platform access detected, skipping tenant context setup");
762
+ return next.handle();
763
+ }
764
+ const tenantInfo = await this.primaryDatabase.getTenantInfo(tenantIdentifier);
765
+ if (!tenantInfo) {
766
+ this.logger.warn(`Invalid tenant: ${tenantIdentifier}`);
767
+ throw new import_common6.UnauthorizedException("Invalid tenant");
768
+ }
769
+ if (tenantInfo.status !== "ACTIVE") {
770
+ this.logger.warn(`Tenant ${tenantIdentifier} has status: ${tenantInfo.status}`);
771
+ throw new import_common6.UnauthorizedException(`Tenant is ${tenantInfo.status}`);
772
+ }
773
+ this.logger.debug(`Tenant config loaded: ${tenantInfo.subdomain} (${tenantInfo.type})`);
774
+ this.tenantContext.setTenant(tenantInfo);
775
+ request.tenant = tenantInfo;
776
+ this.logger.log(`Tenant context set: ${tenantInfo.subdomain}`);
777
+ } catch (error) {
778
+ this.logger.error("Failed to set tenant context", error);
779
+ throw error;
780
+ }
781
+ return next.handle();
782
+ }
783
+ /**
784
+ * Extract tenant identifier: tries subdomain first, then falls back to header
785
+ */
786
+ extractTenantIdentifier(request) {
787
+ let tenantIdentifier = this.extractFromSubdomain(request);
788
+ if (!tenantIdentifier) {
789
+ tenantIdentifier = this.extractFromHeader(request);
790
+ if (tenantIdentifier) {
791
+ this.logger.debug("Using tenant from header (subdomain not found)");
792
+ }
793
+ }
794
+ return tenantIdentifier;
795
+ }
796
+ /**
797
+ * Extract tenant from subdomain
798
+ * @example acme.vritti.com → 'acme'
799
+ */
800
+ extractFromSubdomain(request) {
801
+ if (request.subdomain) {
802
+ return request.subdomain;
803
+ }
804
+ const host = request.headers?.host || request.hostname;
805
+ return extractSubdomain(host);
806
+ }
807
+ /**
808
+ * Extract tenant from HTTP headers
809
+ * Checks x-tenant-id and x-subdomain headers
810
+ */
811
+ extractFromHeader(request) {
812
+ const getHeader = /* @__PURE__ */ __name((key) => {
813
+ const value = request.headers?.[key];
814
+ return Array.isArray(value) ? value[0] : value;
815
+ }, "getHeader");
816
+ return getHeader("x-tenant-id") || getHeader("x-subdomain") || null;
817
+ }
818
+ };
819
+ TenantContextInterceptor = _ts_decorate6([
820
+ (0, import_common6.Injectable)({
821
+ scope: import_common6.Scope.REQUEST
822
+ }),
823
+ _ts_param3(2, (0, import_common6.Inject)(DATABASE_MODULE_OPTIONS)),
824
+ _ts_metadata4("design:type", Function),
825
+ _ts_metadata4("design:paramtypes", [
826
+ typeof TenantContextService === "undefined" ? Object : TenantContextService,
827
+ typeof PrimaryDatabaseService === "undefined" ? Object : PrimaryDatabaseService,
828
+ typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions
829
+ ])
830
+ ], TenantContextInterceptor);
831
+
832
+ // src/database/decorators/tenant.decorator.ts
833
+ var import_common7 = require("@nestjs/common");
834
+ var Tenant = (0, import_common7.createParamDecorator)((data, ctx) => {
835
+ const request = ctx.switchToHttp().getRequest();
836
+ const tenantContext = request.app?.get?.(TenantContextService);
837
+ if (!tenantContext) {
838
+ throw new Error("TenantContextService not found.");
839
+ }
840
+ return tenantContext.getTenant();
841
+ });
30
842
  // Annotate the CommonJS export names for ESM import in node:
31
843
  0 && (module.exports = {
32
- getHello
844
+ DatabaseModule,
845
+ MessageTenantContextInterceptor,
846
+ PrimaryDatabaseService,
847
+ Tenant,
848
+ TenantContextInterceptor,
849
+ TenantContextService,
850
+ TenantDatabaseService,
851
+ extractSubdomain
33
852
  });
34
853
  //# sourceMappingURL=index.cjs.map