372 lines
9.4 KiB
TypeScript
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),
|
|
},
|
|
};
|
|
}
|
|
}
|