/** * Example: User Repository Implementation * * This example demonstrates how to implement IUserRepository * using TypeORM as the underlying data access layer. * * @module @erp-suite/core/examples */ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User, IUserRepository, ServiceContext, PaginatedResult, PaginationOptions, QueryOptions, } from '@erp-suite/core'; /** * User repository implementation * * Implements IUserRepository interface with TypeORM * * @example * ```typescript * // In your module * @Module({ * imports: [TypeOrmModule.forFeature([User])], * providers: [UserRepository], * exports: [UserRepository], * }) * export class UserModule {} * * // In your service * const factory = RepositoryFactory.getInstance(); * const userRepo = factory.getRequired('UserRepository'); * const user = await userRepo.findByEmail(ctx, 'user@example.com'); * ``` */ @Injectable() export class UserRepository implements IUserRepository { constructor( @InjectRepository(User) private readonly ormRepo: Repository, ) {} // ============================================================================ // Core CRUD Operations (from IRepository) // ============================================================================ async findById( ctx: ServiceContext, id: string, options?: QueryOptions, ): Promise { return this.ormRepo.findOne({ where: { id, tenantId: ctx.tenantId }, relations: options?.relations, select: options?.select as any, }); } async findOne( ctx: ServiceContext, criteria: Partial, options?: QueryOptions, ): Promise { return this.ormRepo.findOne({ where: { ...criteria, tenantId: ctx.tenantId }, relations: options?.relations, select: options?.select as any, }); } async findAll( ctx: ServiceContext, filters?: PaginationOptions & Partial, options?: QueryOptions, ): Promise> { const page = filters?.page || 1; const pageSize = filters?.pageSize || 20; // Extract pagination params const { page: _, pageSize: __, ...criteria } = filters || {}; const [data, total] = await this.ormRepo.findAndCount({ where: { ...criteria, tenantId: ctx.tenantId }, relations: options?.relations, select: options?.select as any, skip: (page - 1) * pageSize, take: pageSize, order: { createdAt: 'DESC' }, }); return { data, meta: { page, pageSize, totalRecords: total, totalPages: Math.ceil(total / pageSize), }, }; } async findMany( ctx: ServiceContext, criteria: Partial, options?: QueryOptions, ): Promise { return this.ormRepo.find({ where: { ...criteria, tenantId: ctx.tenantId }, relations: options?.relations, select: options?.select as any, }); } async create(ctx: ServiceContext, data: Partial): Promise { const user = this.ormRepo.create({ ...data, tenantId: ctx.tenantId, }); return this.ormRepo.save(user); } async createMany(ctx: ServiceContext, data: Partial[]): Promise { const users = data.map(item => this.ormRepo.create({ ...item, tenantId: ctx.tenantId, }), ); return this.ormRepo.save(users); } async update( ctx: ServiceContext, id: string, data: Partial, ): Promise { await this.ormRepo.update( { id, tenantId: ctx.tenantId }, data, ); return this.findById(ctx, id); } async updateMany( ctx: ServiceContext, criteria: Partial, data: Partial, ): Promise { const result = await this.ormRepo.update( { ...criteria, tenantId: ctx.tenantId }, data, ); return result.affected || 0; } async softDelete(ctx: ServiceContext, id: string): Promise { const result = await this.ormRepo.softDelete({ id, tenantId: ctx.tenantId, }); return (result.affected || 0) > 0; } async hardDelete(ctx: ServiceContext, id: string): Promise { const result = await this.ormRepo.delete({ id, tenantId: ctx.tenantId, }); return (result.affected || 0) > 0; } async deleteMany(ctx: ServiceContext, criteria: Partial): Promise { const result = await this.ormRepo.delete({ ...criteria, tenantId: ctx.tenantId, }); return result.affected || 0; } async count( ctx: ServiceContext, criteria?: Partial, options?: QueryOptions, ): Promise { return this.ormRepo.count({ where: { ...criteria, tenantId: ctx.tenantId }, relations: options?.relations, }); } async exists( ctx: ServiceContext, id: string, options?: QueryOptions, ): Promise { const count = await this.ormRepo.count({ where: { id, tenantId: ctx.tenantId }, relations: options?.relations, }); return count > 0; } async query( ctx: ServiceContext, sql: string, params: unknown[], ): Promise { // Add tenant filtering to raw SQL const tenantParam = ctx.tenantId; return this.ormRepo.query(sql, [...params, tenantParam]); } async queryOne( ctx: ServiceContext, sql: string, params: unknown[], ): Promise { const results = await this.query(ctx, sql, params); return results[0] || null; } // ============================================================================ // User-Specific Operations (from IUserRepository) // ============================================================================ async findByEmail( ctx: ServiceContext, email: string, ): Promise { return this.ormRepo.findOne({ where: { email, tenantId: ctx.tenantId, }, }); } async findByTenantId( ctx: ServiceContext, tenantId: string, ): Promise { // Note: This bypasses ctx.tenantId for admin use cases return this.ormRepo.find({ where: { tenantId }, order: { createdAt: 'DESC' }, }); } async findActiveUsers( ctx: ServiceContext, filters?: PaginationOptions, ): Promise> { const page = filters?.page || 1; const pageSize = filters?.pageSize || 20; const [data, total] = await this.ormRepo.findAndCount({ where: { tenantId: ctx.tenantId, status: 'active', }, skip: (page - 1) * pageSize, take: pageSize, order: { fullName: 'ASC' }, }); return { data, meta: { page, pageSize, totalRecords: total, totalPages: Math.ceil(total / pageSize), }, }; } async updateLastLogin(ctx: ServiceContext, userId: string): Promise { await this.ormRepo.update( { id: userId, tenantId: ctx.tenantId }, { lastLoginAt: new Date() }, ); } async updatePasswordHash( ctx: ServiceContext, userId: string, passwordHash: string, ): Promise { await this.ormRepo.update( { id: userId, tenantId: ctx.tenantId }, { passwordHash }, ); } // ============================================================================ // Additional Helper Methods (Not in interface, but useful) // ============================================================================ /** * Find users by status */ async findByStatus( ctx: ServiceContext, status: 'active' | 'inactive' | 'suspended', filters?: PaginationOptions, ): Promise> { const page = filters?.page || 1; const pageSize = filters?.pageSize || 20; const [data, total] = await this.ormRepo.findAndCount({ where: { tenantId: ctx.tenantId, status, }, skip: (page - 1) * pageSize, take: pageSize, order: { createdAt: 'DESC' }, }); return { data, meta: { page, pageSize, totalRecords: total, totalPages: Math.ceil(total / pageSize), }, }; } /** * Search users by name or email */ async search( ctx: ServiceContext, query: string, filters?: PaginationOptions, ): Promise> { const page = filters?.page || 1; const pageSize = filters?.pageSize || 20; const queryBuilder = this.ormRepo.createQueryBuilder('user'); queryBuilder.where('user.tenantId = :tenantId', { tenantId: ctx.tenantId }); queryBuilder.andWhere( '(user.fullName ILIKE :query OR user.email ILIKE :query)', { query: `%${query}%` }, ); queryBuilder.orderBy('user.fullName', 'ASC'); queryBuilder.skip((page - 1) * pageSize); queryBuilder.take(pageSize); const [data, total] = await queryBuilder.getManyAndCount(); return { data, meta: { page, pageSize, totalRecords: total, totalPages: Math.ceil(total / pageSize), }, }; } }