import { AssetFaceId, IPersonRepository, PersonSearchOptions, UpdateFacesData } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { AssetEntity, AssetFaceEntity, PersonEntity } from '../entities'; export class PersonRepository implements IPersonRepository { constructor( @InjectRepository(AssetEntity) private assetRepository: Repository, @InjectRepository(PersonEntity) private personRepository: Repository, @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, ) {} /** * Before reassigning faces, delete potential key violations */ async prepareReassignFaces({ oldPersonId, newPersonId }: UpdateFacesData): Promise { const results = await this.assetFaceRepository .createQueryBuilder('face') .select('face."assetId"') .where(`face."personId" IN (:...ids)`, { ids: [oldPersonId, newPersonId] }) .groupBy('face."assetId"') .having('COUNT(face."personId") > 1') .getRawMany(); const assetIds = results.map(({ assetId }) => assetId); await this.assetFaceRepository.delete({ personId: oldPersonId, assetId: In(assetIds) }); return assetIds; } async reassignFaces({ oldPersonId, newPersonId }: UpdateFacesData): Promise { const result = await this.assetFaceRepository .createQueryBuilder() .update() .set({ personId: newPersonId }) .where({ personId: oldPersonId }) .execute(); return result.affected ?? 0; } delete(entity: PersonEntity): Promise { return this.personRepository.remove(entity); } async deleteAll(): Promise { const people = await this.personRepository.find(); await this.personRepository.remove(people); return people.length; } getAllFaces(): Promise { return this.assetFaceRepository.find({ relations: { asset: true }, withDeleted: true }); } getAll(): Promise { return this.personRepository.find(); } getAllWithoutThumbnail(): Promise { return this.personRepository.findBy({ thumbnailPath: '' }); } getAllForUser(userId: string, options?: PersonSearchOptions): Promise { const queryBuilder = this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') .where('person.ownerId = :userId', { userId }) .innerJoin('face.asset', 'asset') .orderBy('person.isHidden', 'ASC') .addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC') .addOrderBy('COUNT(face.assetId)', 'DESC') .addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST') .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 }) .groupBy('person.id') .limit(500); if (!options?.withHidden) { queryBuilder.andWhere('person.isHidden = false'); } return queryBuilder.getMany(); } getAllWithoutFaces(): Promise { return this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') .having('COUNT(face.assetId) = 0') .groupBy('person.id') .withDeleted() .getMany(); } getById(personId: string): Promise { return this.personRepository.findOne({ where: { id: personId } }); } getByName(userId: string, personName: string): Promise { return this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') .where('person.ownerId = :userId', { userId }) .andWhere('LOWER(person.name) LIKE :name', { name: `${personName.toLowerCase()}%` }) .limit(20) .getMany(); } getAssets(personId: string): Promise { return this.assetRepository.find({ where: { faces: { personId, }, isVisible: true, isArchived: false, }, relations: { faces: { person: true, }, exifInfo: true, }, order: { fileCreatedAt: 'desc', }, // TODO: remove after either (1) pagination or (2) time bucket is implemented for this query take: 1000, }); } create(entity: Partial): Promise { return this.personRepository.save(entity); } createFace(entity: Partial): Promise { return this.assetFaceRepository.save(entity); } async update(entity: Partial): Promise { const { id } = await this.personRepository.save(entity); return this.personRepository.findOneByOrFail({ id }); } async getFacesByIds(ids: AssetFaceId[]): Promise { return this.assetFaceRepository.find({ where: ids, relations: { asset: true }, withDeleted: true }); } async getRandomFace(personId: string): Promise { return this.assetFaceRepository.findOneBy({ personId }); } }