@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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const getHello = () => {\n return 'Hello, World!';\n};\n"],"mappings":";;;;AAAO,IAAM,WAAW,6BAAM;AAC5B,SAAO;AACT,GAFwB;","names":[]}
1
+ {"version":3,"sources":["../src/database/database.module.ts","../src/database/constants.ts","../src/database/services/primary-database.service.ts","../src/database/services/tenant-context.service.ts","../src/database/services/tenant-database.service.ts","../src/database/interceptors/message-tenant-context.interceptor.ts","../src/database/interceptors/tenant-context.interceptor.ts","../src/database/utils/subdomain-parser.util.ts","../src/database/decorators/tenant.decorator.ts"],"sourcesContent":["import { DynamicModule, Global, Module, Provider } from '@nestjs/common';\nimport { DATABASE_MODULE_OPTIONS } from './constants';\n\nimport type { DatabaseModuleOptions } from './interfaces';\nimport { PrimaryDatabaseService } from './services/primary-database.service';\nimport { TenantContextService } from './services/tenant-context.service';\nimport { TenantDatabaseService } from './services/tenant-database.service';\n\n/**\n * Dynamic module for multi-tenant database management\n *\n * This module provides:\n * - Tenant context management (request-scoped)\n * - Database connection pooling\n * - Dynamic schema/cluster routing\n * - Support for both gateway and microservice modes\n *\n * ## Gateway Mode (API Gateway)\n * - Automatically extracts tenant from subdomain, falls back to x-tenant-id header\n * - Provide primaryDb configuration and prismaClientConstructor\n * - Automatically queries primary DB for tenant config\n * - Attaches TenantContextInterceptor globally\n *\n * ## Microservice Mode\n * - Only provide prismaClientConstructor\n * - Tenant context comes from RabbitMQ messages\n * - Use MessageTenantContextInterceptor manually\n *\n * @example\n * // Gateway configuration\n * DatabaseModule.forRootAsync({\n * imports: [ConfigModule],\n * useFactory: (config: ConfigService) => ({\n * primaryDb: {\n * host: config.get('PRIMARY_DB_HOST'),\n * port: config.get('PRIMARY_DB_PORT'),\n * username: config.get('PRIMARY_DB_USERNAME'),\n * password: config.get('PRIMARY_DB_PASSWORD'),\n * database: config.get('PRIMARY_DB_DATABASE'),\n * },\n * prismaClientConstructor: PrismaClient,\n * }),\n * inject: [ConfigService],\n * })\n *\n * @example\n * // Microservice configuration\n * DatabaseModule.forRoot({\n * prismaClientConstructor: PrismaClient,\n * })\n */\n@Global()\n@Module({})\nexport class DatabaseModule {\n /**\n * Synchronous configuration\n *\n * @param options Module configuration options\n * @returns Dynamic module configuration\n */\n static forRoot(options: DatabaseModuleOptions): DynamicModule {\n const providers: Provider[] = [\n {\n provide: DATABASE_MODULE_OPTIONS,\n useValue: options,\n },\n TenantDatabaseService,\n TenantContextService,\n PrimaryDatabaseService,\n ];\n\n return {\n module: DatabaseModule,\n providers,\n exports: [TenantDatabaseService, TenantContextService, PrimaryDatabaseService],\n };\n }\n\n /**\n * Asynchronous configuration (recommended)\n *\n * Allows injecting ConfigService or other dependencies\n *\n * @param options Async configuration options\n * @returns Dynamic module configuration\n *\n * @example\n * DatabaseModule.forRootAsync({\n * imports: [ConfigModule],\n * useFactory: async (config: ConfigService) => ({\n * cloudDatabaseUrl: config.get('CLOUD_DATABASE_URL'),\n * prismaClientConstructor: PrismaClient,\n * tenantResolver: 'subdomain',\n * }),\n * inject: [ConfigService],\n * })\n */\n static forRootAsync(options: {\n useFactory: (...args: any[]) => Promise<DatabaseModuleOptions> | DatabaseModuleOptions;\n inject?: any[];\n }): DynamicModule {\n const asyncProvider: Provider = {\n provide: DATABASE_MODULE_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject || [],\n };\n return {\n module: DatabaseModule,\n providers: [\n asyncProvider,\n TenantContextService,\n PrimaryDatabaseService,\n TenantDatabaseService,\n ],\n exports: [TenantDatabaseService, TenantContextService, PrimaryDatabaseService, asyncProvider],\n };\n }\n}\n","export const DATABASE_MODULE_OPTIONS = Symbol('DATABASE_MODULE_OPTIONS');\n","import {\n Inject,\n Injectable,\n InternalServerErrorException,\n Logger,\n OnModuleDestroy,\n OnModuleInit,\n} from '@nestjs/common';\nimport { DATABASE_MODULE_OPTIONS } from '../constants';\nimport type { DatabaseModuleOptions, TenantInfo } from '../interfaces';\n\n/**\n * Service responsible for querying the primary database to resolve tenant configurations\n *\n * This service:\n * - Connects to the primary database (tenant registry)\n * - Queries tenant metadata (database location, credentials, etc.)\n * - Caches tenant configs in memory to reduce database load\n * - Only used in GATEWAY MODE (microservices receive tenant config from messages)\n *\n * @example\n * // In API Gateway\n * const config = await primaryDatabase.getTenantConfig('acme');\n * // Returns: { id, slug, type, databaseHost, databaseName, ... }\n */\n@Injectable()\nexport class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(PrimaryDatabaseService.name);\n\n /** Primary database client for querying tenant registry */\n private primaryDbClient: any;\n\n /** In-memory cache: Map<tenantIdentifier, TenantConfig> */\n private readonly tenantConfigCache = new Map<string, TenantInfo>();\n\n /** Cache TTL in milliseconds */\n private readonly cacheTTL: number;\n\n constructor(\n @Inject(DATABASE_MODULE_OPTIONS)\n private readonly options: DatabaseModuleOptions,\n ) {\n this.cacheTTL = options.connectionCacheTTL || 300000; // 5 minutes default\n }\n\n async onModuleInit() {\n // Only initialize if we have primary database config (gateway mode)\n if (this.options.primaryDb) {\n await this.initializePrimaryDbClient();\n }\n }\n\n /**\n * Initialize connection to primary database\n */\n private async initializePrimaryDbClient(): Promise<void> {\n try {\n const PrimaryDbClient = this.options.prismaClientConstructor;\n\n // Build connection URL from individual properties\n const databaseUrl = this.buildPrimaryDbUrl();\n\n this.primaryDbClient = new PrimaryDbClient({\n datasources: {\n db: {\n url: databaseUrl,\n },\n },\n log: ['error', 'warn'],\n });\n\n await this.primaryDbClient.$connect();\n this.logger.log('Connected to primary database (tenant registry)');\n } catch (error) {\n this.logger.error('Failed to connect to primary database', error);\n throw new InternalServerErrorException('Failed to initialize tenant registry');\n }\n }\n\n /**\n * Build connection URL from primary database properties\n */\n private buildPrimaryDbUrl(): string {\n if (!this.options.primaryDb) {\n throw new Error('Primary database configuration not provided');\n }\n\n const {\n host,\n port = 5432,\n username,\n password,\n database,\n schema = 'public',\n sslMode = 'require',\n } = this.options.primaryDb;\n\n // Build base URL\n let url = `postgresql://${username}:${password}@${host}:${port}/${database}`;\n\n // Add query parameters\n const params = new URLSearchParams();\n if (schema) {\n params.set('schema', schema);\n }\n params.set('sslmode', sslMode);\n\n const queryString = params.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n\n this.logger.debug(`Primary DB connection URL: ${this.maskPassword(url)}`);\n\n return url;\n }\n\n /**\n * Mask password in connection URL for logging\n */\n private maskPassword(url: string): string {\n return url.replace(/:([^@]+)@/, ':****@');\n }\n\n /**\n * Get tenant configuration by identifier (ID or slug)\n *\n * @param tenantIdentifier Tenant ID or slug\n * @returns Tenant configuration or null if not found\n */\n async getTenantInfo(tenantIdentifier: string): Promise<TenantInfo | null> {\n // Check cache first\n const cached = this.tenantConfigCache.get(tenantIdentifier);\n if (cached) {\n this.logger.debug(`Cache hit for tenant: ${tenantIdentifier}`);\n return cached;\n }\n\n // Query primary database\n try {\n if (!this.primaryDbClient) {\n throw new Error('Primary database client not initialized');\n }\n\n this.logger.debug(`Querying primary database for tenant: ${tenantIdentifier}`);\n\n const tenant = await this.primaryDbClient.tenant.findFirst({\n where: {\n OR: [{ id: tenantIdentifier }, { subDomain: tenantIdentifier }],\n status: 'ACTIVE',\n },\n });\n\n if (!tenant) {\n this.logger.warn(`Tenant not found: ${tenantIdentifier}`);\n return null;\n }\n\n // Build info object\n const info: TenantInfo = {\n id: tenant.id,\n subdomain: tenant.subdomain,\n type: tenant.type,\n status: tenant.status,\n schemaName: tenant.schemaName || undefined,\n databaseName: tenant.databaseName || undefined,\n databaseHost: tenant.databaseHost || undefined,\n databasePort: tenant.databasePort || undefined,\n databaseUsername: tenant.databaseUsername\n ? this.decrypt(tenant.databaseUsername)\n : undefined,\n databasePassword: tenant.databasePassword\n ? this.decrypt(tenant.databasePassword)\n : undefined,\n databaseSslMode: tenant.databaseSslMode || undefined,\n connectionPoolSize: tenant.connectionPoolSize || undefined,\n };\n\n // Cache by both ID and slug\n this.cacheInfo(info);\n\n return info;\n } catch (error) {\n this.logger.error(`Failed to fetch tenant info: ${tenantIdentifier}`, error);\n throw new InternalServerErrorException('Failed to resolve tenant');\n }\n }\n\n /**\n * Cache tenant information with TTL\n */\n private cacheInfo(info: TenantInfo): void {\n this.tenantConfigCache.set(info.id, info);\n this.tenantConfigCache.set(info.subdomain, info);\n\n // Set expiration\n setTimeout(() => {\n this.tenantConfigCache.delete(info.id);\n this.tenantConfigCache.delete(info.subdomain);\n this.logger.debug(`Cache expired for tenant: ${info.subdomain}`);\n }, this.cacheTTL);\n }\n\n /**\n * Clear cached tenant information\n *\n * Useful when tenant settings are updated and cache needs to be invalidated\n *\n * @param tenantIdentifier Tenant ID or slug\n */\n clearTenantCache(tenantIdentifier: string): void {\n const config = this.tenantConfigCache.get(tenantIdentifier);\n if (config) {\n this.tenantConfigCache.delete(config.id);\n this.tenantConfigCache.delete(config.subdomain);\n this.logger.log(`Cleared cache for tenant: ${tenantIdentifier}`);\n }\n }\n\n /**\n * Clear all cached tenant configurations\n */\n clearAllCaches(): void {\n const size = this.tenantConfigCache.size;\n this.tenantConfigCache.clear();\n this.logger.log(`Cleared ${size} cached tenant configs`);\n }\n\n /**\n * Get primary database client for direct database access\n *\n * This is useful for platform admin operations (creating tenants, billing, etc.)\n *\n * @returns Primary database client instance\n * @throws Error if primary database client is not initialized\n */\n getPrimaryDbClient<T = any>(): T {\n if (!this.primaryDbClient) {\n throw new Error('Primary database client not initialized. Are you in gateway mode?');\n }\n return this.primaryDbClient;\n }\n\n /**\n * Decrypt database credentials\n *\n * Override this method to implement your encryption strategy\n *\n * @param encrypted Encrypted value\n * @returns Decrypted value\n */\n private decrypt(encrypted: string): string {\n // TODO: Implement actual decryption using this.options.encryptionKey\n // For now, return as-is (assumes unencrypted or encryption happens elsewhere)\n return encrypted;\n }\n\n async onModuleDestroy() {\n if (this.primaryDbClient) {\n await this.primaryDbClient.$disconnect();\n this.logger.log('Disconnected from primary database');\n }\n }\n}\n","import { Injectable, Scope, UnauthorizedException } from '@nestjs/common';\nimport { TenantInfo } from '../interfaces';\n\n/**\n * Request-scoped service that holds tenant context for the current request or RabbitMQ message\n *\n * IMPORTANT: This service is REQUEST-SCOPED, meaning NestJS creates a new instance\n * for each HTTP request or RabbitMQ message. This ensures tenant isolation and\n * prevents cross-tenant data leaks in concurrent scenarios.\n *\n * @example\n * // In a controller or service\n * constructor(private readonly tenantContext: TenantContextService) {}\n *\n * async handleRequest() {\n * const tenant = this.tenantContext.getTenant();\n * console.log(`Processing request for tenant: ${tenant.tenantSlug}`);\n * }\n */\n@Injectable({ scope: Scope.REQUEST })\nexport class TenantContextService {\n private tenantInfo: TenantInfo | null = null;\n\n /**\n * Set tenant information for this request/message\n *\n * This is typically called by:\n * - TenantContextInterceptor (for HTTP requests in gateway)\n * - MessageTenantContextInterceptor (for RabbitMQ messages in microservices)\n * - Manual context setup in message handlers\n *\n * @param tenantInfo Complete tenant information\n * @throws Error if tenant context is already set (prevents accidental overwrites)\n */\n setTenant(tenantInfo: TenantInfo): void {\n if (this.tenantInfo) {\n throw new Error('Tenant context already set for this request');\n }\n this.tenantInfo = tenantInfo;\n }\n\n /**\n * Get tenant information for this request/message\n *\n * @returns Tenant information\n * @throws UnauthorizedException if tenant context hasn't been set\n */\n getTenant(): TenantInfo {\n if (!this.tenantInfo) {\n throw new UnauthorizedException('Tenant context not set');\n }\n return this.tenantInfo;\n }\n\n /**\n * Check if tenant context has been set\n *\n * @returns true if tenant context is available\n */\n hasTenant(): boolean {\n return this.tenantInfo !== null;\n }\n\n /**\n * Clear tenant context\n *\n * This is useful for cleanup in RabbitMQ message handlers\n * after the message has been processed.\n *\n * HTTP requests don't need manual cleanup as the service\n * instance is destroyed when the request ends.\n */\n clearTenant(): void {\n this.tenantInfo = null;\n }\n\n /**\n * Get tenant ID safely (returns null if not set)\n *\n * @returns Tenant ID or null\n */\n getTenantIdSafe(): string | null {\n return this.tenantInfo?.id ?? null;\n }\n\n /**\n * Get tenant subdomain safely (returns null if not set)\n *\n * @returns Tenant subdomain or null\n */\n getTenantSubdomainSafe(): string | null {\n return this.tenantInfo?.subdomain ?? null;\n }\n}\n","import {\n Inject,\n Injectable,\n InternalServerErrorException,\n Logger,\n OnModuleDestroy,\n} from '@nestjs/common';\nimport { DATABASE_MODULE_OPTIONS } from '../constants';\nimport type { DatabaseModuleOptions, TenantInfo } from '../interfaces';\nimport { TenantContextService } from './tenant-context.service';\n\n/**\n * Service responsible for managing tenant-scoped database connections\n *\n * This service:\n * - Maintains a connection pool (Map<cacheKey, DbClient>)\n * - Creates new connections dynamically based on tenant context\n * - Reuses existing connections for the same tenant\n * - Supports both cloud schemas and enterprise databases\n * - Automatically cleans up idle connections\n *\n * @example\n * // In a controller or service\n * const dbClient = await this.tenantDatabase.getDbClient<PrismaClient>();\n * const users = await dbClient.user.findMany();\n */\n@Injectable()\nexport class TenantDatabaseService implements OnModuleDestroy {\n private readonly logger = new Logger(TenantDatabaseService.name);\n\n /** Connection pool: Map<cacheKey, DbClient> */\n private readonly clients = new Map<string, any>();\n\n /** Track last usage time for idle connection cleanup */\n private readonly clientLastUsed = new Map<string, number>();\n\n /** Cleanup interval timer */\n private cleanupInterval?: NodeJS.Timeout;\n\n constructor(\n @Inject(DATABASE_MODULE_OPTIONS)\n private readonly options: DatabaseModuleOptions,\n private readonly tenantContext: TenantContextService,\n ) {\n this.startConnectionCleaner();\n }\n\n /**\n * Get tenant-scoped database client for the current request/message\n *\n * This method:\n * 1. Gets tenant info from TenantContextService\n * 2. Builds a connection URL based on tenant type\n * 3. Returns cached client if exists, otherwise creates new one\n *\n * @returns Promise<Database client instance>\n * @throws UnauthorizedException if tenant context not set\n * @throws InternalServerErrorException if connection fails\n *\n * @example\n * const dbClient = await tenantDatabase.getDbClient<PrismaClient>();\n * const users = await dbClient.user.findMany();\n */\n async getDbClient<T = any>(): Promise<T> {\n const tenant = this.tenantContext.getTenant();\n\n const cacheKey = this.buildCacheKey(tenant);\n\n // Check if connection already exists\n if (this.clients.has(cacheKey)) {\n this.clientLastUsed.set(cacheKey, Date.now());\n this.logger.debug(`Reusing cached connection: ${cacheKey}`);\n return this.clients.get(cacheKey) as T;\n }\n\n // Create new connection\n this.logger.log(`Creating new database connection: ${cacheKey}`);\n const client = await this.createDbClient(tenant);\n this.clients.set(cacheKey, client);\n this.clientLastUsed.set(cacheKey, Date.now());\n\n return client as T;\n }\n\n /**\n * Create a new database client for the given tenant\n */\n private async createDbClient(tenant: TenantInfo): Promise<any> {\n try {\n // Build tenant-specific database URL\n const databaseUrl = this.buildTenantDbUrl(tenant);\n\n // Load Prisma client constructor\n const PrismaClient = await this.options.prismaClientConstructor;\n\n // Create new instance with tenant-specific connection\n const client = new PrismaClient({\n datasources: {\n db: { url: databaseUrl },\n },\n log: ['error', 'warn'],\n });\n\n await client.$connect();\n this.logger.log(`Connected to database for tenant: ${tenant.subdomain}`);\n\n return client;\n } catch (error) {\n this.logger.error(\n `Failed to create database connection for tenant: ${tenant.subdomain}`,\n error,\n );\n throw new InternalServerErrorException('Failed to connect to tenant database');\n }\n }\n\n /**\n * Build connection URL for enterprise tenant (dedicated database)\n */\n private buildTenantDbUrl(tenant: TenantInfo): string {\n const {\n databaseHost,\n databasePort,\n databaseName,\n databaseUsername,\n databasePassword,\n databaseSslMode,\n } = tenant;\n\n if (!databaseHost || !databaseName || !databaseUsername) {\n throw new Error(`Enterprise tenant ${tenant.subdomain} missing database configuration`);\n }\n\n const port = databasePort || 5432;\n const sslMode = databaseSslMode || 'require';\n const connectionUrl = `postgresql://${databaseUsername}:${databasePassword}@${databaseHost}:${port}/${databaseName}?sslmode=${sslMode}`;\n\n this.logger.debug(`Enterprise connection URL: ${this.maskPassword(connectionUrl)}`);\n\n return connectionUrl;\n }\n\n /**\n * Build cache key for connection pooling\n */\n private buildCacheKey(tenant: TenantInfo): string {\n return `${tenant.type}:${tenant.databaseName}@${tenant.databaseHost}`;\n }\n\n /**\n * Start periodic cleanup of idle connections\n */\n private startConnectionCleaner(): void {\n const interval = this.options.connectionCacheTTL || 300000; // 5 minutes\n\n this.cleanupInterval = setInterval(() => {\n this.cleanupIdleConnections();\n }, interval);\n\n this.logger.log(`Connection cleanup scheduled every ${interval / 1000} seconds`);\n }\n\n /**\n * Clean up idle connections that haven't been used recently\n */\n private cleanupIdleConnections(): void {\n const now = Date.now();\n const maxIdle = this.options.connectionCacheTTL || 300000;\n\n let cleaned = 0;\n\n for (const [key, lastUsed] of this.clientLastUsed.entries()) {\n if (now - lastUsed > maxIdle) {\n const client = this.clients.get(key);\n if (client) {\n client\n .$disconnect()\n .then(() => {\n this.logger.debug(`Cleaned up idle connection: ${key}`);\n })\n .catch((error: any) => {\n this.logger.error(`Error disconnecting idle client: ${key}`, error);\n });\n\n this.clients.delete(key);\n this.clientLastUsed.delete(key);\n cleaned++;\n }\n }\n }\n\n if (cleaned > 0) {\n this.logger.log(`Cleaned up ${cleaned} idle connections`);\n }\n }\n\n /**\n * Get current connection pool statistics\n */\n getPoolStats(): {\n activeConnections: number;\n tenants: string[];\n } {\n return {\n activeConnections: this.clients.size,\n tenants: Array.from(this.clients.keys()),\n };\n }\n\n /**\n * Mask password in connection URL for logging\n */\n private maskPassword(url: string): string {\n return url.replace(/:([^@]+)@/, ':****@');\n }\n\n async onModuleDestroy() {\n // Stop cleanup interval\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n }\n\n // Disconnect all clients\n this.logger.log(`Disconnecting ${this.clients.size} database connections`);\n\n const disconnectPromises = Array.from(this.clients.entries()).map(async ([key, client]) => {\n try {\n await client.$disconnect();\n this.logger.debug(`Disconnected: ${key}`);\n } catch (error) {\n this.logger.error(`Error disconnecting client: ${key}`, error);\n }\n });\n\n await Promise.all(disconnectPromises);\n this.logger.log('All database connections closed');\n }\n}\n","import {\n CallHandler,\n ExecutionContext,\n Injectable,\n Logger,\n NestInterceptor,\n Scope,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { tap } from 'rxjs/operators';\nimport { TenantInfo } from '../interfaces';\nimport { TenantContextService } from '../services/tenant-context.service';\n\n/**\n * Interceptor that extracts tenant context from RabbitMQ messages (Microservice Mode)\n *\n * This interceptor:\n * 1. Extracts tenant info from RabbitMQ message payload\n * 2. Sets it in REQUEST-SCOPED TenantContextService\n * 3. Cleans up after message is processed\n *\n * Expected message format:\n * {\n * dto: { ... },\n * tenant: {\n * tenantId: 'abc-123',\n * tenantSlug: 'acme',\n * tenantType: 'ENTERPRISE',\n * databaseHost: 'enterprise-1.aws.com',\n * databaseName: 'acme_db',\n * ...\n * }\n * }\n *\n * @example\n * // In microservice module\n * {\n * provide: APP_INTERCEPTOR,\n * useClass: MessageTenantContextInterceptor,\n * }\n */\n@Injectable({ scope: Scope.REQUEST })\nexport class MessageTenantContextInterceptor implements NestInterceptor {\n private readonly logger = new Logger(MessageTenantContextInterceptor.name);\n\n constructor(private readonly tenantContext: TenantContextService) {}\n\n intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n const contextType = context.getType();\n\n // Only handle RabbitMQ/microservice messages\n if (contextType === 'rpc') {\n const rpcContext = context.switchToRpc();\n const payload = rpcContext.getData();\n\n // Extract tenant from message payload\n if (payload && payload.tenant) {\n const tenant = payload.tenant as TenantInfo;\n\n this.logger.debug(`Setting tenant context from message: ${tenant.subdomain}`);\n\n try {\n this.tenantContext.setTenant(tenant);\n this.logger.log(`Tenant context set: ${tenant.subdomain} (${tenant.type})`);\n } catch (error) {\n this.logger.error('Failed to set tenant context from message', error);\n }\n } else {\n this.logger.warn('Message payload missing tenant information');\n }\n }\n\n // Execute handler and clean up after\n return next.handle().pipe(\n tap({\n next: () => {\n this.cleanupContext();\n },\n error: () => {\n this.cleanupContext();\n },\n complete: () => {\n this.cleanupContext();\n },\n }),\n );\n }\n\n /**\n * Clean up tenant context after message is processed\n */\n private cleanupContext(): void {\n if (this.tenantContext.hasTenant()) {\n const tenant = this.tenantContext.getTenantIdSafe();\n this.tenantContext.clearTenant();\n this.logger.debug(`Cleaned up tenant context: ${tenant}`);\n }\n }\n}\n","import {\n CallHandler,\n ExecutionContext,\n Inject,\n Injectable,\n Logger,\n NestInterceptor,\n Scope,\n UnauthorizedException,\n} from '@nestjs/common';\nimport type { FastifyRequest } from 'fastify';\nimport { Observable } from 'rxjs';\nimport { DATABASE_MODULE_OPTIONS } from '../constants';\nimport type { DatabaseModuleOptions } from '../interfaces';\nimport { PrimaryDatabaseService } from '../services/primary-database.service';\nimport { TenantContextService } from '../services/tenant-context.service';\nimport { extractSubdomain } from '../utils/subdomain-parser.util';\n\n/**\n * Interceptor that extracts tenant context from HTTP requests (Gateway Mode)\n *\n * This interceptor runs BEFORE the controller and:\n * 1. Extracts tenant identifier from request (tries subdomain first, then falls back to header)\n * 2. Queries primary database for tenant configuration\n * 3. Stores tenant info in REQUEST-SCOPED TenantContextService\n *\n * Tenant resolution order:\n * - First: Subdomain (e.g., acme.vritti.com → 'acme')\n * - Fallback: x-tenant-id or x-tenant-slug header\n *\n * Only used in API Gateway. Microservices use MessageTenantContextInterceptor instead.\n *\n * @example\n * // Request: https://acme.vritti.com/api/users\n * // Interceptor extracts \"acme\" from subdomain, queries primary DB, sets context\n */\n@Injectable({ scope: Scope.REQUEST })\nexport class TenantContextInterceptor implements NestInterceptor {\n private readonly logger = new Logger(TenantContextInterceptor.name);\n\n constructor(\n private readonly tenantContext: TenantContextService,\n private readonly primaryDatabase: PrimaryDatabaseService,\n @Inject(DATABASE_MODULE_OPTIONS)\n private readonly options: DatabaseModuleOptions,\n ) {}\n\n async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {\n const request = context.switchToHttp().getRequest();\n\n this.logger.debug(`Processing request: ${request.method} ${request.url}`);\n\n try {\n // Extract tenant identifier from request\n const tenantIdentifier = this.extractTenantIdentifier(request);\n\n if (!tenantIdentifier) {\n throw new UnauthorizedException('Tenant identifier not found in request');\n }\n\n this.logger.debug(`Tenant identifier extracted: ${tenantIdentifier}`);\n\n // Special case: cloud.vritti.com (platform admin)\n if (tenantIdentifier === 'cloud') {\n this.logger.log('Cloud platform access detected, skipping tenant context setup');\n return next.handle();\n }\n\n // Query primary database for tenant configuration\n const tenantInfo = await this.primaryDatabase.getTenantInfo(tenantIdentifier);\n\n if (!tenantInfo) {\n this.logger.warn(`Invalid tenant: ${tenantIdentifier}`);\n throw new UnauthorizedException('Invalid tenant');\n }\n\n if (tenantInfo.status !== 'ACTIVE') {\n this.logger.warn(`Tenant ${tenantIdentifier} has status: ${tenantInfo.status}`);\n throw new UnauthorizedException(`Tenant is ${tenantInfo.status}`);\n }\n\n this.logger.debug(`Tenant config loaded: ${tenantInfo.subdomain} (${tenantInfo.type})`);\n\n // Store in REQUEST-SCOPED context\n this.tenantContext.setTenant(tenantInfo);\n\n // Also attach to request object for easy access\n (request as any).tenant = tenantInfo;\n\n this.logger.log(`Tenant context set: ${tenantInfo.subdomain}`);\n } catch (error) {\n this.logger.error('Failed to set tenant context', error);\n throw error;\n }\n\n return next.handle();\n }\n\n /**\n * Extract tenant identifier: tries subdomain first, then falls back to header\n */\n private extractTenantIdentifier(request: FastifyRequest): string | null {\n // Try subdomain first\n let tenantIdentifier = this.extractFromSubdomain(request);\n\n // Fallback to header if subdomain not found\n if (!tenantIdentifier) {\n tenantIdentifier = this.extractFromHeader(request);\n if (tenantIdentifier) {\n this.logger.debug('Using tenant from header (subdomain not found)');\n }\n }\n\n return tenantIdentifier;\n }\n\n /**\n * Extract tenant from subdomain\n * @example acme.vritti.com → 'acme'\n */\n private extractFromSubdomain(request: FastifyRequest): string | null {\n // Check if middleware already extracted it\n if ((request as any).subdomain) {\n return (request as any).subdomain;\n }\n\n // Otherwise parse from host header\n const host = request.headers?.host || request.hostname;\n return extractSubdomain(host);\n }\n\n /**\n * Extract tenant from HTTP headers\n * Checks x-tenant-id and x-subdomain headers\n */\n private extractFromHeader(request: FastifyRequest): string | null {\n const getHeader = (key: string) => {\n const value = request.headers?.[key];\n return Array.isArray(value) ? value[0] : value;\n };\n\n return getHeader('x-tenant-id') || getHeader('x-subdomain') || null;\n }\n}\n","/**\n * Extract subdomain from hostname\n *\n * @param host Full hostname (e.g., 'acme.vritti.com:3000' or 'acme.vritti.com')\n * @returns Subdomain or null if not found\n *\n * @example\n * extractSubdomain('acme.vritti.com') // 'acme'\n * extractSubdomain('staging-acme.vritti.com') // 'staging-acme'\n * extractSubdomain('localhost') // null\n * extractSubdomain('vritti.com') // null\n */\nexport function extractSubdomain(host: string): string | null {\n if (!host) {\n return null;\n }\n\n // Remove port if present: acme.vritti.com:3000 → acme.vritti.com\n const hostname = host.split(':')[0];\n\n if (!hostname) {\n return null;\n }\n\n // Split by dots\n const parts = hostname.split('.');\n\n // Need at least 3 parts for subdomain (subdomain.domain.tld)\n if (parts.length < 3) {\n return null;\n }\n\n // Return first part as subdomain\n return parts[0] ?? null;\n}\n","import { createParamDecorator, ExecutionContext } from '@nestjs/common';\nimport { TenantInfo } from '../interfaces';\nimport { TenantContextService } from '../services/tenant-context.service';\n\n/**\n * Parameter decorator that injects tenant metadata into controller method\n *\n * This decorator retrieves tenant information (ID, slug, type, etc.)\n * from the REQUEST-SCOPED TenantContextService.\n *\n * Useful for:\n * - Logging tenant-specific information\n * - Implementing tenant-specific business logic\n * - Auditing and tracking\n * - Conditional feature flags\n *\n * @returns TenantInfo object with tenant metadata\n *\n * @example\n * // Access tenant metadata\n * @Get('info')\n * async getTenantInfo(@Tenant() tenant: TenantInfo) {\n * return {\n * tenantId: tenant.tenantId,\n * tenantSlug: tenant.tenantSlug,\n * tenantType: tenant.tenantType,\n * };\n * }\n *\n * @example\n * // Use for logging\n * @Post()\n * async createUser(\n * @Body() dto: CreateUserDto,\n * @Tenant() tenant: TenantInfo,\n * ) {\n * this.logger.log(`Creating user for tenant: ${tenant.tenantSlug}`);\n * // ...\n * }\n *\n * @example\n * // Conditional business logic\n * @Get('features')\n * async getFeatures(@Tenant() tenant: TenantInfo) {\n * if (tenant.tenantType === 'ENTERPRISE') {\n * return ['feature-a', 'feature-b', 'feature-c'];\n * }\n * return ['feature-a'];\n * }\n */\nexport const Tenant = createParamDecorator((data: unknown, ctx: ExecutionContext): TenantInfo => {\n const request = ctx.switchToHttp().getRequest();\n\n // Get from TenantContextService\n const tenantContext = request.app?.get?.(TenantContextService);\n\n if (!tenantContext) {\n throw new Error('TenantContextService not found.');\n }\n\n return tenantContext.getTenant();\n});\n"],"mappings":";;;;AAAA,SAAwBA,QAAQC,cAAwB;;;ACAjD,IAAMC,0BAA0BC,OAAO,yBAAA;;;ACA9C,SACEC,QACAC,YACAC,8BACAC,cAGK;;;;;;;;;;;;;;;;;;AAmBA,IAAMC,yBAAN,MAAMA,wBAAAA;SAAAA;;;;EACMC,SAAS,IAAIC,OAAOF,wBAAuBG,IAAI;;EAGxDC;;EAGSC,oBAAoB,oBAAIC,IAAAA;;EAGxBC;EAEjB,YAEmBC,SACjB;SADiBA,UAAAA;AAEjB,SAAKD,WAAWC,QAAQC,sBAAsB;EAChD;EAEA,MAAMC,eAAe;AAEnB,QAAI,KAAKF,QAAQG,WAAW;AAC1B,YAAM,KAAKC,0BAAyB;IACtC;EACF;;;;EAKA,MAAcA,4BAA2C;AACvD,QAAI;AACF,YAAMC,kBAAkB,KAAKL,QAAQM;AAGrC,YAAMC,cAAc,KAAKC,kBAAiB;AAE1C,WAAKZ,kBAAkB,IAAIS,gBAAgB;QACzCI,aAAa;UACXC,IAAI;YACFC,KAAKJ;UACP;QACF;QACAK,KAAK;UAAC;UAAS;;MACjB,CAAA;AAEA,YAAM,KAAKhB,gBAAgBiB,SAAQ;AACnC,WAAKpB,OAAOmB,IAAI,iDAAA;IAClB,SAASE,OAAO;AACd,WAAKrB,OAAOqB,MAAM,yCAAyCA,KAAAA;AAC3D,YAAM,IAAIC,6BAA6B,sCAAA;IACzC;EACF;;;;EAKQP,oBAA4B;AAClC,QAAI,CAAC,KAAKR,QAAQG,WAAW;AAC3B,YAAM,IAAIa,MAAM,6CAAA;IAClB;AAEA,UAAM,EACJC,MACAC,OAAO,MACPC,UACAC,UACAC,UACAC,SAAS,UACTC,UAAU,UAAS,IACjB,KAAKvB,QAAQG;AAGjB,QAAIQ,MAAM,gBAAgBQ,QAAAA,IAAYC,QAAAA,IAAYH,IAAAA,IAAQC,IAAAA,IAAQG,QAAAA;AAGlE,UAAMG,SAAS,IAAIC,gBAAAA;AACnB,QAAIH,QAAQ;AACVE,aAAOE,IAAI,UAAUJ,MAAAA;IACvB;AACAE,WAAOE,IAAI,WAAWH,OAAAA;AAEtB,UAAMI,cAAcH,OAAOI,SAAQ;AACnC,QAAID,aAAa;AACfhB,aAAO,IAAIgB,WAAAA;IACb;AAEA,SAAKlC,OAAOoC,MAAM,8BAA8B,KAAKC,aAAanB,GAAAA,CAAAA,EAAM;AAExE,WAAOA;EACT;;;;EAKQmB,aAAanB,KAAqB;AACxC,WAAOA,IAAIoB,QAAQ,aAAa,QAAA;EAClC;;;;;;;EAQA,MAAMC,cAAcC,kBAAsD;AAExE,UAAMC,SAAS,KAAKrC,kBAAkBsC,IAAIF,gBAAAA;AAC1C,QAAIC,QAAQ;AACV,WAAKzC,OAAOoC,MAAM,yBAAyBI,gBAAAA,EAAkB;AAC7D,aAAOC;IACT;AAGA,QAAI;AACF,UAAI,CAAC,KAAKtC,iBAAiB;AACzB,cAAM,IAAIoB,MAAM,yCAAA;MAClB;AAEA,WAAKvB,OAAOoC,MAAM,yCAAyCI,gBAAAA,EAAkB;AAE7E,YAAMG,SAAS,MAAM,KAAKxC,gBAAgBwC,OAAOC,UAAU;QACzDC,OAAO;UACLC,IAAI;YAAC;cAAEC,IAAIP;YAAiB;YAAG;cAAEQ,WAAWR;YAAiB;;UAC7DS,QAAQ;QACV;MACF,CAAA;AAEA,UAAI,CAACN,QAAQ;AACX,aAAK3C,OAAOkD,KAAK,qBAAqBV,gBAAAA,EAAkB;AACxD,eAAO;MACT;AAGA,YAAMW,OAAmB;QACvBJ,IAAIJ,OAAOI;QACXK,WAAWT,OAAOS;QAClBC,MAAMV,OAAOU;QACbJ,QAAQN,OAAOM;QACfK,YAAYX,OAAOW,cAAcC;QACjCC,cAAcb,OAAOa,gBAAgBD;QACrCE,cAAcd,OAAOc,gBAAgBF;QACrCG,cAAcf,OAAOe,gBAAgBH;QACrCI,kBAAkBhB,OAAOgB,mBACrB,KAAKC,QAAQjB,OAAOgB,gBAAgB,IACpCJ;QACJM,kBAAkBlB,OAAOkB,mBACrB,KAAKD,QAAQjB,OAAOkB,gBAAgB,IACpCN;QACJO,iBAAiBnB,OAAOmB,mBAAmBP;QAC3CQ,oBAAoBpB,OAAOoB,sBAAsBR;MACnD;AAGA,WAAKS,UAAUb,IAAAA;AAEf,aAAOA;IACT,SAAS9B,OAAO;AACd,WAAKrB,OAAOqB,MAAM,gCAAgCmB,gBAAAA,IAAoBnB,KAAAA;AACtE,YAAM,IAAIC,6BAA6B,0BAAA;IACzC;EACF;;;;EAKQ0C,UAAUb,MAAwB;AACxC,SAAK/C,kBAAkB6B,IAAIkB,KAAKJ,IAAII,IAAAA;AACpC,SAAK/C,kBAAkB6B,IAAIkB,KAAKC,WAAWD,IAAAA;AAG3Cc,eAAW,MAAA;AACT,WAAK7D,kBAAkB8D,OAAOf,KAAKJ,EAAE;AACrC,WAAK3C,kBAAkB8D,OAAOf,KAAKC,SAAS;AAC5C,WAAKpD,OAAOoC,MAAM,6BAA6Be,KAAKC,SAAS,EAAE;IACjE,GAAG,KAAK9C,QAAQ;EAClB;;;;;;;;EASA6D,iBAAiB3B,kBAAgC;AAC/C,UAAM4B,SAAS,KAAKhE,kBAAkBsC,IAAIF,gBAAAA;AAC1C,QAAI4B,QAAQ;AACV,WAAKhE,kBAAkB8D,OAAOE,OAAOrB,EAAE;AACvC,WAAK3C,kBAAkB8D,OAAOE,OAAOhB,SAAS;AAC9C,WAAKpD,OAAOmB,IAAI,6BAA6BqB,gBAAAA,EAAkB;IACjE;EACF;;;;EAKA6B,iBAAuB;AACrB,UAAMC,OAAO,KAAKlE,kBAAkBkE;AACpC,SAAKlE,kBAAkBmE,MAAK;AAC5B,SAAKvE,OAAOmB,IAAI,WAAWmD,IAAAA,wBAA4B;EACzD;;;;;;;;;EAUAE,qBAAiC;AAC/B,QAAI,CAAC,KAAKrE,iBAAiB;AACzB,YAAM,IAAIoB,MAAM,mEAAA;IAClB;AACA,WAAO,KAAKpB;EACd;;;;;;;;;EAUQyD,QAAQa,WAA2B;AAGzC,WAAOA;EACT;EAEA,MAAMC,kBAAkB;AACtB,QAAI,KAAKvE,iBAAiB;AACxB,YAAM,KAAKA,gBAAgBwE,YAAW;AACtC,WAAK3E,OAAOmB,IAAI,oCAAA;IAClB;EACF;AACF;;;;;;;;;;;ACvQA,SAASyD,cAAAA,aAAYC,OAAOC,6BAA6B;;;;;;;;AAoBlD,IAAMC,uBAAN,MAAMA;SAAAA;;;EACHC,aAAgC;;;;;;;;;;;;EAaxCC,UAAUD,YAA8B;AACtC,QAAI,KAAKA,YAAY;AACnB,YAAM,IAAIE,MAAM,6CAAA;IAClB;AACA,SAAKF,aAAaA;EACpB;;;;;;;EAQAG,YAAwB;AACtB,QAAI,CAAC,KAAKH,YAAY;AACpB,YAAM,IAAII,sBAAsB,wBAAA;IAClC;AACA,WAAO,KAAKJ;EACd;;;;;;EAOAK,YAAqB;AACnB,WAAO,KAAKL,eAAe;EAC7B;;;;;;;;;;EAWAM,cAAoB;AAClB,SAAKN,aAAa;EACpB;;;;;;EAOAO,kBAAiC;AAC/B,WAAO,KAAKP,YAAYQ,MAAM;EAChC;;;;;;EAOAC,yBAAwC;AACtC,WAAO,KAAKT,YAAYU,aAAa;EACvC;AACF;;;IA1EcC,OAAOC,MAAMC;;;;;ACnB3B,SACEC,UAAAA,SACAC,cAAAA,aACAC,gCAAAA,+BACAC,UAAAA,eAEK;;;;;;;;;;;;;;;;;;AAqBA,IAAMC,wBAAN,MAAMA,uBAAAA;SAAAA;;;;;EACMC,SAAS,IAAIC,QAAOF,uBAAsBG,IAAI;;EAG9CC,UAAU,oBAAIC,IAAAA;;EAGdC,iBAAiB,oBAAID,IAAAA;;EAG9BE;EAER,YAEmBC,SACAC,eACjB;SAFiBD,UAAAA;SACAC,gBAAAA;AAEjB,SAAKC,uBAAsB;EAC7B;;;;;;;;;;;;;;;;;EAkBA,MAAMC,cAAmC;AACvC,UAAMC,SAAS,KAAKH,cAAcI,UAAS;AAE3C,UAAMC,WAAW,KAAKC,cAAcH,MAAAA;AAGpC,QAAI,KAAKR,QAAQY,IAAIF,QAAAA,GAAW;AAC9B,WAAKR,eAAeW,IAAIH,UAAUI,KAAKC,IAAG,CAAA;AAC1C,WAAKlB,OAAOmB,MAAM,8BAA8BN,QAAAA,EAAU;AAC1D,aAAO,KAAKV,QAAQiB,IAAIP,QAAAA;IAC1B;AAGA,SAAKb,OAAOqB,IAAI,qCAAqCR,QAAAA,EAAU;AAC/D,UAAMS,SAAS,MAAM,KAAKC,eAAeZ,MAAAA;AACzC,SAAKR,QAAQa,IAAIH,UAAUS,MAAAA;AAC3B,SAAKjB,eAAeW,IAAIH,UAAUI,KAAKC,IAAG,CAAA;AAE1C,WAAOI;EACT;;;;EAKA,MAAcC,eAAeZ,QAAkC;AAC7D,QAAI;AAEF,YAAMa,cAAc,KAAKC,iBAAiBd,MAAAA;AAG1C,YAAMe,eAAe,MAAM,KAAKnB,QAAQoB;AAGxC,YAAML,SAAS,IAAII,aAAa;QAC9BE,aAAa;UACXC,IAAI;YAAEC,KAAKN;UAAY;QACzB;QACAH,KAAK;UAAC;UAAS;;MACjB,CAAA;AAEA,YAAMC,OAAOS,SAAQ;AACrB,WAAK/B,OAAOqB,IAAI,qCAAqCV,OAAOqB,SAAS,EAAE;AAEvE,aAAOV;IACT,SAASW,OAAO;AACd,WAAKjC,OAAOiC,MACV,oDAAoDtB,OAAOqB,SAAS,IACpEC,KAAAA;AAEF,YAAM,IAAIC,8BAA6B,sCAAA;IACzC;EACF;;;;EAKQT,iBAAiBd,QAA4B;AACnD,UAAM,EACJwB,cACAC,cACAC,cACAC,kBACAC,kBACAC,gBAAe,IACb7B;AAEJ,QAAI,CAACwB,gBAAgB,CAACE,gBAAgB,CAACC,kBAAkB;AACvD,YAAM,IAAIG,MAAM,qBAAqB9B,OAAOqB,SAAS,iCAAiC;IACxF;AAEA,UAAMU,OAAON,gBAAgB;AAC7B,UAAMO,UAAUH,mBAAmB;AACnC,UAAMI,gBAAgB,gBAAgBN,gBAAAA,IAAoBC,gBAAAA,IAAoBJ,YAAAA,IAAgBO,IAAAA,IAAQL,YAAAA,YAAwBM,OAAAA;AAE9H,SAAK3C,OAAOmB,MAAM,8BAA8B,KAAK0B,aAAaD,aAAAA,CAAAA,EAAgB;AAElF,WAAOA;EACT;;;;EAKQ9B,cAAcH,QAA4B;AAChD,WAAO,GAAGA,OAAOmC,IAAI,IAAInC,OAAO0B,YAAY,IAAI1B,OAAOwB,YAAY;EACrE;;;;EAKQ1B,yBAA+B;AACrC,UAAMsC,WAAW,KAAKxC,QAAQyC,sBAAsB;AAEpD,SAAK1C,kBAAkB2C,YAAY,MAAA;AACjC,WAAKC,uBAAsB;IAC7B,GAAGH,QAAAA;AAEH,SAAK/C,OAAOqB,IAAI,sCAAsC0B,WAAW,GAAA,UAAc;EACjF;;;;EAKQG,yBAA+B;AACrC,UAAMhC,MAAMD,KAAKC,IAAG;AACpB,UAAMiC,UAAU,KAAK5C,QAAQyC,sBAAsB;AAEnD,QAAII,UAAU;AAEd,eAAW,CAACC,KAAKC,QAAAA,KAAa,KAAKjD,eAAekD,QAAO,GAAI;AAC3D,UAAIrC,MAAMoC,WAAWH,SAAS;AAC5B,cAAM7B,SAAS,KAAKnB,QAAQiB,IAAIiC,GAAAA;AAChC,YAAI/B,QAAQ;AACVA,iBACGkC,YAAW,EACXC,KAAK,MAAA;AACJ,iBAAKzD,OAAOmB,MAAM,+BAA+BkC,GAAAA,EAAK;UACxD,CAAA,EACCK,MAAM,CAACzB,UAAAA;AACN,iBAAKjC,OAAOiC,MAAM,oCAAoCoB,GAAAA,IAAOpB,KAAAA;UAC/D,CAAA;AAEF,eAAK9B,QAAQwD,OAAON,GAAAA;AACpB,eAAKhD,eAAesD,OAAON,GAAAA;AAC3BD;QACF;MACF;IACF;AAEA,QAAIA,UAAU,GAAG;AACf,WAAKpD,OAAOqB,IAAI,cAAc+B,OAAAA,mBAA0B;IAC1D;EACF;;;;EAKAQ,eAGE;AACA,WAAO;MACLC,mBAAmB,KAAK1D,QAAQ2D;MAChCC,SAASC,MAAMC,KAAK,KAAK9D,QAAQ+D,KAAI,CAAA;IACvC;EACF;;;;EAKQrB,aAAaf,KAAqB;AACxC,WAAOA,IAAIqC,QAAQ,aAAa,QAAA;EAClC;EAEA,MAAMC,kBAAkB;AAEtB,QAAI,KAAK9D,iBAAiB;AACxB+D,oBAAc,KAAK/D,eAAe;IACpC;AAGA,SAAKN,OAAOqB,IAAI,iBAAiB,KAAKlB,QAAQ2D,IAAI,uBAAuB;AAEzE,UAAMQ,qBAAqBN,MAAMC,KAAK,KAAK9D,QAAQoD,QAAO,CAAA,EAAIgB,IAAI,OAAO,CAAClB,KAAK/B,MAAAA,MAAO;AACpF,UAAI;AACF,cAAMA,OAAOkC,YAAW;AACxB,aAAKxD,OAAOmB,MAAM,iBAAiBkC,GAAAA,EAAK;MAC1C,SAASpB,OAAO;AACd,aAAKjC,OAAOiC,MAAM,+BAA+BoB,GAAAA,IAAOpB,KAAAA;MAC1D;IACF,CAAA;AAEA,UAAMuC,QAAQC,IAAIH,kBAAAA;AAClB,SAAKtE,OAAOqB,IAAI,iCAAA;EAClB;AACF;;;;;;;;;;;;;;;;;;;AJxLO,IAAMqD,iBAAN,MAAMA,gBAAAA;SAAAA;;;;;;;;;EAOX,OAAOC,QAAQC,SAA+C;AAC5D,UAAMC,YAAwB;MAC5B;QACEC,SAASC;QACTC,UAAUJ;MACZ;MACAK;MACAC;MACAC;;AAGF,WAAO;MACLC,QAAQV;MACRG;MACAQ,SAAS;QAACJ;QAAuBC;QAAsBC;;IACzD;EACF;;;;;;;;;;;;;;;;;;;;EAqBA,OAAOG,aAAaV,SAGF;AAChB,UAAMW,gBAA0B;MAC9BT,SAASC;MACTS,YAAYZ,QAAQY;MACpBC,QAAQb,QAAQa,UAAU,CAAA;IAC5B;AACA,WAAO;MACLL,QAAQV;MACRG,WAAW;QACTU;QACAL;QACAC;QACAF;;MAEFI,SAAS;QAACJ;QAAuBC;QAAsBC;QAAwBI;;IACjF;EACF;AACF;;;;;;;AKrHA,SAGEG,cAAAA,aACAC,UAAAA,SAEAC,SAAAA,cACK;AAEP,SAASC,WAAW;;;;;;;;;;;;AAiCb,IAAMC,kCAAN,MAAMA,iCAAAA;SAAAA;;;;EACMC,SAAS,IAAIC,QAAOF,iCAAgCG,IAAI;EAEzE,YAA6BC,eAAqC;SAArCA,gBAAAA;EAAsC;EAEnEC,UAAUC,SAA2BC,MAAoC;AACvE,UAAMC,cAAcF,QAAQG,QAAO;AAGnC,QAAID,gBAAgB,OAAO;AACzB,YAAME,aAAaJ,QAAQK,YAAW;AACtC,YAAMC,UAAUF,WAAWG,QAAO;AAGlC,UAAID,WAAWA,QAAQE,QAAQ;AAC7B,cAAMA,SAASF,QAAQE;AAEvB,aAAKb,OAAOc,MAAM,wCAAwCD,OAAOE,SAAS,EAAE;AAE5E,YAAI;AACF,eAAKZ,cAAca,UAAUH,MAAAA;AAC7B,eAAKb,OAAOiB,IAAI,uBAAuBJ,OAAOE,SAAS,KAAKF,OAAOK,IAAI,GAAG;QAC5E,SAASC,OAAO;AACd,eAAKnB,OAAOmB,MAAM,6CAA6CA,KAAAA;QACjE;MACF,OAAO;AACL,aAAKnB,OAAOoB,KAAK,4CAAA;MACnB;IACF;AAGA,WAAOd,KAAKe,OAAM,EAAGC,KACnBC,IAAI;MACFjB,MAAM,6BAAA;AACJ,aAAKkB,eAAc;MACrB,GAFM;MAGNL,OAAO,6BAAA;AACL,aAAKK,eAAc;MACrB,GAFO;MAGPC,UAAU,6BAAA;AACR,aAAKD,eAAc;MACrB,GAFU;IAGZ,CAAA,CAAA;EAEJ;;;;EAKQA,iBAAuB;AAC7B,QAAI,KAAKrB,cAAcuB,UAAS,GAAI;AAClC,YAAMb,SAAS,KAAKV,cAAcwB,gBAAe;AACjD,WAAKxB,cAAcyB,YAAW;AAC9B,WAAK5B,OAAOc,MAAM,8BAA8BD,MAAAA,EAAQ;IAC1D;EACF;AACF;;;IAzDcgB,OAAOC,OAAMC;;;;;;;;;ACzC3B,SAGEC,UAAAA,SACAC,cAAAA,aACAC,UAAAA,SAEAC,SAAAA,QACAC,yBAAAA,8BACK;;;ACGA,SAASC,iBAAiBC,MAAY;AAC3C,MAAI,CAACA,MAAM;AACT,WAAO;EACT;AAGA,QAAMC,WAAWD,KAAKE,MAAM,GAAA,EAAK,CAAA;AAEjC,MAAI,CAACD,UAAU;AACb,WAAO;EACT;AAGA,QAAME,QAAQF,SAASC,MAAM,GAAA;AAG7B,MAAIC,MAAMC,SAAS,GAAG;AACpB,WAAO;EACT;AAGA,SAAOD,MAAM,CAAA,KAAM;AACrB;AAtBgBJ;;;;;;;;;;;;;;;;;;;;ADyBT,IAAMM,2BAAN,MAAMA,0BAAAA;SAAAA;;;;;;EACMC,SAAS,IAAIC,QAAOF,0BAAyBG,IAAI;EAElE,YACmBC,eACAC,iBAEAC,SACjB;SAJiBF,gBAAAA;SACAC,kBAAAA;SAEAC,UAAAA;EAChB;EAEH,MAAMC,UAAUC,SAA2BC,MAA6C;AACtF,UAAMC,UAAUF,QAAQG,aAAY,EAAGC,WAAU;AAEjD,SAAKX,OAAOY,MAAM,uBAAuBH,QAAQI,MAAM,IAAIJ,QAAQK,GAAG,EAAE;AAExE,QAAI;AAEF,YAAMC,mBAAmB,KAAKC,wBAAwBP,OAAAA;AAEtD,UAAI,CAACM,kBAAkB;AACrB,cAAM,IAAIE,uBAAsB,wCAAA;MAClC;AAEA,WAAKjB,OAAOY,MAAM,gCAAgCG,gBAAAA,EAAkB;AAGpE,UAAIA,qBAAqB,SAAS;AAChC,aAAKf,OAAOkB,IAAI,+DAAA;AAChB,eAAOV,KAAKW,OAAM;MACpB;AAGA,YAAMC,aAAa,MAAM,KAAKhB,gBAAgBiB,cAAcN,gBAAAA;AAE5D,UAAI,CAACK,YAAY;AACf,aAAKpB,OAAOsB,KAAK,mBAAmBP,gBAAAA,EAAkB;AACtD,cAAM,IAAIE,uBAAsB,gBAAA;MAClC;AAEA,UAAIG,WAAWG,WAAW,UAAU;AAClC,aAAKvB,OAAOsB,KAAK,UAAUP,gBAAAA,gBAAgCK,WAAWG,MAAM,EAAE;AAC9E,cAAM,IAAIN,uBAAsB,aAAaG,WAAWG,MAAM,EAAE;MAClE;AAEA,WAAKvB,OAAOY,MAAM,yBAAyBQ,WAAWI,SAAS,KAAKJ,WAAWK,IAAI,GAAG;AAGtF,WAAKtB,cAAcuB,UAAUN,UAAAA;AAG5BX,cAAgBkB,SAASP;AAE1B,WAAKpB,OAAOkB,IAAI,uBAAuBE,WAAWI,SAAS,EAAE;IAC/D,SAASI,OAAO;AACd,WAAK5B,OAAO4B,MAAM,gCAAgCA,KAAAA;AAClD,YAAMA;IACR;AAEA,WAAOpB,KAAKW,OAAM;EACpB;;;;EAKQH,wBAAwBP,SAAwC;AAEtE,QAAIM,mBAAmB,KAAKc,qBAAqBpB,OAAAA;AAGjD,QAAI,CAACM,kBAAkB;AACrBA,yBAAmB,KAAKe,kBAAkBrB,OAAAA;AAC1C,UAAIM,kBAAkB;AACpB,aAAKf,OAAOY,MAAM,gDAAA;MACpB;IACF;AAEA,WAAOG;EACT;;;;;EAMQc,qBAAqBpB,SAAwC;AAEnE,QAAKA,QAAgBe,WAAW;AAC9B,aAAQf,QAAgBe;IAC1B;AAGA,UAAMO,OAAOtB,QAAQuB,SAASD,QAAQtB,QAAQwB;AAC9C,WAAOC,iBAAiBH,IAAAA;EAC1B;;;;;EAMQD,kBAAkBrB,SAAwC;AAChE,UAAM0B,YAAY,wBAACC,QAAAA;AACjB,YAAMC,QAAQ5B,QAAQuB,UAAUI,GAAAA;AAChC,aAAOE,MAAMC,QAAQF,KAAAA,IAASA,MAAM,CAAA,IAAKA;IAC3C,GAHkB;AAKlB,WAAOF,UAAU,aAAA,KAAkBA,UAAU,aAAA,KAAkB;EACjE;AACF;;;IA3GcK,OAAOC,OAAMC;;;;;;;;;;;;AEpC3B,SAASC,4BAA8C;AAkDhD,IAAMC,SAASC,qBAAqB,CAACC,MAAeC,QAAAA;AACzD,QAAMC,UAAUD,IAAIE,aAAY,EAAGC,WAAU;AAG7C,QAAMC,gBAAgBH,QAAQI,KAAKC,MAAMC,oBAAAA;AAEzC,MAAI,CAACH,eAAe;AAClB,UAAM,IAAII,MAAM,iCAAA;EAClB;AAEA,SAAOJ,cAAcK,UAAS;AAChC,CAAA;","names":["Global","Module","DATABASE_MODULE_OPTIONS","Symbol","Inject","Injectable","InternalServerErrorException","Logger","PrimaryDatabaseService","logger","Logger","name","primaryDbClient","tenantConfigCache","Map","cacheTTL","options","connectionCacheTTL","onModuleInit","primaryDb","initializePrimaryDbClient","PrimaryDbClient","prismaClientConstructor","databaseUrl","buildPrimaryDbUrl","datasources","db","url","log","$connect","error","InternalServerErrorException","Error","host","port","username","password","database","schema","sslMode","params","URLSearchParams","set","queryString","toString","debug","maskPassword","replace","getTenantInfo","tenantIdentifier","cached","get","tenant","findFirst","where","OR","id","subDomain","status","warn","info","subdomain","type","schemaName","undefined","databaseName","databaseHost","databasePort","databaseUsername","decrypt","databasePassword","databaseSslMode","connectionPoolSize","cacheInfo","setTimeout","delete","clearTenantCache","config","clearAllCaches","size","clear","getPrimaryDbClient","encrypted","onModuleDestroy","$disconnect","Injectable","Scope","UnauthorizedException","TenantContextService","tenantInfo","setTenant","Error","getTenant","UnauthorizedException","hasTenant","clearTenant","getTenantIdSafe","id","getTenantSubdomainSafe","subdomain","scope","Scope","REQUEST","Inject","Injectable","InternalServerErrorException","Logger","TenantDatabaseService","logger","Logger","name","clients","Map","clientLastUsed","cleanupInterval","options","tenantContext","startConnectionCleaner","getDbClient","tenant","getTenant","cacheKey","buildCacheKey","has","set","Date","now","debug","get","log","client","createDbClient","databaseUrl","buildTenantDbUrl","PrismaClient","prismaClientConstructor","datasources","db","url","$connect","subdomain","error","InternalServerErrorException","databaseHost","databasePort","databaseName","databaseUsername","databasePassword","databaseSslMode","Error","port","sslMode","connectionUrl","maskPassword","type","interval","connectionCacheTTL","setInterval","cleanupIdleConnections","maxIdle","cleaned","key","lastUsed","entries","$disconnect","then","catch","delete","getPoolStats","activeConnections","size","tenants","Array","from","keys","replace","onModuleDestroy","clearInterval","disconnectPromises","map","Promise","all","DatabaseModule","forRoot","options","providers","provide","DATABASE_MODULE_OPTIONS","useValue","TenantDatabaseService","TenantContextService","PrimaryDatabaseService","module","exports","forRootAsync","asyncProvider","useFactory","inject","Injectable","Logger","Scope","tap","MessageTenantContextInterceptor","logger","Logger","name","tenantContext","intercept","context","next","contextType","getType","rpcContext","switchToRpc","payload","getData","tenant","debug","subdomain","setTenant","log","type","error","warn","handle","pipe","tap","cleanupContext","complete","hasTenant","getTenantIdSafe","clearTenant","scope","Scope","REQUEST","Inject","Injectable","Logger","Scope","UnauthorizedException","extractSubdomain","host","hostname","split","parts","length","TenantContextInterceptor","logger","Logger","name","tenantContext","primaryDatabase","options","intercept","context","next","request","switchToHttp","getRequest","debug","method","url","tenantIdentifier","extractTenantIdentifier","UnauthorizedException","log","handle","tenantInfo","getTenantInfo","warn","status","subdomain","type","setTenant","tenant","error","extractFromSubdomain","extractFromHeader","host","headers","hostname","extractSubdomain","getHeader","key","value","Array","isArray","scope","Scope","REQUEST","createParamDecorator","Tenant","createParamDecorator","data","ctx","request","switchToHttp","getRequest","tenantContext","app","get","TenantContextService","Error","getTenant"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vritti/api-sdk",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "scripts": {
34
34
  "dev": "tsx --watch src/index.ts",
35
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
35
+ "build": "tsup",
36
36
  "type-check": "tsc --noEmit",
37
37
  "test": "tsx --test src/**/*.test.ts",
38
38
  "test:watch": "tsx --test --watch src/**/*.test.ts",
@@ -42,11 +42,25 @@
42
42
  "clean": "rm -rf dist",
43
43
  "prepublishOnly": "yarn type-check && yarn test && yarn build"
44
44
  },
45
+ "peerDependencies": {
46
+ "@nestjs/common": "^11.0.0",
47
+ "@nestjs/core": "^11.0.0",
48
+ "reflect-metadata": "^0.2.0",
49
+ "rxjs": "^7.8.0"
50
+ },
45
51
  "devDependencies": {
46
52
  "@eslint/js": "^9.38.0",
47
- "@types/node": "^24.9.1",
53
+ "@nestjs/common": "^11.0.1",
54
+ "@nestjs/core": "^11.0.1",
55
+ "@prisma/client": "^6.18.0",
56
+ "@swc/core": "^1.13.5",
57
+ "@types/node": "^24.9.2",
48
58
  "eslint": "^9.38.0",
59
+ "fastify": "^5.6.1",
49
60
  "prettier": "^3.6.2",
61
+ "prisma": "^6.18.0",
62
+ "reflect-metadata": "^0.2.2",
63
+ "rxjs": "^7.8.1",
50
64
  "tsup": "^8.5.0",
51
65
  "tsx": "^4.20.6",
52
66
  "typescript": "^5.9.3",