import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryResponse, } from 'src/interfaces/user.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { IsNull, Not, Repository } from 'typeorm'; @Instrumentation() @Injectable() export class UserRepository implements IUserRepository { constructor( @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, @InjectRepository(UserEntity) private userRepository: Repository<UserEntity>, ) {} async get(userId: string, options: UserFindOptions): Promise<UserEntity | null> { options = options || {}; return this.userRepository.findOne({ where: { id: userId }, withDeleted: options.withDeleted, }); } @GenerateSql() async getAdmin(): Promise<UserEntity | null> { return this.userRepository.findOne({ where: { isAdmin: true } }); } @GenerateSql() async hasAdmin(): Promise<boolean> { return this.userRepository.exists({ where: { isAdmin: true } }); } @GenerateSql({ params: [DummyValue.EMAIL] }) async getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | null> { const builder = this.userRepository.createQueryBuilder('user').where({ email }); if (withPassword) { builder.addSelect('user.password'); } return builder.getOne(); } @GenerateSql({ params: [DummyValue.STRING] }) async getByStorageLabel(storageLabel: string): Promise<UserEntity | null> { return this.userRepository.findOne({ where: { storageLabel } }); } @GenerateSql({ params: [DummyValue.STRING] }) async getByOAuthId(oauthId: string): Promise<UserEntity | null> { return this.userRepository.findOne({ where: { oauthId } }); } async getDeletedUsers(): Promise<UserEntity[]> { return this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } }); } async getList({ withDeleted }: UserListFilter = {}): Promise<UserEntity[]> { return this.userRepository.find({ withDeleted, order: { createdAt: 'DESC', }, }); } create(user: Partial<UserEntity>): Promise<UserEntity> { return this.save(user); } // TODO change to (user: Partial<UserEntity>) update(id: string, user: Partial<UserEntity>): Promise<UserEntity> { return this.save({ ...user, id }); } async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> { return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user); } @GenerateSql() async getUserStats(): Promise<UserStatsQueryResponse[]> { const stats = await this.userRepository .createQueryBuilder('users') .select('users.id', 'userId') .addSelect('users.name', 'userName') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos') .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage') .addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes') .leftJoin('users.assets', 'assets') .leftJoin('assets.exifInfo', 'exif') .groupBy('users.id') .orderBy('users.createdAt', 'ASC') .getRawMany(); for (const stat of stats) { stat.photos = Number(stat.photos); stat.videos = Number(stat.videos); stat.usage = Number(stat.usage); stat.quotaSizeInBytes = stat.quotaSizeInBytes; } return stats; } @GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] }) async updateUsage(id: string, delta: number): Promise<void> { await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta); } @GenerateSql({ params: [DummyValue.UUID] }) async syncUsage(id?: string) { // we can't use parameters with getQuery, hence the template string const subQuery = this.assetRepository .createQueryBuilder('assets') .select('COALESCE(SUM(exif."fileSizeInByte"), 0)') .leftJoin('assets.exifInfo', 'exif') .where('assets.ownerId = users.id') .andWhere(`assets.libraryId IS NULL`) .withDeleted(); const query = this.userRepository .createQueryBuilder('users') .leftJoin('users.assets', 'assets') .update() .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }); if (id) { query.where('users.id = :id', { id }); } await query.execute(); } private async save(user: Partial<UserEntity>) { const { id } = await this.userRepository.save(user); return this.userRepository.findOneOrFail({ where: { id }, withDeleted: true }); } }