@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.d.ts CHANGED
@@ -1,3 +1,534 @@
1
- declare const getHello: () => string;
1
+ import { DynamicModule, OnModuleInit, OnModuleDestroy, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
+ import { Observable } from 'rxjs';
2
3
 
3
- export { getHello };
4
+ /**
5
+ * Primary database connection configuration
6
+ */
7
+ interface PrimaryDbConfig {
8
+ /** Database host */
9
+ host: string;
10
+ /** Database port (default: 5432) */
11
+ port?: number;
12
+ /** Database username */
13
+ username: string;
14
+ /** Database password */
15
+ password: string;
16
+ /** Database name */
17
+ database: string;
18
+ /** Default schema (default: 'public') */
19
+ schema?: string;
20
+ /** SSL mode: 'require' | 'prefer' | 'disable' (default: 'require') */
21
+ sslMode?: 'require' | 'prefer' | 'disable';
22
+ }
23
+ /**
24
+ * Configuration options for DatabaseModule
25
+ */
26
+ interface DatabaseModuleOptions {
27
+ /**
28
+ * Primary database configuration (for tenant registry queries)
29
+ * Only required in gateway mode
30
+ * @example
31
+ * primaryDb: {
32
+ * host: 'aws-pooler.supabase.com',
33
+ * port: 5432,
34
+ * username: 'postgres.xxx',
35
+ * password: 'xxx',
36
+ * database: 'postgres',
37
+ * schema: 'public',
38
+ * sslMode: 'require',
39
+ * }
40
+ */
41
+ primaryDb: PrimaryDbConfig;
42
+ /**
43
+ * Primary database client constructor (for querying tenant registry)
44
+ * Only required in gateway mode
45
+ * @example import { PrismaClient } from '@prisma/client'
46
+ */
47
+ prismaClientConstructor: any;
48
+ /**
49
+ * Connection cache TTL in milliseconds
50
+ * Idle connections will be closed after this period
51
+ * @default 300000 (5 minutes)
52
+ */
53
+ connectionCacheTTL?: number;
54
+ /**
55
+ * Maximum number of concurrent connections per tenant
56
+ * @default 10
57
+ */
58
+ maxConnections?: number;
59
+ /**
60
+ * Encryption key for decrypting database credentials
61
+ * Required if tenant config stores encrypted passwords
62
+ */
63
+ encryptionKey?: string;
64
+ }
65
+
66
+ /**
67
+ * Tenant configuration stored in cloud database
68
+ * This is the shape of data returned from the tenant registry
69
+ */
70
+ interface TenantInfo {
71
+ /** Unique tenant identifier */
72
+ id: string;
73
+ /** Human-readable tenant slug */
74
+ subdomain: string;
75
+ /** Tenant type */
76
+ type: 'SHARED' | 'DEDIACTED';
77
+ /** Tenant status */
78
+ status: string;
79
+ /** For CLOUD tenants: schema name */
80
+ schemaName?: string;
81
+ /** For ENTERPRISE tenants: database configuration */
82
+ databaseName?: string;
83
+ databaseHost?: string;
84
+ databasePort?: number;
85
+ databaseUsername?: string;
86
+ databasePassword?: string;
87
+ databaseSslMode?: string;
88
+ connectionPoolSize?: number;
89
+ }
90
+
91
+ /**
92
+ * Dynamic module for multi-tenant database management
93
+ *
94
+ * This module provides:
95
+ * - Tenant context management (request-scoped)
96
+ * - Database connection pooling
97
+ * - Dynamic schema/cluster routing
98
+ * - Support for both gateway and microservice modes
99
+ *
100
+ * ## Gateway Mode (API Gateway)
101
+ * - Automatically extracts tenant from subdomain, falls back to x-tenant-id header
102
+ * - Provide primaryDb configuration and prismaClientConstructor
103
+ * - Automatically queries primary DB for tenant config
104
+ * - Attaches TenantContextInterceptor globally
105
+ *
106
+ * ## Microservice Mode
107
+ * - Only provide prismaClientConstructor
108
+ * - Tenant context comes from RabbitMQ messages
109
+ * - Use MessageTenantContextInterceptor manually
110
+ *
111
+ * @example
112
+ * // Gateway configuration
113
+ * DatabaseModule.forRootAsync({
114
+ * imports: [ConfigModule],
115
+ * useFactory: (config: ConfigService) => ({
116
+ * primaryDb: {
117
+ * host: config.get('PRIMARY_DB_HOST'),
118
+ * port: config.get('PRIMARY_DB_PORT'),
119
+ * username: config.get('PRIMARY_DB_USERNAME'),
120
+ * password: config.get('PRIMARY_DB_PASSWORD'),
121
+ * database: config.get('PRIMARY_DB_DATABASE'),
122
+ * },
123
+ * prismaClientConstructor: PrismaClient,
124
+ * }),
125
+ * inject: [ConfigService],
126
+ * })
127
+ *
128
+ * @example
129
+ * // Microservice configuration
130
+ * DatabaseModule.forRoot({
131
+ * prismaClientConstructor: PrismaClient,
132
+ * })
133
+ */
134
+ declare class DatabaseModule {
135
+ /**
136
+ * Synchronous configuration
137
+ *
138
+ * @param options Module configuration options
139
+ * @returns Dynamic module configuration
140
+ */
141
+ static forRoot(options: DatabaseModuleOptions): DynamicModule;
142
+ /**
143
+ * Asynchronous configuration (recommended)
144
+ *
145
+ * Allows injecting ConfigService or other dependencies
146
+ *
147
+ * @param options Async configuration options
148
+ * @returns Dynamic module configuration
149
+ *
150
+ * @example
151
+ * DatabaseModule.forRootAsync({
152
+ * imports: [ConfigModule],
153
+ * useFactory: async (config: ConfigService) => ({
154
+ * cloudDatabaseUrl: config.get('CLOUD_DATABASE_URL'),
155
+ * prismaClientConstructor: PrismaClient,
156
+ * tenantResolver: 'subdomain',
157
+ * }),
158
+ * inject: [ConfigService],
159
+ * })
160
+ */
161
+ static forRootAsync(options: {
162
+ useFactory: (...args: any[]) => Promise<DatabaseModuleOptions> | DatabaseModuleOptions;
163
+ inject?: any[];
164
+ }): DynamicModule;
165
+ }
166
+
167
+ /**
168
+ * Service responsible for querying the primary database to resolve tenant configurations
169
+ *
170
+ * This service:
171
+ * - Connects to the primary database (tenant registry)
172
+ * - Queries tenant metadata (database location, credentials, etc.)
173
+ * - Caches tenant configs in memory to reduce database load
174
+ * - Only used in GATEWAY MODE (microservices receive tenant config from messages)
175
+ *
176
+ * @example
177
+ * // In API Gateway
178
+ * const config = await primaryDatabase.getTenantConfig('acme');
179
+ * // Returns: { id, slug, type, databaseHost, databaseName, ... }
180
+ */
181
+ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
182
+ private readonly options;
183
+ private readonly logger;
184
+ /** Primary database client for querying tenant registry */
185
+ private primaryDbClient;
186
+ /** In-memory cache: Map<tenantIdentifier, TenantConfig> */
187
+ private readonly tenantConfigCache;
188
+ /** Cache TTL in milliseconds */
189
+ private readonly cacheTTL;
190
+ constructor(options: DatabaseModuleOptions);
191
+ onModuleInit(): Promise<void>;
192
+ /**
193
+ * Initialize connection to primary database
194
+ */
195
+ private initializePrimaryDbClient;
196
+ /**
197
+ * Build connection URL from primary database properties
198
+ */
199
+ private buildPrimaryDbUrl;
200
+ /**
201
+ * Mask password in connection URL for logging
202
+ */
203
+ private maskPassword;
204
+ /**
205
+ * Get tenant configuration by identifier (ID or slug)
206
+ *
207
+ * @param tenantIdentifier Tenant ID or slug
208
+ * @returns Tenant configuration or null if not found
209
+ */
210
+ getTenantInfo(tenantIdentifier: string): Promise<TenantInfo | null>;
211
+ /**
212
+ * Cache tenant information with TTL
213
+ */
214
+ private cacheInfo;
215
+ /**
216
+ * Clear cached tenant information
217
+ *
218
+ * Useful when tenant settings are updated and cache needs to be invalidated
219
+ *
220
+ * @param tenantIdentifier Tenant ID or slug
221
+ */
222
+ clearTenantCache(tenantIdentifier: string): void;
223
+ /**
224
+ * Clear all cached tenant configurations
225
+ */
226
+ clearAllCaches(): void;
227
+ /**
228
+ * Get primary database client for direct database access
229
+ *
230
+ * This is useful for platform admin operations (creating tenants, billing, etc.)
231
+ *
232
+ * @returns Primary database client instance
233
+ * @throws Error if primary database client is not initialized
234
+ */
235
+ getPrimaryDbClient<T = any>(): T;
236
+ /**
237
+ * Decrypt database credentials
238
+ *
239
+ * Override this method to implement your encryption strategy
240
+ *
241
+ * @param encrypted Encrypted value
242
+ * @returns Decrypted value
243
+ */
244
+ private decrypt;
245
+ onModuleDestroy(): Promise<void>;
246
+ }
247
+
248
+ /**
249
+ * Request-scoped service that holds tenant context for the current request or RabbitMQ message
250
+ *
251
+ * IMPORTANT: This service is REQUEST-SCOPED, meaning NestJS creates a new instance
252
+ * for each HTTP request or RabbitMQ message. This ensures tenant isolation and
253
+ * prevents cross-tenant data leaks in concurrent scenarios.
254
+ *
255
+ * @example
256
+ * // In a controller or service
257
+ * constructor(private readonly tenantContext: TenantContextService) {}
258
+ *
259
+ * async handleRequest() {
260
+ * const tenant = this.tenantContext.getTenant();
261
+ * console.log(`Processing request for tenant: ${tenant.tenantSlug}`);
262
+ * }
263
+ */
264
+ declare class TenantContextService {
265
+ private tenantInfo;
266
+ /**
267
+ * Set tenant information for this request/message
268
+ *
269
+ * This is typically called by:
270
+ * - TenantContextInterceptor (for HTTP requests in gateway)
271
+ * - MessageTenantContextInterceptor (for RabbitMQ messages in microservices)
272
+ * - Manual context setup in message handlers
273
+ *
274
+ * @param tenantInfo Complete tenant information
275
+ * @throws Error if tenant context is already set (prevents accidental overwrites)
276
+ */
277
+ setTenant(tenantInfo: TenantInfo): void;
278
+ /**
279
+ * Get tenant information for this request/message
280
+ *
281
+ * @returns Tenant information
282
+ * @throws UnauthorizedException if tenant context hasn't been set
283
+ */
284
+ getTenant(): TenantInfo;
285
+ /**
286
+ * Check if tenant context has been set
287
+ *
288
+ * @returns true if tenant context is available
289
+ */
290
+ hasTenant(): boolean;
291
+ /**
292
+ * Clear tenant context
293
+ *
294
+ * This is useful for cleanup in RabbitMQ message handlers
295
+ * after the message has been processed.
296
+ *
297
+ * HTTP requests don't need manual cleanup as the service
298
+ * instance is destroyed when the request ends.
299
+ */
300
+ clearTenant(): void;
301
+ /**
302
+ * Get tenant ID safely (returns null if not set)
303
+ *
304
+ * @returns Tenant ID or null
305
+ */
306
+ getTenantIdSafe(): string | null;
307
+ /**
308
+ * Get tenant subdomain safely (returns null if not set)
309
+ *
310
+ * @returns Tenant subdomain or null
311
+ */
312
+ getTenantSubdomainSafe(): string | null;
313
+ }
314
+
315
+ /**
316
+ * Service responsible for managing tenant-scoped database connections
317
+ *
318
+ * This service:
319
+ * - Maintains a connection pool (Map<cacheKey, DbClient>)
320
+ * - Creates new connections dynamically based on tenant context
321
+ * - Reuses existing connections for the same tenant
322
+ * - Supports both cloud schemas and enterprise databases
323
+ * - Automatically cleans up idle connections
324
+ *
325
+ * @example
326
+ * // In a controller or service
327
+ * const dbClient = await this.tenantDatabase.getDbClient<PrismaClient>();
328
+ * const users = await dbClient.user.findMany();
329
+ */
330
+ declare class TenantDatabaseService implements OnModuleDestroy {
331
+ private readonly options;
332
+ private readonly tenantContext;
333
+ private readonly logger;
334
+ /** Connection pool: Map<cacheKey, DbClient> */
335
+ private readonly clients;
336
+ /** Track last usage time for idle connection cleanup */
337
+ private readonly clientLastUsed;
338
+ /** Cleanup interval timer */
339
+ private cleanupInterval?;
340
+ constructor(options: DatabaseModuleOptions, tenantContext: TenantContextService);
341
+ /**
342
+ * Get tenant-scoped database client for the current request/message
343
+ *
344
+ * This method:
345
+ * 1. Gets tenant info from TenantContextService
346
+ * 2. Builds a connection URL based on tenant type
347
+ * 3. Returns cached client if exists, otherwise creates new one
348
+ *
349
+ * @returns Promise<Database client instance>
350
+ * @throws UnauthorizedException if tenant context not set
351
+ * @throws InternalServerErrorException if connection fails
352
+ *
353
+ * @example
354
+ * const dbClient = await tenantDatabase.getDbClient<PrismaClient>();
355
+ * const users = await dbClient.user.findMany();
356
+ */
357
+ getDbClient<T = any>(): Promise<T>;
358
+ /**
359
+ * Create a new database client for the given tenant
360
+ */
361
+ private createDbClient;
362
+ /**
363
+ * Build connection URL for enterprise tenant (dedicated database)
364
+ */
365
+ private buildTenantDbUrl;
366
+ /**
367
+ * Build cache key for connection pooling
368
+ */
369
+ private buildCacheKey;
370
+ /**
371
+ * Start periodic cleanup of idle connections
372
+ */
373
+ private startConnectionCleaner;
374
+ /**
375
+ * Clean up idle connections that haven't been used recently
376
+ */
377
+ private cleanupIdleConnections;
378
+ /**
379
+ * Get current connection pool statistics
380
+ */
381
+ getPoolStats(): {
382
+ activeConnections: number;
383
+ tenants: string[];
384
+ };
385
+ /**
386
+ * Mask password in connection URL for logging
387
+ */
388
+ private maskPassword;
389
+ onModuleDestroy(): Promise<void>;
390
+ }
391
+
392
+ /**
393
+ * Interceptor that extracts tenant context from RabbitMQ messages (Microservice Mode)
394
+ *
395
+ * This interceptor:
396
+ * 1. Extracts tenant info from RabbitMQ message payload
397
+ * 2. Sets it in REQUEST-SCOPED TenantContextService
398
+ * 3. Cleans up after message is processed
399
+ *
400
+ * Expected message format:
401
+ * {
402
+ * dto: { ... },
403
+ * tenant: {
404
+ * tenantId: 'abc-123',
405
+ * tenantSlug: 'acme',
406
+ * tenantType: 'ENTERPRISE',
407
+ * databaseHost: 'enterprise-1.aws.com',
408
+ * databaseName: 'acme_db',
409
+ * ...
410
+ * }
411
+ * }
412
+ *
413
+ * @example
414
+ * // In microservice module
415
+ * {
416
+ * provide: APP_INTERCEPTOR,
417
+ * useClass: MessageTenantContextInterceptor,
418
+ * }
419
+ */
420
+ declare class MessageTenantContextInterceptor implements NestInterceptor {
421
+ private readonly tenantContext;
422
+ private readonly logger;
423
+ constructor(tenantContext: TenantContextService);
424
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
425
+ /**
426
+ * Clean up tenant context after message is processed
427
+ */
428
+ private cleanupContext;
429
+ }
430
+
431
+ /**
432
+ * Interceptor that extracts tenant context from HTTP requests (Gateway Mode)
433
+ *
434
+ * This interceptor runs BEFORE the controller and:
435
+ * 1. Extracts tenant identifier from request (tries subdomain first, then falls back to header)
436
+ * 2. Queries primary database for tenant configuration
437
+ * 3. Stores tenant info in REQUEST-SCOPED TenantContextService
438
+ *
439
+ * Tenant resolution order:
440
+ * - First: Subdomain (e.g., acme.vritti.com → 'acme')
441
+ * - Fallback: x-tenant-id or x-tenant-slug header
442
+ *
443
+ * Only used in API Gateway. Microservices use MessageTenantContextInterceptor instead.
444
+ *
445
+ * @example
446
+ * // Request: https://acme.vritti.com/api/users
447
+ * // Interceptor extracts "acme" from subdomain, queries primary DB, sets context
448
+ */
449
+ declare class TenantContextInterceptor implements NestInterceptor {
450
+ private readonly tenantContext;
451
+ private readonly primaryDatabase;
452
+ private readonly options;
453
+ private readonly logger;
454
+ constructor(tenantContext: TenantContextService, primaryDatabase: PrimaryDatabaseService, options: DatabaseModuleOptions);
455
+ intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>>;
456
+ /**
457
+ * Extract tenant identifier: tries subdomain first, then falls back to header
458
+ */
459
+ private extractTenantIdentifier;
460
+ /**
461
+ * Extract tenant from subdomain
462
+ * @example acme.vritti.com → 'acme'
463
+ */
464
+ private extractFromSubdomain;
465
+ /**
466
+ * Extract tenant from HTTP headers
467
+ * Checks x-tenant-id and x-subdomain headers
468
+ */
469
+ private extractFromHeader;
470
+ }
471
+
472
+ /**
473
+ * Parameter decorator that injects tenant metadata into controller method
474
+ *
475
+ * This decorator retrieves tenant information (ID, slug, type, etc.)
476
+ * from the REQUEST-SCOPED TenantContextService.
477
+ *
478
+ * Useful for:
479
+ * - Logging tenant-specific information
480
+ * - Implementing tenant-specific business logic
481
+ * - Auditing and tracking
482
+ * - Conditional feature flags
483
+ *
484
+ * @returns TenantInfo object with tenant metadata
485
+ *
486
+ * @example
487
+ * // Access tenant metadata
488
+ * @Get('info')
489
+ * async getTenantInfo(@Tenant() tenant: TenantInfo) {
490
+ * return {
491
+ * tenantId: tenant.tenantId,
492
+ * tenantSlug: tenant.tenantSlug,
493
+ * tenantType: tenant.tenantType,
494
+ * };
495
+ * }
496
+ *
497
+ * @example
498
+ * // Use for logging
499
+ * @Post()
500
+ * async createUser(
501
+ * @Body() dto: CreateUserDto,
502
+ * @Tenant() tenant: TenantInfo,
503
+ * ) {
504
+ * this.logger.log(`Creating user for tenant: ${tenant.tenantSlug}`);
505
+ * // ...
506
+ * }
507
+ *
508
+ * @example
509
+ * // Conditional business logic
510
+ * @Get('features')
511
+ * async getFeatures(@Tenant() tenant: TenantInfo) {
512
+ * if (tenant.tenantType === 'ENTERPRISE') {
513
+ * return ['feature-a', 'feature-b', 'feature-c'];
514
+ * }
515
+ * return ['feature-a'];
516
+ * }
517
+ */
518
+ declare const Tenant: (...dataOrPipes: unknown[]) => ParameterDecorator;
519
+
520
+ /**
521
+ * Extract subdomain from hostname
522
+ *
523
+ * @param host Full hostname (e.g., 'acme.vritti.com:3000' or 'acme.vritti.com')
524
+ * @returns Subdomain or null if not found
525
+ *
526
+ * @example
527
+ * extractSubdomain('acme.vritti.com') // 'acme'
528
+ * extractSubdomain('staging-acme.vritti.com') // 'staging-acme'
529
+ * extractSubdomain('localhost') // null
530
+ * extractSubdomain('vritti.com') // null
531
+ */
532
+ declare function extractSubdomain(host: string): string | null;
533
+
534
+ export { DatabaseModule, type DatabaseModuleOptions, MessageTenantContextInterceptor, PrimaryDatabaseService, type PrimaryDbConfig, Tenant, TenantContextInterceptor, TenantContextService, TenantDatabaseService, type TenantInfo, extractSubdomain };