@vritti/api-sdk 0.0.9 → 0.1.0

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,5 +1,8 @@
1
1
  import * as _nestjs_common from '@nestjs/common';
2
2
  import { DynamicModule, OnModuleDestroy, OnModuleInit, Logger, CanActivate, ExecutionContext, ExceptionFilter, ArgumentsHost, HttpException, HttpStatus, ModuleMetadata, Type, NestModule, MiddlewareConsumer, LoggerService as LoggerService$1, NestMiddleware, NestInterceptor, CallHandler } from '@nestjs/common';
3
+ import { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
+ import { InferInsertModel, InferSelectModel, SQL } from 'drizzle-orm';
5
+ import { PgTable } from 'drizzle-orm/pg-core';
3
6
  import { ConfigService } from '@nestjs/config';
4
7
  import { Reflector } from '@nestjs/core';
5
8
  import { JwtService } from '@nestjs/jwt';
@@ -81,6 +84,34 @@ declare class AuthConfigModule {
81
84
  static forRootAsync(): DynamicModule;
82
85
  }
83
86
 
87
+ /**
88
+ * Schema Registry Interface
89
+ *
90
+ * Projects augment this interface to register their Drizzle schema.
91
+ * This enables type-safe db.query access without passing schema types everywhere.
92
+ *
93
+ * @example
94
+ * // In your project's schema.registry.ts:
95
+ * declare module '@vritti/api-sdk' {
96
+ * interface SchemaRegistry {
97
+ * schema: typeof import('./schema');
98
+ * }
99
+ * }
100
+ */
101
+ interface SchemaRegistry {
102
+ }
103
+ /**
104
+ * Extracts the registered schema type.
105
+ * Falls back to Record<string, unknown> if no schema is registered.
106
+ */
107
+ type RegisteredSchema = SchemaRegistry extends {
108
+ schema: infer S;
109
+ } ? S : Record<string, unknown>;
110
+ /**
111
+ * Type alias for the Drizzle database client with registered schema
112
+ */
113
+ type TypedDrizzleClient = NodePgDatabase<RegisteredSchema>;
114
+
84
115
  /**
85
116
  * Primary database connection configuration
86
117
  */
@@ -97,8 +128,8 @@ interface PrimaryDbConfig {
97
128
  database: string;
98
129
  /** Default schema (default: 'public') */
99
130
  schema?: string;
100
- /** SSL mode: 'require' | 'prefer' | 'disable' (default: 'require') */
101
- sslMode?: 'require' | 'prefer' | 'disable';
131
+ /** SSL mode: 'require' | 'prefer' | 'disable' | 'no-verify' (default: 'require') */
132
+ sslMode?: 'require' | 'prefer' | 'disable' | 'no-verify';
102
133
  }
103
134
  /**
104
135
  * Configuration options for DatabaseModule
@@ -120,11 +151,11 @@ interface DatabaseModuleOptions {
120
151
  */
121
152
  primaryDb: PrimaryDbConfig;
122
153
  /**
123
- * Primary database client constructor (for querying tenant registry)
124
- * Only required in gateway mode
125
- * @example import { PrismaClient } from '@prisma/client'
154
+ * Drizzle schema object containing all tables and relations
155
+ * Import your schema from db/schema/index.ts and pass it here
156
+ * @example import * as schema from '@/db/schema'
126
157
  */
127
- prismaClientConstructor: any;
158
+ drizzleSchema: RegisteredSchema;
128
159
  /**
129
160
  * Connection cache TTL in milliseconds
130
161
  * Idle connections will be closed after this period
@@ -356,7 +387,7 @@ declare class TenantContextService {
356
387
  * Service responsible for managing tenant-scoped database connections
357
388
  *
358
389
  * This service:
359
- * - Maintains a connection pool (Map<cacheKey, DbClient>)
390
+ * - Maintains a connection pool (Map<cacheKey, TenantConnection>)
360
391
  * - Creates new connections dynamically based on tenant context
361
392
  * - Reuses existing connections for the same tenant
362
393
  * - Supports both cloud schemas and enterprise databases
@@ -364,14 +395,14 @@ declare class TenantContextService {
364
395
  *
365
396
  * @example
366
397
  * // In a controller or service
367
- * const dbClient = await this.tenantDatabase.getDbClient<PrismaClient>();
368
- * const users = await dbClient.user.findMany();
398
+ * const db = this.tenantDatabase.drizzleClient;
399
+ * const users = await db.select().from(usersTable);
369
400
  */
370
401
  declare class TenantDatabaseService implements OnModuleDestroy {
371
402
  private readonly options;
372
403
  private readonly tenantContext;
373
404
  private readonly logger;
374
- /** Connection pool: Map<cacheKey, DbClient> */
405
+ /** Connection pool: Map<cacheKey, TenantConnection> */
375
406
  private readonly clients;
376
407
  /** Track last usage time for idle connection cleanup */
377
408
  private readonly clientLastUsed;
@@ -379,14 +410,18 @@ declare class TenantDatabaseService implements OnModuleDestroy {
379
410
  private cleanupInterval?;
380
411
  constructor(options: DatabaseModuleOptions, tenantContext: TenantContextService);
381
412
  /**
382
- * Get the Prisma client for the current tenant's database.
413
+ * Get the Drizzle client for the current tenant's database.
383
414
  * This returns the tenant-scoped database client.
384
415
  *
385
- * @returns Tenant-scoped database client instance
416
+ * @returns Tenant-scoped Drizzle database instance
386
417
  * @throws UnauthorizedException if tenant context not set
387
418
  * @throws InternalServerErrorException if connection fails
388
419
  */
389
- get prismaClient(): any;
420
+ get drizzleClient(): TypedDrizzleClient;
421
+ /**
422
+ * Get the Drizzle schema
423
+ */
424
+ get schema(): Record<string, unknown>;
390
425
  /**
391
426
  * Get tenant-scoped database client for the current request/message
392
427
  *
@@ -395,21 +430,17 @@ declare class TenantDatabaseService implements OnModuleDestroy {
395
430
  * 2. Builds a connection URL based on tenant type
396
431
  * 3. Returns cached client if exists, otherwise creates new one
397
432
  *
398
- * @returns Promise<Database client instance>
433
+ * @returns Drizzle database instance
399
434
  * @throws UnauthorizedException if tenant context not set
400
435
  * @throws InternalServerErrorException if connection fails
401
- *
402
- * @example
403
- * const dbClient = await tenantDatabase.getDbClient<PrismaClient>();
404
- * const users = await dbClient.user.findMany();
405
436
  */
406
437
  private getDbClient;
407
438
  /**
408
- * Create a new database client for the given tenant
439
+ * Create a new database client for the given tenant (synchronous)
409
440
  */
410
- private createDbClient;
441
+ private createDbClientSync;
411
442
  /**
412
- * Build connection URL for enterprise tenant (dedicated database)
443
+ * Build connection URL for tenant (dedicated database)
413
444
  */
414
445
  private buildTenantDbUrl;
415
446
  /**
@@ -455,8 +486,10 @@ declare class TenantDatabaseService implements OnModuleDestroy {
455
486
  declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
456
487
  private readonly options;
457
488
  private readonly logger;
458
- /** Primary database client for querying tenant registry */
459
- private primaryDbClient;
489
+ /** PostgreSQL connection pool */
490
+ private pool;
491
+ /** Drizzle database instance */
492
+ private db;
460
493
  /** In-memory cache: Map<tenantIdentifier, TenantConfig> */
461
494
  private readonly tenantConfigCache;
462
495
  /** Cache TTL in milliseconds */
@@ -464,9 +497,9 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
464
497
  constructor(options: DatabaseModuleOptions);
465
498
  onModuleInit(): Promise<void>;
466
499
  /**
467
- * Initialize connection to primary database
500
+ * Initialize connection to primary database using Drizzle
468
501
  */
469
- private initializePrimaryDbClient;
502
+ private initializeDrizzleClient;
470
503
  /**
471
504
  * Build connection URL from primary database properties
472
505
  */
@@ -476,9 +509,9 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
476
509
  */
477
510
  private maskPassword;
478
511
  /**
479
- * Get tenant configuration by identifier (ID or slug)
512
+ * Get tenant configuration by identifier (ID or subdomain)
480
513
  *
481
- * @param tenantIdentifier Tenant ID or slug
514
+ * @param tenantIdentifier Tenant ID or subdomain
482
515
  * @returns Tenant configuration or null if not found
483
516
  */
484
517
  getTenantInfo(tenantIdentifier: string): Promise<TenantInfo | null>;
@@ -491,7 +524,7 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
491
524
  *
492
525
  * Useful when tenant settings are updated and cache needs to be invalidated
493
526
  *
494
- * @param tenantIdentifier Tenant ID or slug
527
+ * @param tenantIdentifier Tenant ID or subdomain
495
528
  */
496
529
  clearTenantCache(tenantIdentifier: string): void;
497
530
  /**
@@ -499,13 +532,17 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
499
532
  */
500
533
  clearAllCaches(): void;
501
534
  /**
502
- * Get the Prisma client for the primary database.
503
- * This is a synchronous property that returns the initialized Prisma client.
535
+ * Get the Drizzle database instance for the primary database.
536
+ * This is a synchronous property that returns the initialized Drizzle client.
504
537
  *
505
- * @returns Primary database client instance
538
+ * @returns Primary database Drizzle instance
506
539
  * @throws Error if primary database client is not initialized
507
540
  */
508
- get prismaClient(): any;
541
+ get drizzleClient(): TypedDrizzleClient;
542
+ /**
543
+ * Get the Drizzle schema
544
+ */
545
+ get schema(): typeof this$1.options.drizzleSchema;
509
546
  /**
510
547
  * Decrypt database credentials
511
548
  *
@@ -519,90 +556,123 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
519
556
  }
520
557
 
521
558
  /**
522
- * Abstract base repository for primary database operations.
559
+ * Type-safe wrapper for Drizzle's RelationalQueryBuilder.
560
+ * This interface matches the method signatures of RelationalQueryBuilder
561
+ * but properly binds the TSelect generic for type safety.
562
+ *
563
+ * We use this instead of RelationalQueryBuilder directly because
564
+ * TypeScript cannot infer TSelect from the generic base repository context.
565
+ */
566
+ interface TypedRelationalQueryBuilder<TSelect> {
567
+ findFirst(config?: {
568
+ where?: SQL;
569
+ with?: Record<string, unknown>;
570
+ columns?: Record<string, boolean>;
571
+ }): Promise<TSelect | undefined>;
572
+ findMany(config?: {
573
+ where?: SQL;
574
+ orderBy?: SQL;
575
+ limit?: number;
576
+ offset?: number;
577
+ with?: Record<string, unknown>;
578
+ columns?: Record<string, boolean>;
579
+ }): Promise<TSelect[]>;
580
+ }
581
+ /**
582
+ * Abstract base repository for primary database operations using Drizzle ORM.
523
583
  * Provides common CRUD operations with automatic logging.
524
584
  *
525
- * @template TModel - The Prisma model type
526
- * @template TCreateDTO - DTO type for create operations
527
- * @template TUpdateDTO - DTO type for update operations
585
+ * @template TTable - The Drizzle table type (must be registered in SchemaRegistry)
586
+ * @template TInsert - Type for insert operations (inferred from table.$inferInsert)
587
+ * @template TSelect - Type for select operations (inferred from table.$inferSelect)
588
+ *
589
+ * @remarks
590
+ * **Type Assertion Pattern:** This repository uses `as any` casts when passing
591
+ * the generic table to Drizzle methods. This is necessary because TypeScript
592
+ * cannot prove that a generic `TTable extends PgTable` satisfies Drizzle's
593
+ * stricter internal type requirements for `insert()`, `update()`, and `delete()`.
594
+ *
595
+ * The public API maintains full type safety:
596
+ * - Input parameters are typed as `TInsert` (inferred from table)
597
+ * - Return values are typed as `TSelect` (inferred from table)
598
+ * - The casts are implementation details that don't leak to consumers
528
599
  *
529
600
  * @example
530
601
  * ```typescript
531
- * // Using the model delegate pattern (RECOMMENDED)
532
- * // Type-safe, IDE autocomplete, refactor-friendly
533
- * @Injectable()
534
- * export class UserRepository extends PrimaryBaseRepository<
535
- * User,
536
- * CreateUserDto,
537
- * UpdateUserDto
538
- * > {
539
- * constructor(database: PrimaryDatabaseService) {
540
- * super(database, (prisma) => prisma.user); // ✅ Type-safe with autocomplete!
541
- * }
602
+ * import { users } from '@/db/schema';
542
603
  *
543
- * // Add custom methods as needed
544
- * async findByEmail(email: string): Promise<User | null> {
545
- * return this.model.findUnique({ where: { email } });
546
- * }
547
- * }
604
+ * type User = typeof users.$inferSelect;
605
+ * type NewUser = typeof users.$inferInsert;
548
606
  *
549
- * // Short syntax is also supported
550
607
  * @Injectable()
551
- * export class TenantRepository extends PrimaryBaseRepository<Tenant> {
608
+ * export class UserRepository extends PrimaryBaseRepository<typeof users> {
552
609
  * constructor(database: PrimaryDatabaseService) {
553
- * super(database, (p) => p.tenant); // ✅ Concise!
610
+ * super(database, users);
554
611
  * }
555
- * }
556
612
  *
557
- * // Works with complex model names
558
- * @Injectable()
559
- * export class EmailVerificationRepository extends PrimaryBaseRepository<EmailVerification> {
560
- * constructor(database: PrimaryDatabaseService) {
561
- * super(database, (p) => p.emailVerification); // ✅ Matches Prisma naming
613
+ * // Use Prisma-like relational query syntax (recommended)
614
+ * async findByEmail(email: string): Promise<User | undefined> {
615
+ * return this.model.findFirst({
616
+ * where: eq(users.email, email),
617
+ * });
618
+ * }
619
+ *
620
+ * // Use Prisma-like with relations
621
+ * async findWithRelations(id: string): Promise<User | undefined> {
622
+ * return this.model.findFirst({
623
+ * where: eq(users.id, id),
624
+ * with: { posts: true, profile: true }
625
+ * });
562
626
  * }
563
627
  * }
564
628
  * ```
565
629
  */
566
- declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDTO = any> {
630
+ declare abstract class PrimaryBaseRepository<TTable extends PgTable, TInsert = InferInsertModel<TTable>, TSelect = InferSelectModel<TTable>> {
567
631
  protected readonly database: PrimaryDatabaseService;
632
+ protected readonly table: TTable;
568
633
  protected readonly logger: Logger;
569
- private readonly modelGetter;
570
634
  /**
571
- * Lazy getter for Prisma client.
635
+ * The table name extracted from the Drizzle table at runtime.
636
+ * Used to access the query API for this repository's table.
637
+ */
638
+ private readonly tableName;
639
+ /**
640
+ * Lazy getter for Drizzle client.
572
641
  * Accesses the client from the database service only when needed,
573
642
  * avoiding initialization timing issues with NestJS lifecycle.
574
643
  */
575
- protected get prisma(): any;
644
+ protected get db(): TypedDrizzleClient;
576
645
  /**
577
- * Lazy getter for the Prisma model delegate.
578
- * Returns the specific model (e.g., prisma.user, prisma.tenant) for this repository.
646
+ * Model query API for THIS repository's table (Prisma-like syntax)
647
+ * Scoped to only the table this repository manages.
648
+ * Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * // Use relational queries with type safety
653
+ * const user = await this.model.findFirst({
654
+ * where: eq(users.id, id),
655
+ * with: { posts: true, profile: true }
656
+ * });
657
+ * ```
579
658
  */
580
- protected get model(): any;
659
+ protected get model(): TypedRelationalQueryBuilder<TSelect>;
581
660
  /**
582
661
  * Create a new repository instance
583
662
  *
584
663
  * @param database - The primary database service
585
- * @param getModel - Function that returns the Prisma model delegate from the client
664
+ * @param table - The Drizzle table schema object
586
665
  *
587
666
  * @example
588
667
  * ```typescript
589
- * // Standard usage with full parameter name
590
- * constructor(database: PrimaryDatabaseService) {
591
- * super(database, (prisma) => prisma.user);
592
- * }
668
+ * import { users } from '@/db/schema';
593
669
  *
594
- * // Short syntax
595
670
  * constructor(database: PrimaryDatabaseService) {
596
- * super(database, (p) => p.user);
597
- * }
598
- *
599
- * // Complex model names
600
- * constructor(database: PrimaryDatabaseService) {
601
- * super(database, (p) => p.emailVerification);
671
+ * super(database, users);
602
672
  * }
603
673
  * ```
604
674
  */
605
- constructor(database: PrimaryDatabaseService, getModel: (prisma: any) => any);
675
+ constructor(database: PrimaryDatabaseService, table: TTable);
606
676
  /**
607
677
  * Create a new record
608
678
  *
@@ -613,63 +683,64 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
613
683
  * ```typescript
614
684
  * const user = await userRepository.create({
615
685
  * email: 'user@example.com',
616
- * name: 'John Doe'
686
+ * firstName: 'John'
617
687
  * });
618
688
  * ```
619
689
  */
620
- create(data: TCreateDTO): Promise<TModel>;
690
+ create(data: TInsert): Promise<TSelect>;
621
691
  /**
622
692
  * Find a single record by ID
623
693
  *
624
694
  * @param id - The record ID
625
- * @returns Promise resolving to the record or null if not found
695
+ * @returns Promise resolving to the record or undefined if not found
626
696
  *
627
697
  * @example
628
698
  * ```typescript
629
699
  * const user = await userRepository.findById('user-id-123');
630
700
  * ```
631
701
  */
632
- findById(id: string): Promise<TModel | null>;
702
+ findById(id: string): Promise<TSelect | undefined>;
633
703
  /**
634
704
  * Find a single record with custom where clause
635
705
  *
636
- * @param where - The where clause or findUnique args
637
- * @returns Promise resolving to the record or null if not found
706
+ * @param where - SQL condition
707
+ * @returns Promise resolving to the record or undefined if not found
638
708
  *
639
709
  * @example
640
710
  * ```typescript
641
- * // Simple where clause
642
- * const user = await userRepository.findOne({ email: 'user@example.com' });
643
- *
644
- * // With include
645
- * const user = await userRepository.findOne({
646
- * where: { email: 'user@example.com' },
647
- * include: { posts: true }
648
- * });
711
+ * import { eq } from 'drizzle-orm';
712
+ * const user = await userRepository.findOne(eq(users.email, 'user@example.com'));
649
713
  * ```
650
714
  */
651
- findOne(where: any): Promise<TModel | null>;
715
+ findOne(where: SQL): Promise<TSelect | undefined>;
652
716
  /**
653
717
  * Find multiple records
654
718
  *
655
- * @param args - Prisma findMany arguments (where, orderBy, take, skip, etc.)
719
+ * @param options - Query options (where, orderBy, limit, offset)
656
720
  * @returns Promise resolving to an array of records
657
721
  *
658
722
  * @example
659
723
  * ```typescript
724
+ * import { eq, desc } from 'drizzle-orm';
725
+ *
660
726
  * // Find all users
661
727
  * const users = await userRepository.findMany();
662
728
  *
663
729
  * // Find with filtering and pagination
664
730
  * const users = await userRepository.findMany({
665
- * where: { status: 'ACTIVE' },
666
- * orderBy: { createdAt: 'desc' },
667
- * take: 10,
668
- * skip: 0
731
+ * where: eq(users.accountStatus, 'ACTIVE'),
732
+ * orderBy: desc(users.createdAt),
733
+ * limit: 10,
734
+ * offset: 0
669
735
  * });
670
736
  * ```
671
737
  */
672
- findMany(args?: any): Promise<TModel[]>;
738
+ findMany(options?: {
739
+ where?: SQL;
740
+ orderBy?: SQL;
741
+ limit?: number;
742
+ offset?: number;
743
+ }): Promise<TSelect[]>;
673
744
  /**
674
745
  * Update a record by ID
675
746
  *
@@ -680,28 +751,30 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
680
751
  * @example
681
752
  * ```typescript
682
753
  * const user = await userRepository.update('user-id-123', {
683
- * name: 'Jane Doe'
754
+ * firstName: 'Jane'
684
755
  * });
685
756
  * ```
686
757
  */
687
- update(id: string, data: TUpdateDTO): Promise<TModel>;
758
+ update(id: string, data: Partial<TInsert>): Promise<TSelect>;
688
759
  /**
689
760
  * Update multiple records
690
761
  *
691
- * @param where - The where clause to match records
762
+ * @param where - SQL condition to match records
692
763
  * @param data - The data to update
693
764
  * @returns Promise resolving to the count of updated records
694
765
  *
695
766
  * @example
696
767
  * ```typescript
768
+ * import { eq } from 'drizzle-orm';
769
+ *
697
770
  * const result = await userRepository.updateMany(
698
- * { status: 'PENDING' },
699
- * { status: 'ACTIVE' }
771
+ * eq(users.accountStatus, 'PENDING'),
772
+ * { accountStatus: 'ACTIVE' }
700
773
  * );
701
774
  * console.log(`Updated ${result.count} users`);
702
775
  * ```
703
776
  */
704
- updateMany(where: any, data: TUpdateDTO): Promise<{
777
+ updateMany(where: SQL, data: Partial<TInsert>): Promise<{
705
778
  count: number;
706
779
  }>;
707
780
  /**
@@ -715,142 +788,166 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
715
788
  * const user = await userRepository.delete('user-id-123');
716
789
  * ```
717
790
  */
718
- delete(id: string): Promise<TModel>;
791
+ delete(id: string): Promise<TSelect>;
719
792
  /**
720
793
  * Delete multiple records
721
794
  *
722
- * @param where - The where clause to match records
795
+ * @param where - SQL condition to match records
723
796
  * @returns Promise resolving to the count of deleted records
724
797
  *
725
798
  * @example
726
799
  * ```typescript
727
- * const result = await userRepository.deleteMany({
728
- * status: 'INACTIVE',
729
- * createdAt: { lt: new Date('2020-01-01') }
730
- * });
800
+ * import { lt } from 'drizzle-orm';
801
+ *
802
+ * const result = await userRepository.deleteMany(
803
+ * lt(users.createdAt, new Date('2020-01-01'))
804
+ * );
731
805
  * console.log(`Deleted ${result.count} users`);
732
806
  * ```
733
807
  */
734
- deleteMany(where: any): Promise<{
808
+ deleteMany(where: SQL): Promise<{
735
809
  count: number;
736
810
  }>;
737
811
  /**
738
812
  * Count records
739
813
  *
740
- * @param where - Optional where clause to filter records
814
+ * @param where - Optional SQL condition to filter records
741
815
  * @returns Promise resolving to the count of records
742
816
  *
743
817
  * @example
744
818
  * ```typescript
819
+ * import { eq } from 'drizzle-orm';
820
+ *
745
821
  * // Count all users
746
822
  * const total = await userRepository.count();
747
823
  *
748
824
  * // Count active users
749
- * const activeCount = await userRepository.count({ status: 'ACTIVE' });
825
+ * const activeCount = await userRepository.count(
826
+ * eq(users.accountStatus, 'ACTIVE')
827
+ * );
750
828
  * ```
751
829
  */
752
- count(where?: any): Promise<number>;
830
+ count(where?: SQL): Promise<number>;
753
831
  /**
754
832
  * Check if a record exists
755
833
  *
756
- * @param where - The where clause to match records
834
+ * @param where - SQL condition to match records
757
835
  * @returns Promise resolving to true if at least one record exists, false otherwise
758
836
  *
759
837
  * @example
760
838
  * ```typescript
761
- * const emailExists = await userRepository.exists({
762
- * email: 'user@example.com'
763
- * });
839
+ * import { eq } from 'drizzle-orm';
840
+ *
841
+ * const emailExists = await userRepository.exists(
842
+ * eq(users.email, 'user@example.com')
843
+ * );
764
844
  * ```
765
845
  */
766
- exists(where: any): Promise<boolean>;
846
+ exists(where: SQL): Promise<boolean>;
767
847
  }
768
848
 
769
849
  /**
770
- * Abstract base repository for tenant-scoped database operations.
850
+ * Type helper to extract table name from Drizzle table.
851
+ * TTable['_']['name'] gives us the string literal type (e.g., 'products')
852
+ */
853
+ type ExtractTableName<TTable extends PgTable> = TTable['_']['name'];
854
+ /**
855
+ * Abstract base repository for tenant-scoped database operations using Drizzle ORM.
771
856
  * All operations are automatically scoped to the current tenant.
772
857
  *
773
- * @template TModel - The Prisma model type
774
- * @template TCreateDTO - DTO type for create operations
775
- * @template TUpdateDTO - DTO type for update operations
858
+ * @template TTable - The Drizzle table type (must be registered in SchemaRegistry)
859
+ * @template TInsert - Type for insert operations (inferred from table.$inferInsert)
860
+ * @template TSelect - Type for select operations (inferred from table.$inferSelect)
861
+ *
862
+ * @remarks
863
+ * **Type Assertion Pattern:** This repository uses `as any` casts when passing
864
+ * the generic table to Drizzle methods. This is necessary because TypeScript
865
+ * cannot prove that a generic `TTable extends PgTable` satisfies Drizzle's
866
+ * stricter internal type requirements for `insert()`, `update()`, and `delete()`.
867
+ *
868
+ * The public API maintains full type safety:
869
+ * - Input parameters are typed as `TInsert` (inferred from table)
870
+ * - Return values are typed as `TSelect` (inferred from table)
871
+ * - The casts are implementation details that don't leak to consumers
776
872
  *
777
873
  * @example
778
874
  * ```typescript
779
- * // Using the model delegate pattern (RECOMMENDED)
780
- * // Type-safe, IDE autocomplete, refactor-friendly
875
+ * import { products } from '@/db/schema';
876
+ *
877
+ * type Product = typeof products.$inferSelect;
878
+ * type NewProduct = typeof products.$inferInsert;
879
+ *
781
880
  * @Injectable()
782
- * export class ProductRepository extends TenantBaseRepository<
783
- * Product,
784
- * CreateProductDto,
785
- * UpdateProductDto
786
- * > {
881
+ * export class ProductRepository extends TenantBaseRepository<typeof products> {
787
882
  * constructor(database: TenantDatabaseService) {
788
- * super(database, (prisma) => prisma.product); // ✅ Type-safe with autocomplete!
883
+ * super(database, products);
789
884
  * }
790
885
  *
791
- * // Add custom methods as needed
886
+ * // Use SQL-builder syntax
792
887
  * async findBySku(sku: string): Promise<Product | null> {
793
- * return this.model.findUnique({ where: { sku } });
888
+ * const [result] = await this.db
889
+ * .select()
890
+ * .from(this.table)
891
+ * .where(eq(products.sku, sku))
892
+ * .limit(1);
893
+ * return result ?? null;
794
894
  * }
795
- * }
796
895
  *
797
- * // Short syntax is also supported
798
- * @Injectable()
799
- * export class OrderRepository extends TenantBaseRepository<Order> {
800
- * constructor(database: TenantDatabaseService) {
801
- * super(database, (p) => p.order); // Concise!
802
- * }
803
- * }
804
- *
805
- * // Works with complex model names
806
- * @Injectable()
807
- * export class InventoryItemRepository extends TenantBaseRepository<InventoryItem> {
808
- * constructor(database: TenantDatabaseService) {
809
- * super(database, (p) => p.inventoryItem); // ✅ Matches Prisma naming
896
+ * // Use Prisma-like relational query syntax
897
+ * async findWithRelations(id: string): Promise<Product | null> {
898
+ * return await this.model.findFirst({
899
+ * where: eq(products.id, id),
900
+ * with: { category: true, variants: true }
901
+ * });
810
902
  * }
811
903
  * }
812
904
  * ```
813
905
  */
814
- declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO = any> {
906
+ declare abstract class TenantBaseRepository<TTable extends PgTable, TInsert = InferInsertModel<TTable>, TSelect = InferSelectModel<TTable>> {
815
907
  protected readonly database: TenantDatabaseService;
908
+ protected readonly table: TTable;
816
909
  protected readonly logger: Logger;
817
- private readonly modelGetter;
818
910
  /**
819
- * Lazy getter for Prisma client.
911
+ * The table name extracted from the Drizzle table at runtime.
912
+ * Used to access the query API for this repository's table.
913
+ */
914
+ private readonly tableName;
915
+ /**
916
+ * Lazy getter for Drizzle client.
820
917
  * Accesses the client from the database service only when needed,
821
918
  * avoiding initialization timing issues with NestJS lifecycle.
822
919
  */
823
- protected get prisma(): any;
920
+ protected get db(): TypedDrizzleClient;
824
921
  /**
825
- * Lazy getter for the Prisma model delegate.
826
- * Returns the specific model (e.g., prisma.product, prisma.order) for this repository.
922
+ * Model query API for THIS repository's table (Prisma-like syntax)
923
+ * Scoped to only the table this repository manages
924
+ *
925
+ * @example
926
+ * ```typescript
927
+ * // Use relational queries with type safety
928
+ * const product = await this.model.findFirst({
929
+ * where: eq(products.id, id),
930
+ * with: { category: true, variants: true }
931
+ * });
932
+ * ```
827
933
  */
828
- protected get model(): any;
934
+ protected get model(): TypedDrizzleClient['query'][ExtractTableName<TTable> & keyof TypedDrizzleClient['query']];
829
935
  /**
830
936
  * Create a new repository instance
831
937
  *
832
938
  * @param database - The tenant database service
833
- * @param getModel - Function that returns the Prisma model delegate from the client
939
+ * @param table - The Drizzle table schema object
834
940
  *
835
941
  * @example
836
942
  * ```typescript
837
- * // Standard usage with full parameter name
838
- * constructor(database: TenantDatabaseService) {
839
- * super(database, (prisma) => prisma.product);
840
- * }
943
+ * import { products } from '@/db/schema';
841
944
  *
842
- * // Short syntax
843
945
  * constructor(database: TenantDatabaseService) {
844
- * super(database, (p) => p.product);
845
- * }
846
- *
847
- * // Complex model names
848
- * constructor(database: TenantDatabaseService) {
849
- * super(database, (p) => p.inventoryItem);
946
+ * super(database, products);
850
947
  * }
851
948
  * ```
852
949
  */
853
- constructor(database: TenantDatabaseService, getModel: (prisma: any) => any);
950
+ constructor(database: TenantDatabaseService, table: TTable);
854
951
  /**
855
952
  * Create a new record
856
953
  *
@@ -866,7 +963,7 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
866
963
  * });
867
964
  * ```
868
965
  */
869
- create(data: TCreateDTO): Promise<TModel>;
966
+ create(data: TInsert): Promise<TSelect>;
870
967
  /**
871
968
  * Find a single record by ID
872
969
  *
@@ -878,47 +975,48 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
878
975
  * const product = await productRepository.findById('product-id-123');
879
976
  * ```
880
977
  */
881
- findById(id: string): Promise<TModel | null>;
978
+ findById(id: string): Promise<TSelect | null>;
882
979
  /**
883
980
  * Find a single record with custom where clause
884
981
  *
885
- * @param where - The where clause or findUnique args
982
+ * @param where - SQL condition
886
983
  * @returns Promise resolving to the record or null if not found
887
984
  *
888
985
  * @example
889
986
  * ```typescript
890
- * // Simple where clause
891
- * const product = await productRepository.findOne({ sku: 'WDG-001' });
892
- *
893
- * // With include
894
- * const product = await productRepository.findOne({
895
- * where: { sku: 'WDG-001' },
896
- * include: { category: true }
897
- * });
987
+ * import { eq } from 'drizzle-orm';
988
+ * const product = await productRepository.findOne(eq(products.sku, 'WDG-001'));
898
989
  * ```
899
990
  */
900
- findOne(where: any): Promise<TModel | null>;
991
+ findOne(where: SQL): Promise<TSelect | null>;
901
992
  /**
902
993
  * Find multiple records
903
994
  *
904
- * @param args - Prisma findMany arguments (where, orderBy, take, skip, etc.)
995
+ * @param options - Query options (where, orderBy, limit, offset)
905
996
  * @returns Promise resolving to an array of records
906
997
  *
907
998
  * @example
908
999
  * ```typescript
1000
+ * import { eq, desc } from 'drizzle-orm';
1001
+ *
909
1002
  * // Find all products
910
1003
  * const products = await productRepository.findMany();
911
1004
  *
912
1005
  * // Find with filtering and pagination
913
1006
  * const products = await productRepository.findMany({
914
- * where: { status: 'ACTIVE' },
915
- * orderBy: { createdAt: 'desc' },
916
- * take: 10,
917
- * skip: 0
1007
+ * where: eq(products.status, 'ACTIVE'),
1008
+ * orderBy: desc(products.createdAt),
1009
+ * limit: 10,
1010
+ * offset: 0
918
1011
  * });
919
1012
  * ```
920
1013
  */
921
- findMany(args?: any): Promise<TModel[]>;
1014
+ findMany(options?: {
1015
+ where?: SQL;
1016
+ orderBy?: SQL;
1017
+ limit?: number;
1018
+ offset?: number;
1019
+ }): Promise<TSelect[]>;
922
1020
  /**
923
1021
  * Update a record by ID
924
1022
  *
@@ -933,24 +1031,26 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
933
1031
  * });
934
1032
  * ```
935
1033
  */
936
- update(id: string, data: TUpdateDTO): Promise<TModel>;
1034
+ update(id: string, data: Partial<TInsert>): Promise<TSelect>;
937
1035
  /**
938
1036
  * Update multiple records
939
1037
  *
940
- * @param where - The where clause to match records
1038
+ * @param where - SQL condition to match records
941
1039
  * @param data - The data to update
942
1040
  * @returns Promise resolving to the count of updated records
943
1041
  *
944
1042
  * @example
945
1043
  * ```typescript
1044
+ * import { eq } from 'drizzle-orm';
1045
+ *
946
1046
  * const result = await productRepository.updateMany(
947
- * { status: 'PENDING' },
1047
+ * eq(products.status, 'PENDING'),
948
1048
  * { status: 'ACTIVE' }
949
1049
  * );
950
1050
  * console.log(`Updated ${result.count} products`);
951
1051
  * ```
952
1052
  */
953
- updateMany(where: any, data: TUpdateDTO): Promise<{
1053
+ updateMany(where: SQL, data: Partial<TInsert>): Promise<{
954
1054
  count: number;
955
1055
  }>;
956
1056
  /**
@@ -964,55 +1064,62 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
964
1064
  * const product = await productRepository.delete('product-id-123');
965
1065
  * ```
966
1066
  */
967
- delete(id: string): Promise<TModel>;
1067
+ delete(id: string): Promise<TSelect>;
968
1068
  /**
969
1069
  * Delete multiple records
970
1070
  *
971
- * @param where - The where clause to match records
1071
+ * @param where - SQL condition to match records
972
1072
  * @returns Promise resolving to the count of deleted records
973
1073
  *
974
1074
  * @example
975
1075
  * ```typescript
976
- * const result = await productRepository.deleteMany({
977
- * status: 'INACTIVE',
978
- * createdAt: { lt: new Date('2020-01-01') }
979
- * });
1076
+ * import { lt } from 'drizzle-orm';
1077
+ *
1078
+ * const result = await productRepository.deleteMany(
1079
+ * lt(products.createdAt, new Date('2020-01-01'))
1080
+ * );
980
1081
  * console.log(`Deleted ${result.count} products`);
981
1082
  * ```
982
1083
  */
983
- deleteMany(where: any): Promise<{
1084
+ deleteMany(where: SQL): Promise<{
984
1085
  count: number;
985
1086
  }>;
986
1087
  /**
987
1088
  * Count records
988
1089
  *
989
- * @param where - Optional where clause to filter records
1090
+ * @param where - Optional SQL condition to filter records
990
1091
  * @returns Promise resolving to the count of records
991
1092
  *
992
1093
  * @example
993
1094
  * ```typescript
1095
+ * import { eq } from 'drizzle-orm';
1096
+ *
994
1097
  * // Count all products
995
1098
  * const total = await productRepository.count();
996
1099
  *
997
1100
  * // Count active products
998
- * const activeCount = await productRepository.count({ status: 'ACTIVE' });
1101
+ * const activeCount = await productRepository.count(
1102
+ * eq(products.status, 'ACTIVE')
1103
+ * );
999
1104
  * ```
1000
1105
  */
1001
- count(where?: any): Promise<number>;
1106
+ count(where?: SQL): Promise<number>;
1002
1107
  /**
1003
1108
  * Check if a record exists
1004
1109
  *
1005
- * @param where - The where clause to match records
1110
+ * @param where - SQL condition to match records
1006
1111
  * @returns Promise resolving to true if at least one record exists, false otherwise
1007
1112
  *
1008
1113
  * @example
1009
1114
  * ```typescript
1010
- * const skuExists = await productRepository.exists({
1011
- * sku: 'WDG-001'
1012
- * });
1115
+ * import { eq } from 'drizzle-orm';
1116
+ *
1117
+ * const skuExists = await productRepository.exists(
1118
+ * eq(products.sku, 'WDG-001')
1119
+ * );
1013
1120
  * ```
1014
1121
  */
1015
- exists(where: any): Promise<boolean>;
1122
+ exists(where: SQL): Promise<boolean>;
1016
1123
  }
1017
1124
 
1018
1125
  declare class RequestService {
@@ -1336,7 +1443,7 @@ declare class HttpExceptionFilter implements ExceptionFilter {
1336
1443
  catch(exception: unknown, host: ArgumentsHost): void;
1337
1444
  }
1338
1445
 
1339
- interface FieldError$1 {
1446
+ interface FieldError {
1340
1447
  field?: string;
1341
1448
  message: string;
1342
1449
  }
@@ -1346,13 +1453,9 @@ interface ProblemDetails {
1346
1453
  detail: string;
1347
1454
  }
1348
1455
  interface ApiErrorResponse extends ProblemDetails {
1349
- errors: FieldError$1[];
1456
+ errors: FieldError[];
1350
1457
  }
1351
1458
 
1352
- interface FieldError {
1353
- field?: string;
1354
- message: string;
1355
- }
1356
1459
  declare abstract class BaseFieldException extends HttpException {
1357
1460
  constructor(statusOrMessageOrErrors: HttpStatus | string | FieldError[], messageOrStatus?: string | HttpStatus, statusOrDetail?: HttpStatus | string, detail?: string);
1358
1461
  }
@@ -2167,4 +2270,4 @@ declare function generateCorrelationId(): string;
2167
2270
  */
2168
2271
  declare function addCorrelationIdToResponse(reply: FastifyReply, correlationId: string, headerName?: string): void;
2169
2272
 
2170
- export { type ApiErrorResponse, AuthConfigModule, BadGatewayException, BadRequestException, BaseFieldException, ConflictException, type CorrelationContext, CorrelationIdMiddleware, CsrfGuard, DEFAULT_CORRELATION_HEADER, DatabaseModule, type DatabaseModuleOptions, type FieldError, ForbiddenException, GoneException, HttpExceptionFilter, HttpLoggerInterceptor, type HttpLoggerOptions, HttpModule, InternalServerErrorException, LOGGER_MODULE_OPTIONS, type LogFormat, type LogLevel, type LogMetadata, LoggerModule, type LoggerModuleAsyncOptions, type LoggerModuleOptions, type LoggerOptionsFactory, LoggerService, MethodNotAllowedException, NotAcceptableException, NotFoundException, NotImplementedException, Onboarding, PayloadTooLargeException, PrimaryBaseRepository, PrimaryDatabaseService, type PrimaryDbConfig, type ProblemDetails, Public, RequestTimeoutException, ServiceUnavailableException, Tenant, TenantBaseRepository, TenantContextService, TenantDatabaseService, type TenantInfo, TooManyRequestsException, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, ValidationException, VrittiAuthGuard, addCorrelationIdToResponse, correlationStorage, generateCorrelationId, getCorrelationContext, getHttpStatusTitle, runWithCorrelationContext, updateCorrelationContext };
2273
+ export { type ApiErrorResponse, AuthConfigModule, BadGatewayException, BadRequestException, BaseFieldException, ConflictException, type CorrelationContext, CorrelationIdMiddleware, CsrfGuard, DEFAULT_CORRELATION_HEADER, DatabaseModule, type DatabaseModuleOptions, type FieldError, ForbiddenException, GoneException, HttpExceptionFilter, HttpLoggerInterceptor, type HttpLoggerOptions, HttpModule, InternalServerErrorException, LOGGER_MODULE_OPTIONS, type LogFormat, type LogLevel, type LogMetadata, LoggerModule, type LoggerModuleAsyncOptions, type LoggerModuleOptions, type LoggerOptionsFactory, LoggerService, MethodNotAllowedException, NotAcceptableException, NotFoundException, NotImplementedException, Onboarding, PayloadTooLargeException, PrimaryBaseRepository, PrimaryDatabaseService, type PrimaryDbConfig, type ProblemDetails, Public, type RegisteredSchema, RequestTimeoutException, type SchemaRegistry, ServiceUnavailableException, Tenant, TenantBaseRepository, TenantContextService, TenantDatabaseService, type TenantInfo, TooManyRequestsException, type TypedDrizzleClient, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, ValidationException, VrittiAuthGuard, addCorrelationIdToResponse, correlationStorage, generateCorrelationId, getCorrelationContext, getHttpStatusTitle, runWithCorrelationContext, updateCorrelationContext };