erp-suite/apps/shared-libs/core/examples/user.repository.example.ts

372 lines
9.4 KiB
TypeScript

/**
* 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<IUserRepository>('UserRepository');
* const user = await userRepo.findByEmail(ctx, 'user@example.com');
* ```
*/
@Injectable()
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(User)
private readonly ormRepo: Repository<User>,
) {}
// ============================================================================
// Core CRUD Operations (from IRepository<User>)
// ============================================================================
async findById(
ctx: ServiceContext,
id: string,
options?: QueryOptions,
): Promise<User | null> {
return this.ormRepo.findOne({
where: { id, tenantId: ctx.tenantId },
relations: options?.relations,
select: options?.select as any,
});
}
async findOne(
ctx: ServiceContext,
criteria: Partial<User>,
options?: QueryOptions,
): Promise<User | null> {
return this.ormRepo.findOne({
where: { ...criteria, tenantId: ctx.tenantId },
relations: options?.relations,
select: options?.select as any,
});
}
async findAll(
ctx: ServiceContext,
filters?: PaginationOptions & Partial<User>,
options?: QueryOptions,
): Promise<PaginatedResult<User>> {
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<User>,
options?: QueryOptions,
): Promise<User[]> {
return this.ormRepo.find({
where: { ...criteria, tenantId: ctx.tenantId },
relations: options?.relations,
select: options?.select as any,
});
}
async create(ctx: ServiceContext, data: Partial<User>): Promise<User> {
const user = this.ormRepo.create({
...data,
tenantId: ctx.tenantId,
});
return this.ormRepo.save(user);
}
async createMany(ctx: ServiceContext, data: Partial<User>[]): Promise<User[]> {
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<User>,
): Promise<User | null> {
await this.ormRepo.update(
{ id, tenantId: ctx.tenantId },
data,
);
return this.findById(ctx, id);
}
async updateMany(
ctx: ServiceContext,
criteria: Partial<User>,
data: Partial<User>,
): Promise<number> {
const result = await this.ormRepo.update(
{ ...criteria, tenantId: ctx.tenantId },
data,
);
return result.affected || 0;
}
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
const result = await this.ormRepo.softDelete({
id,
tenantId: ctx.tenantId,
});
return (result.affected || 0) > 0;
}
async hardDelete(ctx: ServiceContext, id: string): Promise<boolean> {
const result = await this.ormRepo.delete({
id,
tenantId: ctx.tenantId,
});
return (result.affected || 0) > 0;
}
async deleteMany(ctx: ServiceContext, criteria: Partial<User>): Promise<number> {
const result = await this.ormRepo.delete({
...criteria,
tenantId: ctx.tenantId,
});
return result.affected || 0;
}
async count(
ctx: ServiceContext,
criteria?: Partial<User>,
options?: QueryOptions,
): Promise<number> {
return this.ormRepo.count({
where: { ...criteria, tenantId: ctx.tenantId },
relations: options?.relations,
});
}
async exists(
ctx: ServiceContext,
id: string,
options?: QueryOptions,
): Promise<boolean> {
const count = await this.ormRepo.count({
where: { id, tenantId: ctx.tenantId },
relations: options?.relations,
});
return count > 0;
}
async query<R = unknown>(
ctx: ServiceContext,
sql: string,
params: unknown[],
): Promise<R[]> {
// Add tenant filtering to raw SQL
const tenantParam = ctx.tenantId;
return this.ormRepo.query(sql, [...params, tenantParam]);
}
async queryOne<R = unknown>(
ctx: ServiceContext,
sql: string,
params: unknown[],
): Promise<R | null> {
const results = await this.query<R>(ctx, sql, params);
return results[0] || null;
}
// ============================================================================
// User-Specific Operations (from IUserRepository)
// ============================================================================
async findByEmail(
ctx: ServiceContext,
email: string,
): Promise<User | null> {
return this.ormRepo.findOne({
where: {
email,
tenantId: ctx.tenantId,
},
});
}
async findByTenantId(
ctx: ServiceContext,
tenantId: string,
): Promise<User[]> {
// 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<PaginatedResult<User>> {
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<void> {
await this.ormRepo.update(
{ id: userId, tenantId: ctx.tenantId },
{ lastLoginAt: new Date() },
);
}
async updatePasswordHash(
ctx: ServiceContext,
userId: string,
passwordHash: string,
): Promise<void> {
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<PaginatedResult<User>> {
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<PaginatedResult<User>> {
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),
},
};
}
}