@vritti/api-sdk 0.0.9 → 0.1.1

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.cts 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,17 @@ 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
155
+ * Import your schema from db/schema/index.ts and pass it here
156
+ * @example import * as schema from '@/db/schema'
157
+ */
158
+ drizzleSchema: RegisteredSchema;
159
+ /**
160
+ * Drizzle relations object from defineRelations()
161
+ * Required for relational queries (db.query.*.findFirst/findMany)
162
+ * @example import { relations } from '@/db/schema'
126
163
  */
127
- prismaClientConstructor: any;
164
+ drizzleRelations?: Record<string, any>;
128
165
  /**
129
166
  * Connection cache TTL in milliseconds
130
167
  * Idle connections will be closed after this period
@@ -356,7 +393,7 @@ declare class TenantContextService {
356
393
  * Service responsible for managing tenant-scoped database connections
357
394
  *
358
395
  * This service:
359
- * - Maintains a connection pool (Map<cacheKey, DbClient>)
396
+ * - Maintains a connection pool (Map<cacheKey, TenantConnection>)
360
397
  * - Creates new connections dynamically based on tenant context
361
398
  * - Reuses existing connections for the same tenant
362
399
  * - Supports both cloud schemas and enterprise databases
@@ -364,14 +401,14 @@ declare class TenantContextService {
364
401
  *
365
402
  * @example
366
403
  * // In a controller or service
367
- * const dbClient = await this.tenantDatabase.getDbClient<PrismaClient>();
368
- * const users = await dbClient.user.findMany();
404
+ * const db = this.tenantDatabase.drizzleClient;
405
+ * const users = await db.select().from(usersTable);
369
406
  */
370
407
  declare class TenantDatabaseService implements OnModuleDestroy {
371
408
  private readonly options;
372
409
  private readonly tenantContext;
373
410
  private readonly logger;
374
- /** Connection pool: Map<cacheKey, DbClient> */
411
+ /** Connection pool: Map<cacheKey, TenantConnection> */
375
412
  private readonly clients;
376
413
  /** Track last usage time for idle connection cleanup */
377
414
  private readonly clientLastUsed;
@@ -379,14 +416,18 @@ declare class TenantDatabaseService implements OnModuleDestroy {
379
416
  private cleanupInterval?;
380
417
  constructor(options: DatabaseModuleOptions, tenantContext: TenantContextService);
381
418
  /**
382
- * Get the Prisma client for the current tenant's database.
419
+ * Get the Drizzle client for the current tenant's database.
383
420
  * This returns the tenant-scoped database client.
384
421
  *
385
- * @returns Tenant-scoped database client instance
422
+ * @returns Tenant-scoped Drizzle database instance
386
423
  * @throws UnauthorizedException if tenant context not set
387
424
  * @throws InternalServerErrorException if connection fails
388
425
  */
389
- get prismaClient(): any;
426
+ get drizzleClient(): TypedDrizzleClient;
427
+ /**
428
+ * Get the Drizzle schema
429
+ */
430
+ get schema(): Record<string, unknown>;
390
431
  /**
391
432
  * Get tenant-scoped database client for the current request/message
392
433
  *
@@ -395,21 +436,17 @@ declare class TenantDatabaseService implements OnModuleDestroy {
395
436
  * 2. Builds a connection URL based on tenant type
396
437
  * 3. Returns cached client if exists, otherwise creates new one
397
438
  *
398
- * @returns Promise<Database client instance>
439
+ * @returns Drizzle database instance
399
440
  * @throws UnauthorizedException if tenant context not set
400
441
  * @throws InternalServerErrorException if connection fails
401
- *
402
- * @example
403
- * const dbClient = await tenantDatabase.getDbClient<PrismaClient>();
404
- * const users = await dbClient.user.findMany();
405
442
  */
406
443
  private getDbClient;
407
444
  /**
408
- * Create a new database client for the given tenant
445
+ * Create a new database client for the given tenant (synchronous)
409
446
  */
410
- private createDbClient;
447
+ private createDbClientSync;
411
448
  /**
412
- * Build connection URL for enterprise tenant (dedicated database)
449
+ * Build connection URL for tenant (dedicated database)
413
450
  */
414
451
  private buildTenantDbUrl;
415
452
  /**
@@ -455,8 +492,10 @@ declare class TenantDatabaseService implements OnModuleDestroy {
455
492
  declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
456
493
  private readonly options;
457
494
  private readonly logger;
458
- /** Primary database client for querying tenant registry */
459
- private primaryDbClient;
495
+ /** PostgreSQL connection pool */
496
+ private pool;
497
+ /** Drizzle database instance */
498
+ private db;
460
499
  /** In-memory cache: Map<tenantIdentifier, TenantConfig> */
461
500
  private readonly tenantConfigCache;
462
501
  /** Cache TTL in milliseconds */
@@ -464,9 +503,9 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
464
503
  constructor(options: DatabaseModuleOptions);
465
504
  onModuleInit(): Promise<void>;
466
505
  /**
467
- * Initialize connection to primary database
506
+ * Initialize connection to primary database using Drizzle
468
507
  */
469
- private initializePrimaryDbClient;
508
+ private initializeDrizzleClient;
470
509
  /**
471
510
  * Build connection URL from primary database properties
472
511
  */
@@ -476,9 +515,9 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
476
515
  */
477
516
  private maskPassword;
478
517
  /**
479
- * Get tenant configuration by identifier (ID or slug)
518
+ * Get tenant configuration by identifier (ID or subdomain)
480
519
  *
481
- * @param tenantIdentifier Tenant ID or slug
520
+ * @param tenantIdentifier Tenant ID or subdomain
482
521
  * @returns Tenant configuration or null if not found
483
522
  */
484
523
  getTenantInfo(tenantIdentifier: string): Promise<TenantInfo | null>;
@@ -491,7 +530,7 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
491
530
  *
492
531
  * Useful when tenant settings are updated and cache needs to be invalidated
493
532
  *
494
- * @param tenantIdentifier Tenant ID or slug
533
+ * @param tenantIdentifier Tenant ID or subdomain
495
534
  */
496
535
  clearTenantCache(tenantIdentifier: string): void;
497
536
  /**
@@ -499,13 +538,17 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
499
538
  */
500
539
  clearAllCaches(): void;
501
540
  /**
502
- * Get the Prisma client for the primary database.
503
- * This is a synchronous property that returns the initialized Prisma client.
541
+ * Get the Drizzle database instance for the primary database.
542
+ * This is a synchronous property that returns the initialized Drizzle client.
504
543
  *
505
- * @returns Primary database client instance
544
+ * @returns Primary database Drizzle instance
506
545
  * @throws Error if primary database client is not initialized
507
546
  */
508
- get prismaClient(): any;
547
+ get drizzleClient(): TypedDrizzleClient;
548
+ /**
549
+ * Get the Drizzle schema
550
+ */
551
+ get schema(): typeof this$1.options.drizzleSchema;
509
552
  /**
510
553
  * Decrypt database credentials
511
554
  *
@@ -519,90 +562,147 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
519
562
  }
520
563
 
521
564
  /**
522
- * Abstract base repository for primary database operations.
565
+ * Drizzle ORM v2 object-based where filter type.
566
+ * Supports simple equality, operators, AND/OR/NOT, and RAW SQL.
567
+ *
568
+ * @example
569
+ * ```typescript
570
+ * // Simple equality
571
+ * { email: 'user@example.com' }
572
+ *
573
+ * // With operators
574
+ * { age: { gt: 18, lt: 65 } }
575
+ *
576
+ * // AND/OR combinations
577
+ * { AND: [{ status: 'ACTIVE' }, { age: { gte: 18 } }] }
578
+ *
579
+ * // RAW SQL expression
580
+ * { RAW: (table) => sql`${table.email} ILIKE '%@gmail.com'` }
581
+ * ```
582
+ */
583
+ type RelationsWhereFilter = Record<string, any>;
584
+ /**
585
+ * Type-safe wrapper for Drizzle's RelationalQueryBuilder (v2 API).
586
+ * This interface matches the method signatures of RelationalQueryBuilder
587
+ * but properly binds the TSelect generic for type safety.
588
+ *
589
+ * We use this instead of RelationalQueryBuilder directly because
590
+ * TypeScript cannot infer TSelect from the generic base repository context.
591
+ *
592
+ * @remarks
593
+ * Drizzle ORM v2 uses object-based `where` filters instead of SQL expressions.
594
+ * See: https://orm.drizzle.team/docs/relations-v1-v2
595
+ */
596
+ interface TypedRelationalQueryBuilder<TSelect> {
597
+ findFirst(config?: {
598
+ where?: RelationsWhereFilter;
599
+ with?: Record<string, unknown>;
600
+ columns?: Record<string, boolean>;
601
+ }): Promise<TSelect | undefined>;
602
+ findMany(config?: {
603
+ where?: RelationsWhereFilter;
604
+ orderBy?: Record<string, 'asc' | 'desc'>;
605
+ limit?: number;
606
+ offset?: number;
607
+ with?: Record<string, unknown>;
608
+ columns?: Record<string, boolean>;
609
+ }): Promise<TSelect[]>;
610
+ }
611
+ /**
612
+ * Abstract base repository for primary database operations using Drizzle ORM.
523
613
  * Provides common CRUD operations with automatic logging.
524
614
  *
525
- * @template TModel - The Prisma model type
526
- * @template TCreateDTO - DTO type for create operations
527
- * @template TUpdateDTO - DTO type for update operations
615
+ * @template TTable - The Drizzle table type (must be registered in SchemaRegistry)
616
+ * @template TInsert - Type for insert operations (inferred from table.$inferInsert)
617
+ * @template TSelect - Type for select operations (inferred from table.$inferSelect)
618
+ *
619
+ * @remarks
620
+ * **Type Assertion Pattern:** This repository uses `as any` casts when passing
621
+ * the generic table to Drizzle methods. This is necessary because TypeScript
622
+ * cannot prove that a generic `TTable extends PgTable` satisfies Drizzle's
623
+ * stricter internal type requirements for `insert()`, `update()`, and `delete()`.
624
+ *
625
+ * The public API maintains full type safety:
626
+ * - Input parameters are typed as `TInsert` (inferred from table)
627
+ * - Return values are typed as `TSelect` (inferred from table)
628
+ * - The casts are implementation details that don't leak to consumers
528
629
  *
529
630
  * @example
530
631
  * ```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
- * }
632
+ * import { users } from '@/db/schema';
542
633
  *
543
- * // Add custom methods as needed
544
- * async findByEmail(email: string): Promise<User | null> {
545
- * return this.model.findUnique({ where: { email } });
546
- * }
547
- * }
634
+ * type User = typeof users.$inferSelect;
635
+ * type NewUser = typeof users.$inferInsert;
548
636
  *
549
- * // Short syntax is also supported
550
637
  * @Injectable()
551
- * export class TenantRepository extends PrimaryBaseRepository<Tenant> {
638
+ * export class UserRepository extends PrimaryBaseRepository<typeof users> {
552
639
  * constructor(database: PrimaryDatabaseService) {
553
- * super(database, (p) => p.tenant); // ✅ Concise!
640
+ * super(database, users);
554
641
  * }
555
- * }
556
642
  *
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
643
+ * // Use Drizzle v2 object-based where syntax (recommended)
644
+ * async findByEmail(email: string): Promise<User | undefined> {
645
+ * return this.model.findFirst({
646
+ * where: { email },
647
+ * });
648
+ * }
649
+ *
650
+ * // With relations
651
+ * async findWithRelations(id: string): Promise<User | undefined> {
652
+ * return this.model.findFirst({
653
+ * where: { id },
654
+ * with: { posts: true, profile: true }
655
+ * });
562
656
  * }
563
657
  * }
564
658
  * ```
565
659
  */
566
- declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDTO = any> {
660
+ declare abstract class PrimaryBaseRepository<TTable extends PgTable, TInsert = InferInsertModel<TTable>, TSelect = InferSelectModel<TTable>> {
567
661
  protected readonly database: PrimaryDatabaseService;
662
+ protected readonly table: TTable;
568
663
  protected readonly logger: Logger;
569
- private readonly modelGetter;
570
664
  /**
571
- * Lazy getter for Prisma client.
665
+ * The table name extracted from the Drizzle table at runtime.
666
+ * Used to access the query API for this repository's table.
667
+ */
668
+ private readonly tableName;
669
+ /**
670
+ * Lazy getter for Drizzle client.
572
671
  * Accesses the client from the database service only when needed,
573
672
  * avoiding initialization timing issues with NestJS lifecycle.
574
673
  */
575
- protected get prisma(): any;
674
+ protected get db(): TypedDrizzleClient;
576
675
  /**
577
- * Lazy getter for the Prisma model delegate.
578
- * Returns the specific model (e.g., prisma.user, prisma.tenant) for this repository.
676
+ * Model query API for THIS repository's table (Drizzle v2 relational queries)
677
+ * Scoped to only the table this repository manages.
678
+ * Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * // Use relational queries with v2 object-based where syntax
683
+ * const user = await this.model.findFirst({
684
+ * where: { id },
685
+ * with: { posts: true, profile: true }
686
+ * });
687
+ * ```
579
688
  */
580
- protected get model(): any;
689
+ protected get model(): TypedRelationalQueryBuilder<TSelect>;
581
690
  /**
582
691
  * Create a new repository instance
583
692
  *
584
693
  * @param database - The primary database service
585
- * @param getModel - Function that returns the Prisma model delegate from the client
694
+ * @param table - The Drizzle table schema object
586
695
  *
587
696
  * @example
588
697
  * ```typescript
589
- * // Standard usage with full parameter name
590
- * constructor(database: PrimaryDatabaseService) {
591
- * super(database, (prisma) => prisma.user);
592
- * }
593
- *
594
- * // Short syntax
595
- * constructor(database: PrimaryDatabaseService) {
596
- * super(database, (p) => p.user);
597
- * }
698
+ * import { users } from '@/db/schema';
598
699
  *
599
- * // Complex model names
600
700
  * constructor(database: PrimaryDatabaseService) {
601
- * super(database, (p) => p.emailVerification);
701
+ * super(database, users);
602
702
  * }
603
703
  * ```
604
704
  */
605
- constructor(database: PrimaryDatabaseService, getModel: (prisma: any) => any);
705
+ constructor(database: PrimaryDatabaseService, table: TTable);
606
706
  /**
607
707
  * Create a new record
608
708
  *
@@ -613,46 +713,49 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
613
713
  * ```typescript
614
714
  * const user = await userRepository.create({
615
715
  * email: 'user@example.com',
616
- * name: 'John Doe'
716
+ * firstName: 'John'
617
717
  * });
618
718
  * ```
619
719
  */
620
- create(data: TCreateDTO): Promise<TModel>;
720
+ create(data: TInsert): Promise<TSelect>;
621
721
  /**
622
722
  * Find a single record by ID
623
723
  *
624
724
  * @param id - The record ID
625
- * @returns Promise resolving to the record or null if not found
725
+ * @returns Promise resolving to the record or undefined if not found
626
726
  *
627
727
  * @example
628
728
  * ```typescript
629
729
  * const user = await userRepository.findById('user-id-123');
630
730
  * ```
631
731
  */
632
- findById(id: string): Promise<TModel | null>;
732
+ findById(id: string): Promise<TSelect | undefined>;
633
733
  /**
634
- * Find a single record with custom where clause
734
+ * Find a single record with custom where clause (Drizzle v2 object-based syntax)
635
735
  *
636
- * @param where - The where clause or findUnique args
637
- * @returns Promise resolving to the record or null if not found
736
+ * @param where - Object-based filter condition
737
+ * @returns Promise resolving to the record or undefined if not found
638
738
  *
639
739
  * @example
640
740
  * ```typescript
641
- * // Simple where clause
741
+ * // Simple equality
642
742
  * const user = await userRepository.findOne({ email: 'user@example.com' });
643
743
  *
644
- * // With include
744
+ * // With operators
745
+ * const user = await userRepository.findOne({ age: { gte: 18 } });
746
+ *
747
+ * // Multiple conditions (AND)
645
748
  * const user = await userRepository.findOne({
646
- * where: { email: 'user@example.com' },
647
- * include: { posts: true }
749
+ * email: 'user@example.com',
750
+ * status: 'ACTIVE'
648
751
  * });
649
752
  * ```
650
753
  */
651
- findOne(where: any): Promise<TModel | null>;
754
+ findOne(where: RelationsWhereFilter): Promise<TSelect | undefined>;
652
755
  /**
653
- * Find multiple records
756
+ * Find multiple records (Drizzle v2 object-based syntax)
654
757
  *
655
- * @param args - Prisma findMany arguments (where, orderBy, take, skip, etc.)
758
+ * @param options - Query options (where, orderBy, limit, offset)
656
759
  * @returns Promise resolving to an array of records
657
760
  *
658
761
  * @example
@@ -660,16 +763,31 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
660
763
  * // Find all users
661
764
  * const users = await userRepository.findMany();
662
765
  *
663
- * // Find with filtering and pagination
766
+ * // Find with filtering and pagination (v2 object syntax)
664
767
  * const users = await userRepository.findMany({
665
- * where: { status: 'ACTIVE' },
768
+ * where: { accountStatus: 'ACTIVE' },
666
769
  * orderBy: { createdAt: 'desc' },
667
- * take: 10,
668
- * skip: 0
770
+ * limit: 10,
771
+ * offset: 0
772
+ * });
773
+ *
774
+ * // Multiple conditions
775
+ * const users = await userRepository.findMany({
776
+ * where: {
777
+ * AND: [
778
+ * { status: 'ACTIVE' },
779
+ * { age: { gte: 18 } }
780
+ * ]
781
+ * }
669
782
  * });
670
783
  * ```
671
784
  */
672
- findMany(args?: any): Promise<TModel[]>;
785
+ findMany(options?: {
786
+ where?: RelationsWhereFilter;
787
+ orderBy?: Record<string, 'asc' | 'desc'>;
788
+ limit?: number;
789
+ offset?: number;
790
+ }): Promise<TSelect[]>;
673
791
  /**
674
792
  * Update a record by ID
675
793
  *
@@ -680,28 +798,30 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
680
798
  * @example
681
799
  * ```typescript
682
800
  * const user = await userRepository.update('user-id-123', {
683
- * name: 'Jane Doe'
801
+ * firstName: 'Jane'
684
802
  * });
685
803
  * ```
686
804
  */
687
- update(id: string, data: TUpdateDTO): Promise<TModel>;
805
+ update(id: string, data: Partial<TInsert>): Promise<TSelect>;
688
806
  /**
689
807
  * Update multiple records
690
808
  *
691
- * @param where - The where clause to match records
809
+ * @param where - SQL condition to match records
692
810
  * @param data - The data to update
693
811
  * @returns Promise resolving to the count of updated records
694
812
  *
695
813
  * @example
696
814
  * ```typescript
815
+ * import { eq } from 'drizzle-orm';
816
+ *
697
817
  * const result = await userRepository.updateMany(
698
- * { status: 'PENDING' },
699
- * { status: 'ACTIVE' }
818
+ * eq(users.accountStatus, 'PENDING'),
819
+ * { accountStatus: 'ACTIVE' }
700
820
  * );
701
821
  * console.log(`Updated ${result.count} users`);
702
822
  * ```
703
823
  */
704
- updateMany(where: any, data: TUpdateDTO): Promise<{
824
+ updateMany(where: SQL, data: Partial<TInsert>): Promise<{
705
825
  count: number;
706
826
  }>;
707
827
  /**
@@ -715,142 +835,166 @@ declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDT
715
835
  * const user = await userRepository.delete('user-id-123');
716
836
  * ```
717
837
  */
718
- delete(id: string): Promise<TModel>;
838
+ delete(id: string): Promise<TSelect>;
719
839
  /**
720
840
  * Delete multiple records
721
841
  *
722
- * @param where - The where clause to match records
842
+ * @param where - SQL condition to match records
723
843
  * @returns Promise resolving to the count of deleted records
724
844
  *
725
845
  * @example
726
846
  * ```typescript
727
- * const result = await userRepository.deleteMany({
728
- * status: 'INACTIVE',
729
- * createdAt: { lt: new Date('2020-01-01') }
730
- * });
847
+ * import { lt } from 'drizzle-orm';
848
+ *
849
+ * const result = await userRepository.deleteMany(
850
+ * lt(users.createdAt, new Date('2020-01-01'))
851
+ * );
731
852
  * console.log(`Deleted ${result.count} users`);
732
853
  * ```
733
854
  */
734
- deleteMany(where: any): Promise<{
855
+ deleteMany(where: SQL): Promise<{
735
856
  count: number;
736
857
  }>;
737
858
  /**
738
859
  * Count records
739
860
  *
740
- * @param where - Optional where clause to filter records
861
+ * @param where - Optional SQL condition to filter records
741
862
  * @returns Promise resolving to the count of records
742
863
  *
743
864
  * @example
744
865
  * ```typescript
866
+ * import { eq } from 'drizzle-orm';
867
+ *
745
868
  * // Count all users
746
869
  * const total = await userRepository.count();
747
870
  *
748
871
  * // Count active users
749
- * const activeCount = await userRepository.count({ status: 'ACTIVE' });
872
+ * const activeCount = await userRepository.count(
873
+ * eq(users.accountStatus, 'ACTIVE')
874
+ * );
750
875
  * ```
751
876
  */
752
- count(where?: any): Promise<number>;
877
+ count(where?: SQL): Promise<number>;
753
878
  /**
754
879
  * Check if a record exists
755
880
  *
756
- * @param where - The where clause to match records
881
+ * @param where - SQL condition to match records
757
882
  * @returns Promise resolving to true if at least one record exists, false otherwise
758
883
  *
759
884
  * @example
760
885
  * ```typescript
761
- * const emailExists = await userRepository.exists({
762
- * email: 'user@example.com'
763
- * });
886
+ * import { eq } from 'drizzle-orm';
887
+ *
888
+ * const emailExists = await userRepository.exists(
889
+ * eq(users.email, 'user@example.com')
890
+ * );
764
891
  * ```
765
892
  */
766
- exists(where: any): Promise<boolean>;
893
+ exists(where: SQL): Promise<boolean>;
767
894
  }
768
895
 
769
896
  /**
770
- * Abstract base repository for tenant-scoped database operations.
897
+ * Type helper to extract table name from Drizzle table.
898
+ * TTable['_']['name'] gives us the string literal type (e.g., 'products')
899
+ */
900
+ type ExtractTableName<TTable extends PgTable> = TTable['_']['name'];
901
+ /**
902
+ * Abstract base repository for tenant-scoped database operations using Drizzle ORM.
771
903
  * All operations are automatically scoped to the current tenant.
772
904
  *
773
- * @template TModel - The Prisma model type
774
- * @template TCreateDTO - DTO type for create operations
775
- * @template TUpdateDTO - DTO type for update operations
905
+ * @template TTable - The Drizzle table type (must be registered in SchemaRegistry)
906
+ * @template TInsert - Type for insert operations (inferred from table.$inferInsert)
907
+ * @template TSelect - Type for select operations (inferred from table.$inferSelect)
908
+ *
909
+ * @remarks
910
+ * **Type Assertion Pattern:** This repository uses `as any` casts when passing
911
+ * the generic table to Drizzle methods. This is necessary because TypeScript
912
+ * cannot prove that a generic `TTable extends PgTable` satisfies Drizzle's
913
+ * stricter internal type requirements for `insert()`, `update()`, and `delete()`.
914
+ *
915
+ * The public API maintains full type safety:
916
+ * - Input parameters are typed as `TInsert` (inferred from table)
917
+ * - Return values are typed as `TSelect` (inferred from table)
918
+ * - The casts are implementation details that don't leak to consumers
776
919
  *
777
920
  * @example
778
921
  * ```typescript
779
- * // Using the model delegate pattern (RECOMMENDED)
780
- * // Type-safe, IDE autocomplete, refactor-friendly
922
+ * import { products } from '@/db/schema';
923
+ *
924
+ * type Product = typeof products.$inferSelect;
925
+ * type NewProduct = typeof products.$inferInsert;
926
+ *
781
927
  * @Injectable()
782
- * export class ProductRepository extends TenantBaseRepository<
783
- * Product,
784
- * CreateProductDto,
785
- * UpdateProductDto
786
- * > {
928
+ * export class ProductRepository extends TenantBaseRepository<typeof products> {
787
929
  * constructor(database: TenantDatabaseService) {
788
- * super(database, (prisma) => prisma.product); // ✅ Type-safe with autocomplete!
930
+ * super(database, products);
789
931
  * }
790
932
  *
791
- * // Add custom methods as needed
933
+ * // Use SQL-builder syntax
792
934
  * async findBySku(sku: string): Promise<Product | null> {
793
- * return this.model.findUnique({ where: { sku } });
935
+ * const [result] = await this.db
936
+ * .select()
937
+ * .from(this.table)
938
+ * .where(eq(products.sku, sku))
939
+ * .limit(1);
940
+ * return result ?? null;
794
941
  * }
795
- * }
796
942
  *
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
943
+ * // Use Prisma-like relational query syntax
944
+ * async findWithRelations(id: string): Promise<Product | null> {
945
+ * return await this.model.findFirst({
946
+ * where: eq(products.id, id),
947
+ * with: { category: true, variants: true }
948
+ * });
810
949
  * }
811
950
  * }
812
951
  * ```
813
952
  */
814
- declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO = any> {
953
+ declare abstract class TenantBaseRepository<TTable extends PgTable, TInsert = InferInsertModel<TTable>, TSelect = InferSelectModel<TTable>> {
815
954
  protected readonly database: TenantDatabaseService;
955
+ protected readonly table: TTable;
816
956
  protected readonly logger: Logger;
817
- private readonly modelGetter;
818
957
  /**
819
- * Lazy getter for Prisma client.
958
+ * The table name extracted from the Drizzle table at runtime.
959
+ * Used to access the query API for this repository's table.
960
+ */
961
+ private readonly tableName;
962
+ /**
963
+ * Lazy getter for Drizzle client.
820
964
  * Accesses the client from the database service only when needed,
821
965
  * avoiding initialization timing issues with NestJS lifecycle.
822
966
  */
823
- protected get prisma(): any;
967
+ protected get db(): TypedDrizzleClient;
824
968
  /**
825
- * Lazy getter for the Prisma model delegate.
826
- * Returns the specific model (e.g., prisma.product, prisma.order) for this repository.
969
+ * Model query API for THIS repository's table (Prisma-like syntax)
970
+ * Scoped to only the table this repository manages
971
+ *
972
+ * @example
973
+ * ```typescript
974
+ * // Use relational queries with type safety
975
+ * const product = await this.model.findFirst({
976
+ * where: eq(products.id, id),
977
+ * with: { category: true, variants: true }
978
+ * });
979
+ * ```
827
980
  */
828
- protected get model(): any;
981
+ protected get model(): TypedDrizzleClient['query'][ExtractTableName<TTable> & keyof TypedDrizzleClient['query']];
829
982
  /**
830
983
  * Create a new repository instance
831
984
  *
832
985
  * @param database - The tenant database service
833
- * @param getModel - Function that returns the Prisma model delegate from the client
986
+ * @param table - The Drizzle table schema object
834
987
  *
835
988
  * @example
836
989
  * ```typescript
837
- * // Standard usage with full parameter name
838
- * constructor(database: TenantDatabaseService) {
839
- * super(database, (prisma) => prisma.product);
840
- * }
841
- *
842
- * // Short syntax
843
- * constructor(database: TenantDatabaseService) {
844
- * super(database, (p) => p.product);
845
- * }
990
+ * import { products } from '@/db/schema';
846
991
  *
847
- * // Complex model names
848
992
  * constructor(database: TenantDatabaseService) {
849
- * super(database, (p) => p.inventoryItem);
993
+ * super(database, products);
850
994
  * }
851
995
  * ```
852
996
  */
853
- constructor(database: TenantDatabaseService, getModel: (prisma: any) => any);
997
+ constructor(database: TenantDatabaseService, table: TTable);
854
998
  /**
855
999
  * Create a new record
856
1000
  *
@@ -866,7 +1010,7 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
866
1010
  * });
867
1011
  * ```
868
1012
  */
869
- create(data: TCreateDTO): Promise<TModel>;
1013
+ create(data: TInsert): Promise<TSelect>;
870
1014
  /**
871
1015
  * Find a single record by ID
872
1016
  *
@@ -878,47 +1022,48 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
878
1022
  * const product = await productRepository.findById('product-id-123');
879
1023
  * ```
880
1024
  */
881
- findById(id: string): Promise<TModel | null>;
1025
+ findById(id: string): Promise<TSelect | null>;
882
1026
  /**
883
1027
  * Find a single record with custom where clause
884
1028
  *
885
- * @param where - The where clause or findUnique args
1029
+ * @param where - SQL condition
886
1030
  * @returns Promise resolving to the record or null if not found
887
1031
  *
888
1032
  * @example
889
1033
  * ```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
- * });
1034
+ * import { eq } from 'drizzle-orm';
1035
+ * const product = await productRepository.findOne(eq(products.sku, 'WDG-001'));
898
1036
  * ```
899
1037
  */
900
- findOne(where: any): Promise<TModel | null>;
1038
+ findOne(where: SQL): Promise<TSelect | null>;
901
1039
  /**
902
1040
  * Find multiple records
903
1041
  *
904
- * @param args - Prisma findMany arguments (where, orderBy, take, skip, etc.)
1042
+ * @param options - Query options (where, orderBy, limit, offset)
905
1043
  * @returns Promise resolving to an array of records
906
1044
  *
907
1045
  * @example
908
1046
  * ```typescript
1047
+ * import { eq, desc } from 'drizzle-orm';
1048
+ *
909
1049
  * // Find all products
910
1050
  * const products = await productRepository.findMany();
911
1051
  *
912
1052
  * // Find with filtering and pagination
913
1053
  * const products = await productRepository.findMany({
914
- * where: { status: 'ACTIVE' },
915
- * orderBy: { createdAt: 'desc' },
916
- * take: 10,
917
- * skip: 0
1054
+ * where: eq(products.status, 'ACTIVE'),
1055
+ * orderBy: desc(products.createdAt),
1056
+ * limit: 10,
1057
+ * offset: 0
918
1058
  * });
919
1059
  * ```
920
1060
  */
921
- findMany(args?: any): Promise<TModel[]>;
1061
+ findMany(options?: {
1062
+ where?: SQL;
1063
+ orderBy?: SQL;
1064
+ limit?: number;
1065
+ offset?: number;
1066
+ }): Promise<TSelect[]>;
922
1067
  /**
923
1068
  * Update a record by ID
924
1069
  *
@@ -933,24 +1078,26 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
933
1078
  * });
934
1079
  * ```
935
1080
  */
936
- update(id: string, data: TUpdateDTO): Promise<TModel>;
1081
+ update(id: string, data: Partial<TInsert>): Promise<TSelect>;
937
1082
  /**
938
1083
  * Update multiple records
939
1084
  *
940
- * @param where - The where clause to match records
1085
+ * @param where - SQL condition to match records
941
1086
  * @param data - The data to update
942
1087
  * @returns Promise resolving to the count of updated records
943
1088
  *
944
1089
  * @example
945
1090
  * ```typescript
1091
+ * import { eq } from 'drizzle-orm';
1092
+ *
946
1093
  * const result = await productRepository.updateMany(
947
- * { status: 'PENDING' },
1094
+ * eq(products.status, 'PENDING'),
948
1095
  * { status: 'ACTIVE' }
949
1096
  * );
950
1097
  * console.log(`Updated ${result.count} products`);
951
1098
  * ```
952
1099
  */
953
- updateMany(where: any, data: TUpdateDTO): Promise<{
1100
+ updateMany(where: SQL, data: Partial<TInsert>): Promise<{
954
1101
  count: number;
955
1102
  }>;
956
1103
  /**
@@ -964,55 +1111,62 @@ declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO
964
1111
  * const product = await productRepository.delete('product-id-123');
965
1112
  * ```
966
1113
  */
967
- delete(id: string): Promise<TModel>;
1114
+ delete(id: string): Promise<TSelect>;
968
1115
  /**
969
1116
  * Delete multiple records
970
1117
  *
971
- * @param where - The where clause to match records
1118
+ * @param where - SQL condition to match records
972
1119
  * @returns Promise resolving to the count of deleted records
973
1120
  *
974
1121
  * @example
975
1122
  * ```typescript
976
- * const result = await productRepository.deleteMany({
977
- * status: 'INACTIVE',
978
- * createdAt: { lt: new Date('2020-01-01') }
979
- * });
1123
+ * import { lt } from 'drizzle-orm';
1124
+ *
1125
+ * const result = await productRepository.deleteMany(
1126
+ * lt(products.createdAt, new Date('2020-01-01'))
1127
+ * );
980
1128
  * console.log(`Deleted ${result.count} products`);
981
1129
  * ```
982
1130
  */
983
- deleteMany(where: any): Promise<{
1131
+ deleteMany(where: SQL): Promise<{
984
1132
  count: number;
985
1133
  }>;
986
1134
  /**
987
1135
  * Count records
988
1136
  *
989
- * @param where - Optional where clause to filter records
1137
+ * @param where - Optional SQL condition to filter records
990
1138
  * @returns Promise resolving to the count of records
991
1139
  *
992
1140
  * @example
993
1141
  * ```typescript
1142
+ * import { eq } from 'drizzle-orm';
1143
+ *
994
1144
  * // Count all products
995
1145
  * const total = await productRepository.count();
996
1146
  *
997
1147
  * // Count active products
998
- * const activeCount = await productRepository.count({ status: 'ACTIVE' });
1148
+ * const activeCount = await productRepository.count(
1149
+ * eq(products.status, 'ACTIVE')
1150
+ * );
999
1151
  * ```
1000
1152
  */
1001
- count(where?: any): Promise<number>;
1153
+ count(where?: SQL): Promise<number>;
1002
1154
  /**
1003
1155
  * Check if a record exists
1004
1156
  *
1005
- * @param where - The where clause to match records
1157
+ * @param where - SQL condition to match records
1006
1158
  * @returns Promise resolving to true if at least one record exists, false otherwise
1007
1159
  *
1008
1160
  * @example
1009
1161
  * ```typescript
1010
- * const skuExists = await productRepository.exists({
1011
- * sku: 'WDG-001'
1012
- * });
1162
+ * import { eq } from 'drizzle-orm';
1163
+ *
1164
+ * const skuExists = await productRepository.exists(
1165
+ * eq(products.sku, 'WDG-001')
1166
+ * );
1013
1167
  * ```
1014
1168
  */
1015
- exists(where: any): Promise<boolean>;
1169
+ exists(where: SQL): Promise<boolean>;
1016
1170
  }
1017
1171
 
1018
1172
  declare class RequestService {
@@ -1336,7 +1490,7 @@ declare class HttpExceptionFilter implements ExceptionFilter {
1336
1490
  catch(exception: unknown, host: ArgumentsHost): void;
1337
1491
  }
1338
1492
 
1339
- interface FieldError$1 {
1493
+ interface FieldError {
1340
1494
  field?: string;
1341
1495
  message: string;
1342
1496
  }
@@ -1346,13 +1500,9 @@ interface ProblemDetails {
1346
1500
  detail: string;
1347
1501
  }
1348
1502
  interface ApiErrorResponse extends ProblemDetails {
1349
- errors: FieldError$1[];
1503
+ errors: FieldError[];
1350
1504
  }
1351
1505
 
1352
- interface FieldError {
1353
- field?: string;
1354
- message: string;
1355
- }
1356
1506
  declare abstract class BaseFieldException extends HttpException {
1357
1507
  constructor(statusOrMessageOrErrors: HttpStatus | string | FieldError[], messageOrStatus?: string | HttpStatus, statusOrDetail?: HttpStatus | string, detail?: string);
1358
1508
  }
@@ -2167,4 +2317,4 @@ declare function generateCorrelationId(): string;
2167
2317
  */
2168
2318
  declare function addCorrelationIdToResponse(reply: FastifyReply, correlationId: string, headerName?: string): void;
2169
2319
 
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 };
2320
+ 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 };