import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { EntityManager, Repository } from 'typeorm'; import { UserEntity, UserRole } from '../../database/entities/user.entity'; import { ApiError } from '../../common/errors/api-error'; import { ERROR_CODES } from '../../common/errors/error-codes'; type UserRepoLike = Repository | EntityManager; @Injectable() export class UsersService { constructor(@InjectRepository(UserEntity) private readonly repo: Repository) {} countAdmins(manager?: EntityManager): Promise { return this.getRepo(manager).count({ where: { role: 'admin' } }); } findById(id: string, manager?: EntityManager): Promise { return this.getRepo(manager).findOne({ where: { id } }); } listAll(): Promise { return this.repo.find({ order: { createdAt: 'ASC' } }); } /** * Verify, inside an open transaction, that demoting/deleting the given user * would not leave the system with zero admins. Throws ApiError(LAST_ADMIN) * if so. Must be invoked from a `dataSource.transaction(...)` callback. */ async enforceLastAdminInvariant(manager: EntityManager, targetId: string, nextRole?: UserRole): Promise { const repo = manager.getRepository(UserEntity); const target = await repo.findOne({ where: { id: targetId } }); if (!target) throw ApiError.notFound('User not found'); const wouldDemoteOrDelete = target.role === 'admin' && (nextRole === undefined || nextRole !== 'admin'); if (!wouldDemoteOrDelete) return; const adminCount = await repo.count({ where: { role: 'admin' } }); if (adminCount <= 1) { throw ApiError.conflict(ERROR_CODES.LAST_ADMIN, 'Cannot delete or demote the last admin'); } } private getRepo(manager?: EntityManager): Repository { return manager ? manager.getRepository(UserEntity) : this.repo; } }