mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	fix(server): regenerate missing person thumbnails (#3970)
* Regenerate missing person thumbnails * Check for empty string instead of zero length * Remember asset used as person face * Define entity relation between person and asset via faceAssetId * Typo * Fix entity relation * Tests * Tests * Fix code formatting * Fix import path * Fix migration * format * Fix entity and migration * Linting * Remove unneeded cast * Conventions * Simplify queries * Simplify queries * Remove unneeded typings from entity * Remove unneeded cast --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									b8777d7739
								
							
						
					
					
						commit
						3432b4625f
					
				@ -296,6 +296,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
        colorspace: Colorspace.P3,
 | 
					        colorspace: Colorspace.P3,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(personMock.update).toHaveBeenCalledWith({
 | 
					      expect(personMock.update).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        faceAssetId: 'asset-1',
 | 
				
			||||||
        id: 'person-1',
 | 
					        id: 'person-1',
 | 
				
			||||||
        thumbnailPath: 'upload/thumbs/user-id/person-1.jpeg',
 | 
					        thumbnailPath: 'upload/thumbs/user-id/person-1.jpeg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
				
			|||||||
@ -171,7 +171,7 @@ export class FacialRecognitionService {
 | 
				
			|||||||
      quality: thumbnail.quality,
 | 
					      quality: thumbnail.quality,
 | 
				
			||||||
    } as const;
 | 
					    } as const;
 | 
				
			||||||
    await this.mediaRepository.resize(croppedOutput, output, thumbnailOptions);
 | 
					    await this.mediaRepository.resize(croppedOutput, output, thumbnailOptions);
 | 
				
			||||||
    await this.personRepository.update({ id: personId, thumbnailPath: output });
 | 
					    await this.personRepository.update({ id: personId, thumbnailPath: output, faceAssetId: data.assetId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -28,6 +28,7 @@ export enum JobName {
 | 
				
			|||||||
  GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail',
 | 
					  GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail',
 | 
				
			||||||
  GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail',
 | 
					  GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail',
 | 
				
			||||||
  GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail',
 | 
					  GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail',
 | 
				
			||||||
 | 
					  GENERATE_FACE_THUMBNAIL = 'generate-face-thumbnail',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // metadata
 | 
					  // metadata
 | 
				
			||||||
  QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
 | 
					  QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
 | 
				
			||||||
@ -50,7 +51,6 @@ export enum JobName {
 | 
				
			|||||||
  // facial recognition
 | 
					  // facial recognition
 | 
				
			||||||
  QUEUE_RECOGNIZE_FACES = 'queue-recognize-faces',
 | 
					  QUEUE_RECOGNIZE_FACES = 'queue-recognize-faces',
 | 
				
			||||||
  RECOGNIZE_FACES = 'recognize-faces',
 | 
					  RECOGNIZE_FACES = 'recognize-faces',
 | 
				
			||||||
  GENERATE_FACE_THUMBNAIL = 'generate-face-thumbnail',
 | 
					 | 
				
			||||||
  PERSON_CLEANUP = 'person-cleanup',
 | 
					  PERSON_CLEANUP = 'person-cleanup',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // cleanup
 | 
					  // cleanup
 | 
				
			||||||
@ -97,6 +97,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
 | 
				
			|||||||
  [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
  [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
  [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
 | 
					  [JobName.GENERATE_FACE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // metadata
 | 
					  // metadata
 | 
				
			||||||
  [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
 | 
					  [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
 | 
				
			||||||
@ -115,7 +116,6 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
 | 
				
			|||||||
  // facial recognition
 | 
					  // facial recognition
 | 
				
			||||||
  [JobName.QUEUE_RECOGNIZE_FACES]: QueueName.RECOGNIZE_FACES,
 | 
					  [JobName.QUEUE_RECOGNIZE_FACES]: QueueName.RECOGNIZE_FACES,
 | 
				
			||||||
  [JobName.RECOGNIZE_FACES]: QueueName.RECOGNIZE_FACES,
 | 
					  [JobName.RECOGNIZE_FACES]: QueueName.RECOGNIZE_FACES,
 | 
				
			||||||
  [JobName.GENERATE_FACE_THUMBNAIL]: QueueName.RECOGNIZE_FACES,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // clip
 | 
					  // clip
 | 
				
			||||||
  [JobName.QUEUE_ENCODE_CLIP]: QueueName.CLIP_ENCODING,
 | 
					  [JobName.QUEUE_ENCODE_CLIP]: QueueName.CLIP_ENCODING,
 | 
				
			||||||
 | 
				
			|||||||
@ -9,15 +9,19 @@ import {
 | 
				
			|||||||
} from '@app/infra/entities';
 | 
					} from '@app/infra/entities';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  assetStub,
 | 
					  assetStub,
 | 
				
			||||||
 | 
					  faceStub,
 | 
				
			||||||
  newAssetRepositoryMock,
 | 
					  newAssetRepositoryMock,
 | 
				
			||||||
  newJobRepositoryMock,
 | 
					  newJobRepositoryMock,
 | 
				
			||||||
  newMediaRepositoryMock,
 | 
					  newMediaRepositoryMock,
 | 
				
			||||||
 | 
					  newPersonRepositoryMock,
 | 
				
			||||||
  newStorageRepositoryMock,
 | 
					  newStorageRepositoryMock,
 | 
				
			||||||
  newSystemConfigRepositoryMock,
 | 
					  newSystemConfigRepositoryMock,
 | 
				
			||||||
 | 
					  personStub,
 | 
				
			||||||
  probeStub,
 | 
					  probeStub,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
import { IJobRepository, JobName } from '../job';
 | 
					import { IJobRepository, JobName } from '../job';
 | 
				
			||||||
 | 
					import { IPersonRepository } from '../person';
 | 
				
			||||||
import { IStorageRepository } from '../storage';
 | 
					import { IStorageRepository } from '../storage';
 | 
				
			||||||
import { ISystemConfigRepository } from '../system-config';
 | 
					import { ISystemConfigRepository } from '../system-config';
 | 
				
			||||||
import { IMediaRepository } from './media.repository';
 | 
					import { IMediaRepository } from './media.repository';
 | 
				
			||||||
@ -29,6 +33,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
					  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let mediaMock: jest.Mocked<IMediaRepository>;
 | 
					  let mediaMock: jest.Mocked<IMediaRepository>;
 | 
				
			||||||
 | 
					  let personMock: jest.Mocked<IPersonRepository>;
 | 
				
			||||||
  let storageMock: jest.Mocked<IStorageRepository>;
 | 
					  let storageMock: jest.Mocked<IStorageRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
@ -36,9 +41,10 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    configMock = newSystemConfigRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    mediaMock = newMediaRepositoryMock();
 | 
					    mediaMock = newMediaRepositoryMock();
 | 
				
			||||||
 | 
					    personMock = newPersonRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new MediaService(assetMock, jobMock, mediaMock, storageMock, configMock);
 | 
					    sut = new MediaService(assetMock, personMock, jobMock, mediaMock, storageMock, configMock);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should be defined', () => {
 | 
					  it('should be defined', () => {
 | 
				
			||||||
@ -51,6 +57,8 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        items: [assetStub.image],
 | 
					        items: [assetStub.image],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      personMock.getAll.mockResolvedValue([personStub.newThumbnail]);
 | 
				
			||||||
 | 
					      personMock.getFaceById.mockResolvedValue(faceStub.face1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleQueueGenerateThumbnails({ force: true });
 | 
					      await sut.handleQueueGenerateThumbnails({ force: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -60,6 +68,57 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        name: JobName.GENERATE_JPEG_THUMBNAIL,
 | 
					        name: JobName.GENERATE_JPEG_THUMBNAIL,
 | 
				
			||||||
        data: { id: assetStub.image.id },
 | 
					        data: { id: assetStub.image.id },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(personMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(personMock.getAllWithoutThumbnail).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          imageWidth: faceStub.face1.imageWidth,
 | 
				
			||||||
 | 
					          imageHeight: faceStub.face1.imageHeight,
 | 
				
			||||||
 | 
					          boundingBox: {
 | 
				
			||||||
 | 
					            x1: faceStub.face1.boundingBoxX1,
 | 
				
			||||||
 | 
					            x2: faceStub.face1.boundingBoxX2,
 | 
				
			||||||
 | 
					            y1: faceStub.face1.boundingBoxY1,
 | 
				
			||||||
 | 
					            y2: faceStub.face1.boundingBoxY2,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          assetId: faceStub.face1.assetId,
 | 
				
			||||||
 | 
					          personId: personStub.newThumbnail.id,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should queue all people with missing thumbnail path', async () => {
 | 
				
			||||||
 | 
					      assetMock.getWithout.mockResolvedValue({
 | 
				
			||||||
 | 
					        items: [assetStub.image],
 | 
				
			||||||
 | 
					        hasNextPage: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      personMock.getAllWithoutThumbnail.mockResolvedValue([personStub.noThumbnail]);
 | 
				
			||||||
 | 
					      personMock.getRandomFace.mockResolvedValue(faceStub.face1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(assetMock.getAll).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(personMock.getAll).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(personMock.getAllWithoutThumbnail).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(personMock.getRandomFace).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          imageWidth: faceStub.face1.imageWidth,
 | 
				
			||||||
 | 
					          imageHeight: faceStub.face1.imageHeight,
 | 
				
			||||||
 | 
					          boundingBox: {
 | 
				
			||||||
 | 
					            x1: faceStub.face1.boundingBoxX1,
 | 
				
			||||||
 | 
					            x2: faceStub.face1.boundingBoxX2,
 | 
				
			||||||
 | 
					            y1: faceStub.face1.boundingBoxY1,
 | 
				
			||||||
 | 
					            y2: faceStub.face1.boundingBoxY2,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          assetId: faceStub.face1.assetId,
 | 
				
			||||||
 | 
					          personId: personStub.newThumbnail.id,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should queue all assets with missing resize path', async () => {
 | 
					    it('should queue all assets with missing resize path', async () => {
 | 
				
			||||||
@ -67,6 +126,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        items: [assetStub.noResizePath],
 | 
					        items: [assetStub.noResizePath],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      personMock.getAllWithoutThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
					      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -76,6 +136,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        name: JobName.GENERATE_JPEG_THUMBNAIL,
 | 
					        name: JobName.GENERATE_JPEG_THUMBNAIL,
 | 
				
			||||||
        data: { id: assetStub.image.id },
 | 
					        data: { id: assetStub.image.id },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(personMock.getAll).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(personMock.getAllWithoutThumbnail).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should queue all assets with missing webp path', async () => {
 | 
					    it('should queue all assets with missing webp path', async () => {
 | 
				
			||||||
@ -83,6 +146,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        items: [assetStub.noWebpPath],
 | 
					        items: [assetStub.noWebpPath],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      personMock.getAllWithoutThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
					      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -92,6 +156,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        name: JobName.GENERATE_WEBP_THUMBNAIL,
 | 
					        name: JobName.GENERATE_WEBP_THUMBNAIL,
 | 
				
			||||||
        data: { id: assetStub.image.id },
 | 
					        data: { id: assetStub.image.id },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(personMock.getAll).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(personMock.getAllWithoutThumbnail).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should queue all assets with missing thumbhash', async () => {
 | 
					    it('should queue all assets with missing thumbhash', async () => {
 | 
				
			||||||
@ -99,6 +166,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        items: [assetStub.noThumbhash],
 | 
					        items: [assetStub.noThumbhash],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      personMock.getAllWithoutThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
					      await sut.handleQueueGenerateThumbnails({ force: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -108,6 +176,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        name: JobName.GENERATE_THUMBHASH_THUMBNAIL,
 | 
					        name: JobName.GENERATE_THUMBHASH_THUMBNAIL,
 | 
				
			||||||
        data: { id: assetStub.image.id },
 | 
					        data: { id: assetStub.image.id },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(personMock.getAll).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(personMock.getAllWithoutThumbnail).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -245,6 +316,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
        items: [assetStub.video],
 | 
					        items: [assetStub.video],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      personMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleQueueVideoConversion({ force: true });
 | 
					      await sut.handleQueueVideoConversion({ force: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,11 +4,13 @@ import { join } from 'path';
 | 
				
			|||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
import { usePagination } from '../domain.util';
 | 
					import { usePagination } from '../domain.util';
 | 
				
			||||||
import { IBaseJob, IEntityJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
 | 
					import { IBaseJob, IEntityJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
 | 
				
			||||||
 | 
					import { IPersonRepository } from '../person';
 | 
				
			||||||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 | 
					import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 | 
				
			||||||
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
 | 
					import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
 | 
				
			||||||
import { SystemConfigCore } from '../system-config/system-config.core';
 | 
					import { SystemConfigCore } from '../system-config/system-config.core';
 | 
				
			||||||
import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository';
 | 
					import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository';
 | 
				
			||||||
import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util';
 | 
					import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class MediaService {
 | 
					export class MediaService {
 | 
				
			||||||
  private logger = new Logger(MediaService.name);
 | 
					  private logger = new Logger(MediaService.name);
 | 
				
			||||||
@ -17,6 +19,7 @@ export class MediaService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
 | 
					    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
@ -49,6 +52,32 @@ export class MediaService {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const people = force ? await this.personRepository.getAll() : await this.personRepository.getAllWithoutThumbnail();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const person of people) {
 | 
				
			||||||
 | 
					      // use stored asset for generating thumbnail or pick a random one if not present
 | 
				
			||||||
 | 
					      const face = person.faceAssetId
 | 
				
			||||||
 | 
					        ? await this.personRepository.getFaceById({ personId: person.id, assetId: person.faceAssetId })
 | 
				
			||||||
 | 
					        : await this.personRepository.getRandomFace(person.id);
 | 
				
			||||||
 | 
					      if (face) {
 | 
				
			||||||
 | 
					        await this.jobRepository.queue({
 | 
				
			||||||
 | 
					          name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            imageWidth: face.imageWidth,
 | 
				
			||||||
 | 
					            imageHeight: face.imageHeight,
 | 
				
			||||||
 | 
					            boundingBox: {
 | 
				
			||||||
 | 
					              x1: face.boundingBoxX1,
 | 
				
			||||||
 | 
					              x2: face.boundingBoxX2,
 | 
				
			||||||
 | 
					              y1: face.boundingBoxY1,
 | 
				
			||||||
 | 
					              y2: face.boundingBoxY2,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            assetId: face.assetId,
 | 
				
			||||||
 | 
					            personId: person.id,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,9 @@ export interface UpdateFacesData {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPersonRepository {
 | 
					export interface IPersonRepository {
 | 
				
			||||||
  getAll(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
 | 
					  getAll(): Promise<PersonEntity[]>;
 | 
				
			||||||
 | 
					  getAllWithoutThumbnail(): Promise<PersonEntity[]>;
 | 
				
			||||||
 | 
					  getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
 | 
				
			||||||
  getAllWithoutFaces(): Promise<PersonEntity[]>;
 | 
					  getAllWithoutFaces(): Promise<PersonEntity[]>;
 | 
				
			||||||
  getById(userId: string, personId: string): Promise<PersonEntity | null>;
 | 
					  getById(userId: string, personId: string): Promise<PersonEntity | null>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,4 +29,5 @@ export interface IPersonRepository {
 | 
				
			|||||||
  deleteAll(): Promise<number>;
 | 
					  deleteAll(): Promise<number>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getFaceById(payload: AssetFaceId): Promise<AssetFaceEntity | null>;
 | 
					  getFaceById(payload: AssetFaceId): Promise<AssetFaceEntity | null>;
 | 
				
			||||||
 | 
					  getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -42,25 +42,31 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('getAll', () => {
 | 
					  describe('getAll', () => {
 | 
				
			||||||
    it('should get all people with thumbnails', async () => {
 | 
					    it('should get all people with thumbnails', async () => {
 | 
				
			||||||
      personMock.getAll.mockResolvedValue([personStub.withName, personStub.noThumbnail]);
 | 
					      personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.noThumbnail]);
 | 
				
			||||||
      await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({
 | 
					      await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({
 | 
				
			||||||
        total: 1,
 | 
					        total: 1,
 | 
				
			||||||
        visible: 1,
 | 
					        visible: 1,
 | 
				
			||||||
        people: [responseDto],
 | 
					        people: [responseDto],
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: false });
 | 
					      expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, {
 | 
				
			||||||
 | 
					        minimumFaceCount: 1,
 | 
				
			||||||
 | 
					        withHidden: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    it('should get all visible people with thumbnails', async () => {
 | 
					    it('should get all visible people with thumbnails', async () => {
 | 
				
			||||||
      personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]);
 | 
					      personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
 | 
				
			||||||
      await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({
 | 
					      await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({
 | 
				
			||||||
        total: 2,
 | 
					        total: 2,
 | 
				
			||||||
        visible: 1,
 | 
					        visible: 1,
 | 
				
			||||||
        people: [responseDto],
 | 
					        people: [responseDto],
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: false });
 | 
					      expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, {
 | 
				
			||||||
 | 
					        minimumFaceCount: 1,
 | 
				
			||||||
 | 
					        withHidden: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    it('should get all hidden and visible people with thumbnails', async () => {
 | 
					    it('should get all hidden and visible people with thumbnails', async () => {
 | 
				
			||||||
      personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]);
 | 
					      personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
 | 
				
			||||||
      await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({
 | 
					      await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({
 | 
				
			||||||
        total: 2,
 | 
					        total: 2,
 | 
				
			||||||
        visible: 1,
 | 
					        visible: 1,
 | 
				
			||||||
@ -75,7 +81,10 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: true });
 | 
					      expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, {
 | 
				
			||||||
 | 
					        minimumFaceCount: 1,
 | 
				
			||||||
 | 
					        withHidden: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ export class PersonService {
 | 
				
			|||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
 | 
					  async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
 | 
				
			||||||
    const people = await this.repository.getAll(authUser.id, {
 | 
					    const people = await this.repository.getAllForUser(authUser.id, {
 | 
				
			||||||
      minimumFaceCount: 1,
 | 
					      minimumFaceCount: 1,
 | 
				
			||||||
      withHidden: dto.withHidden || false,
 | 
					      withHidden: dto.withHidden || false,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  UpdateDateColumn,
 | 
					  UpdateDateColumn,
 | 
				
			||||||
} from 'typeorm';
 | 
					} from 'typeorm';
 | 
				
			||||||
import { AssetFaceEntity } from './asset-face.entity';
 | 
					import { AssetFaceEntity } from './asset-face.entity';
 | 
				
			||||||
 | 
					import { AssetEntity } from './asset.entity';
 | 
				
			||||||
import { UserEntity } from './user.entity';
 | 
					import { UserEntity } from './user.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('person')
 | 
					@Entity('person')
 | 
				
			||||||
@ -36,6 +37,12 @@ export class PersonEntity {
 | 
				
			|||||||
  @Column({ default: '' })
 | 
					  @Column({ default: '' })
 | 
				
			||||||
  thumbnailPath!: string;
 | 
					  thumbnailPath!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  faceAssetId!: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ManyToOne(() => AssetEntity, { onDelete: 'SET NULL', nullable: true })
 | 
				
			||||||
 | 
					  faceAsset!: AssetEntity | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person)
 | 
					  @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person)
 | 
				
			||||||
  faces!: AssetFaceEntity[];
 | 
					  faces!: AssetFaceEntity[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { MigrationInterface, QueryRunner } from "typeorm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AddPersonFaceAssetId1693833336881 implements MigrationInterface {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					    await queryRunner.query(`ALTER TABLE "person" ADD "faceAssetId" uuid`);
 | 
				
			||||||
 | 
					    await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					    await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`);
 | 
				
			||||||
 | 
					    await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "faceAssetId"`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -50,7 +50,15 @@ export class PersonRepository implements IPersonRepository {
 | 
				
			|||||||
    return people.length;
 | 
					    return people.length;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAll(userId: string, options?: PersonSearchOptions): Promise<PersonEntity[]> {
 | 
					  getAll(): Promise<PersonEntity[]> {
 | 
				
			||||||
 | 
					    return this.personRepository.find();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAllWithoutThumbnail(): Promise<PersonEntity[]> {
 | 
				
			||||||
 | 
					    return this.personRepository.findBy({ thumbnailPath: '' });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAllForUser(userId: string, options?: PersonSearchOptions): Promise<PersonEntity[]> {
 | 
				
			||||||
    const queryBuilder = this.personRepository
 | 
					    const queryBuilder = this.personRepository
 | 
				
			||||||
      .createQueryBuilder('person')
 | 
					      .createQueryBuilder('person')
 | 
				
			||||||
      .leftJoin('person.faces', 'face')
 | 
					      .leftJoin('person.faces', 'face')
 | 
				
			||||||
@ -118,4 +126,8 @@ export class PersonRepository implements IPersonRepository {
 | 
				
			|||||||
  async getFaceById({ personId, assetId }: AssetFaceId): Promise<AssetFaceEntity | null> {
 | 
					  async getFaceById({ personId, assetId }: AssetFaceId): Promise<AssetFaceEntity | null> {
 | 
				
			||||||
    return this.assetFaceRepository.findOneBy({ assetId, personId });
 | 
					    return this.assetFaceRepository.findOneBy({ assetId, personId });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
 | 
				
			||||||
 | 
					    return this.assetFaceRepository.findOneBy({ personId });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								server/test/fixtures/person.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								server/test/fixtures/person.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,5 @@
 | 
				
			|||||||
import { PersonEntity } from '@app/infra/entities';
 | 
					import { PersonEntity } from '@app/infra/entities';
 | 
				
			||||||
 | 
					import { assetStub } from '@test/fixtures/asset.stub';
 | 
				
			||||||
import { userStub } from './user.stub';
 | 
					import { userStub } from './user.stub';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const personStub = {
 | 
					export const personStub = {
 | 
				
			||||||
@ -12,6 +13,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  hidden: Object.freeze<PersonEntity>({
 | 
					  hidden: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -24,6 +27,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: true,
 | 
					    isHidden: true,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  withName: Object.freeze<PersonEntity>({
 | 
					  withName: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -36,6 +41,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  noBirthDate: Object.freeze<PersonEntity>({
 | 
					  noBirthDate: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -48,6 +55,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  withBirthDate: Object.freeze<PersonEntity>({
 | 
					  withBirthDate: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -60,6 +69,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: new Date('1976-06-30'),
 | 
					    birthDate: new Date('1976-06-30'),
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  noThumbnail: Object.freeze<PersonEntity>({
 | 
					  noThumbnail: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -72,6 +83,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '',
 | 
					    thumbnailPath: '',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  newThumbnail: Object.freeze<PersonEntity>({
 | 
					  newThumbnail: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -84,6 +97,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/new/path/to/thumbnail.jpg',
 | 
					    thumbnailPath: '/new/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: assetStub.image.id,
 | 
				
			||||||
 | 
					    faceAsset: assetStub.image,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  primaryPerson: Object.freeze<PersonEntity>({
 | 
					  primaryPerson: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -96,6 +111,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail',
 | 
					    thumbnailPath: '/path/to/thumbnail',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  mergePerson: Object.freeze<PersonEntity>({
 | 
					  mergePerson: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -108,6 +125,8 @@ export const personStub = {
 | 
				
			|||||||
    birthDate: null,
 | 
					    birthDate: null,
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail',
 | 
					    thumbnailPath: '/path/to/thumbnail',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    faceAssetId: null,
 | 
				
			||||||
 | 
					    faceAsset: null,
 | 
				
			||||||
    isHidden: false,
 | 
					    isHidden: false,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
 | 
				
			|||||||
  return {
 | 
					  return {
 | 
				
			||||||
    getById: jest.fn(),
 | 
					    getById: jest.fn(),
 | 
				
			||||||
    getAll: jest.fn(),
 | 
					    getAll: jest.fn(),
 | 
				
			||||||
 | 
					    getAllWithoutThumbnail: jest.fn(),
 | 
				
			||||||
 | 
					    getAllForUser: jest.fn(),
 | 
				
			||||||
    getAssets: jest.fn(),
 | 
					    getAssets: jest.fn(),
 | 
				
			||||||
    getAllWithoutFaces: jest.fn(),
 | 
					    getAllWithoutFaces: jest.fn(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,6 +15,7 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
 | 
				
			|||||||
    delete: jest.fn(),
 | 
					    delete: jest.fn(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getFaceById: jest.fn(),
 | 
					    getFaceById: jest.fn(),
 | 
				
			||||||
 | 
					    getRandomFace: jest.fn(),
 | 
				
			||||||
    prepareReassignFaces: jest.fn(),
 | 
					    prepareReassignFaces: jest.fn(),
 | 
				
			||||||
    reassignFaces: jest.fn(),
 | 
					    reassignFaces: jest.fn(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user