From 27e283e7244d1b014caa7f0408417fa8f07dab56 Mon Sep 17 00:00:00 2001
From: Lukas <lukas@lschaefer.xyz>
Date: Thu, 5 Sep 2024 10:12:46 -0400
Subject: [PATCH] fix(server): search suggestions include partner assets
 (#12269)

search suggestions now include partner assets

Co-authored-by: Alex <alex.tran1502@gmail.com>
---
 server/src/interfaces/metadata.interface.ts   | 10 +++++-----
 .../src/repositories/metadata.repository.ts   | 20 +++++++++----------
 server/src/services/search.service.spec.ts    |  4 ++--
 server/src/services/search.service.ts         | 15 +++++++-------
 .../repositories/partner.repository.mock.ts   |  2 +-
 5 files changed, 26 insertions(+), 25 deletions(-)

diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts
index 1f808cbd21..04e7b89d1e 100644
--- a/server/src/interfaces/metadata.interface.ts
+++ b/server/src/interfaces/metadata.interface.ts
@@ -53,9 +53,9 @@ export interface IMetadataRepository {
   readTags(path: string): Promise<ImmichTags | null>;
   writeTags(path: string, tags: Partial<Tags>): Promise<void>;
   extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
-  getCountries(userId: string): Promise<Array<string | null>>;
-  getStates(userId: string, country?: string): Promise<Array<string | null>>;
-  getCities(userId: string, country?: string, state?: string): Promise<Array<string | null>>;
-  getCameraMakes(userId: string, model?: string): Promise<Array<string | null>>;
-  getCameraModels(userId: string, make?: string): Promise<Array<string | null>>;
+  getCountries(userIds: string[]): Promise<Array<string | null>>;
+  getStates(userIds: string[], country?: string): Promise<Array<string | null>>;
+  getCities(userIds: string[], country?: string, state?: string): Promise<Array<string | null>>;
+  getCameraMakes(userIds: string[], model?: string): Promise<Array<string | null>>;
+  getCameraModels(userIds: string[], make?: string): Promise<Array<string | null>>;
 }
diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts
index 832cffbee6..abffc1b785 100644
--- a/server/src/repositories/metadata.repository.ts
+++ b/server/src/repositories/metadata.repository.ts
@@ -56,11 +56,11 @@ export class MetadataRepository implements IMetadataRepository {
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
-  async getCountries(userId: string): Promise<string[]> {
+  async getCountries(userIds: string[]): Promise<string[]> {
     const results = await this.exifRepository
       .createQueryBuilder('exif')
       .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId = :userId', { userId })
+      .where('asset.ownerId IN (:...userIds )', { userIds })
       .select('exif.country', 'country')
       .distinctOn(['exif.country'])
       .getRawMany<{ country: string }>();
@@ -69,11 +69,11 @@ export class MetadataRepository implements IMetadataRepository {
   }
 
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
-  async getStates(userId: string, country: string | undefined): Promise<string[]> {
+  async getStates(userIds: string[], country: string | undefined): Promise<string[]> {
     const query = this.exifRepository
       .createQueryBuilder('exif')
       .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId = :userId', { userId })
+      .where('asset.ownerId IN (:...userIds )', { userIds })
       .select('exif.state', 'state')
       .distinctOn(['exif.state']);
 
@@ -87,11 +87,11 @@ export class MetadataRepository implements IMetadataRepository {
   }
 
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] })
-  async getCities(userId: string, country: string | undefined, state: string | undefined): Promise<string[]> {
+  async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> {
     const query = this.exifRepository
       .createQueryBuilder('exif')
       .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId = :userId', { userId })
+      .where('asset.ownerId IN (:...userIds )', { userIds })
       .select('exif.city', 'city')
       .distinctOn(['exif.city']);
 
@@ -109,11 +109,11 @@ export class MetadataRepository implements IMetadataRepository {
   }
 
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
-  async getCameraMakes(userId: string, model: string | undefined): Promise<string[]> {
+  async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> {
     const query = this.exifRepository
       .createQueryBuilder('exif')
       .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId = :userId', { userId })
+      .where('asset.ownerId IN (:...userIds )', { userIds })
       .select('exif.make', 'make')
       .distinctOn(['exif.make']);
 
@@ -126,11 +126,11 @@ export class MetadataRepository implements IMetadataRepository {
   }
 
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
-  async getCameraModels(userId: string, make: string | undefined): Promise<string[]> {
+  async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> {
     const query = this.exifRepository
       .createQueryBuilder('exif')
       .leftJoin('exif.asset', 'asset')
-      .where('asset.ownerId = :userId', { userId })
+      .where('asset.ownerId IN (:...userIds )', { userIds })
       .select('exif.model', 'model')
       .distinctOn(['exif.model']);
 
diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts
index 89609d5d89..ded087b8b5 100644
--- a/server/src/services/search.service.spec.ts
+++ b/server/src/services/search.service.spec.ts
@@ -103,7 +103,7 @@ describe(SearchService.name, () => {
       await expect(
         sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
       ).resolves.toEqual(['USA', null]);
-      expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id);
+      expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
     });
 
     it('should return search suggestions (without null)', async () => {
@@ -111,7 +111,7 @@ describe(SearchService.name, () => {
       await expect(
         sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }),
       ).resolves.toEqual(['USA']);
-      expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id);
+      expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
     });
   });
 });
diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts
index 35fd29a2de..4c86d4ad75 100644
--- a/server/src/services/search.service.ts
+++ b/server/src/services/search.service.ts
@@ -121,26 +121,27 @@ export class SearchService {
   }
 
   async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) {
-    const results = await this.getSuggestions(auth.user.id, dto);
+    const userIds = await this.getUserIdsToSearch(auth);
+    const results = await this.getSuggestions(userIds, dto);
     return results.filter((result) => (dto.includeNull ? true : result !== null));
   }
 
-  private getSuggestions(userId: string, dto: SearchSuggestionRequestDto) {
+  private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) {
     switch (dto.type) {
       case SearchSuggestionType.COUNTRY: {
-        return this.metadataRepository.getCountries(userId);
+        return this.metadataRepository.getCountries(userIds);
       }
       case SearchSuggestionType.STATE: {
-        return this.metadataRepository.getStates(userId, dto.country);
+        return this.metadataRepository.getStates(userIds, dto.country);
       }
       case SearchSuggestionType.CITY: {
-        return this.metadataRepository.getCities(userId, dto.country, dto.state);
+        return this.metadataRepository.getCities(userIds, dto.country, dto.state);
       }
       case SearchSuggestionType.CAMERA_MAKE: {
-        return this.metadataRepository.getCameraMakes(userId, dto.model);
+        return this.metadataRepository.getCameraMakes(userIds, dto.model);
       }
       case SearchSuggestionType.CAMERA_MODEL: {
-        return this.metadataRepository.getCameraModels(userId, dto.make);
+        return this.metadataRepository.getCameraModels(userIds, dto.make);
       }
       default: {
         return [];
diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts
index e16bb6ffdc..ec1f141075 100644
--- a/server/test/repositories/partner.repository.mock.ts
+++ b/server/test/repositories/partner.repository.mock.ts
@@ -5,7 +5,7 @@ export const newPartnerRepositoryMock = (): Mocked<IPartnerRepository> => {
   return {
     create: vitest.fn(),
     remove: vitest.fn(),
-    getAll: vitest.fn(),
+    getAll: vitest.fn().mockResolvedValue([]),
     get: vitest.fn(),
     update: vitest.fn(),
   };