From 7335a44bd07c1b02a79bfbfee92605c4b40ea12f Mon Sep 17 00:00:00 2001 From: Torbjorn Date: Sun, 11 Feb 2024 21:52:40 +0100 Subject: [PATCH 01/10] gitignore: VIM swap files old-school :-D --- server/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/server/.gitignore b/server/.gitignore index 4a66f04b4fcaf..c253379936623 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -28,6 +28,7 @@ www/ *.launch .settings/ *.sublime-workspace +*.swp # IDE - VSCode .vscode/* From bee52cc8824baf1dce16d4342f11871203fd3311 Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Mon, 12 Feb 2024 00:40:06 +0100 Subject: [PATCH 02/10] feat: expose partner shared persons add the withPartners flag to /person endpoint, should allow requesting person objects shared by the partner in a similar way that the assets endpoint is doing feature request #6339 #5089 #5457 #7038 --- open-api/immich-openapi-specs.json | 10 +++- server/src/domain/person/person.dto.ts | 5 ++ .../src/domain/person/person.service.spec.ts | 53 ++++++++++++++++--- server/src/domain/person/person.service.ts | 15 +++++- .../domain/repositories/access.repository.ts | 1 + .../domain/repositories/person.repository.ts | 5 +- server/test/fixtures/person.stub.ts | 14 +++++ .../repositories/access.repository.mock.ts | 1 + .../repositories/person.repository.mock.ts | 2 +- 9 files changed, 94 insertions(+), 12 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6870e140caa66..dc23275363a19 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4005,6 +4005,14 @@ "default": false, "type": "boolean" } + }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } } ], "responses": { @@ -10514,4 +10522,4 @@ } } } -} \ No newline at end of file +} diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index 360a9b2348ef3..ef27ff0343fa9 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -63,6 +63,11 @@ export class PersonSearchDto { @IsBoolean() @Transform(toBoolean) withHidden?: boolean = false; + + @Optional() + @IsBoolean() + @Transform(toBoolean) + withPartners?: boolean = false; } export class PersonResponseDto { diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 5da8666016080..d91232a7f1858 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -12,10 +12,12 @@ import { newMachineLearningRepositoryMock, newMediaRepositoryMock, newMoveRepositoryMock, + newPartnerRepositoryMock, newPersonRepositoryMock, newSearchRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, + partnerStub, personStub, } from '@test'; import { IsNull } from 'typeorm'; @@ -30,6 +32,7 @@ import { IMachineLearningRepository, IMediaRepository, IMoveRepository, + IPartnerRepository, IPersonRepository, ISearchRepository, IStorageRepository, @@ -74,6 +77,7 @@ describe(PersonService.name, () => { let machineLearningMock: jest.Mocked; let mediaMock: jest.Mocked; let moveMock: jest.Mocked; + let partnerMock: jest.Mocked; let personMock: jest.Mocked; let storageMock: jest.Mocked; let searchMock: jest.Mocked; @@ -88,6 +92,7 @@ describe(PersonService.name, () => { machineLearningMock = newMachineLearningRepositoryMock(); moveMock = newMoveRepositoryMock(); mediaMock = newMediaRepositoryMock(); + partnerMock = newPartnerRepositoryMock(); personMock = newPersonRepositoryMock(); storageMock = newStorageRepositoryMock(); searchMock = newSearchRepositoryMock(); @@ -98,6 +103,7 @@ describe(PersonService.name, () => { machineLearningMock, moveMock, mediaMock, + partnerMock, personMock, configMock, storageMock, @@ -115,31 +121,66 @@ describe(PersonService.name, () => { describe('getAll', () => { it('should get all people with thumbnails', async () => { - personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.noThumbnail]); + personMock.getAllForUsers.mockResolvedValue([personStub.withName, personStub.noThumbnail]); personMock.getNumberOfPeople.mockResolvedValue(1); await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({ total: 1, people: [responseDto], }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { + expect(personMock.getAllForUsers).toHaveBeenCalledWith([authStub.admin.user.id], { + minimumFaceCount: 3, + withHidden: false, + }); + }); + it('should get partner\'s people when requested ', async () => { + personMock.getAllForUsers.mockResolvedValue([personStub.withName, personStub.partnerPerson]); + personMock.getNumberOfPeople.mockResolvedValue(2); + partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); + await expect(sut.getAll(authStub.admin, { withHidden: undefined, withPartners: true })).resolves.toEqual({ + total: 2, + people: [ + responseDto, + { + id: 'person-4', + name: 'Person 4', + birthDate: null, + thumbnailPath: '/path/to/thumbnail', + isHidden: false, + } + ], + }); + expect(personMock.getAllForUsers).toHaveBeenCalledWith([authStub.admin.user.id, authStub.user1.user.id], { + minimumFaceCount: 3, + withHidden: false, + }); + }); + it('should NOT get partners people without flag', async () => { + personMock.getAllForUsers.mockResolvedValue([personStub.withName]); + personMock.getNumberOfPeople.mockResolvedValue(1); + partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); + await expect(sut.getAll(authStub.admin, { /*withPartners: true,*/ withHidden: undefined })).resolves.toEqual({ + total: 1, + people: [responseDto], + }); + expect(personMock.getAllForUsers).toHaveBeenCalledWith([authStub.admin.user.id], { minimumFaceCount: 3, withHidden: false, }); }); it('should get all visible people with thumbnails', async () => { - personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]); + personMock.getAllForUsers.mockResolvedValue([personStub.withName, personStub.hidden]); personMock.getNumberOfPeople.mockResolvedValue(2); await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({ total: 2, people: [responseDto], }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { + expect(personMock.getAllForUsers).toHaveBeenCalledWith([authStub.admin.user.id], { minimumFaceCount: 3, withHidden: false, }); }); it('should get all hidden and visible people with thumbnails', async () => { - personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]); + personMock.getAllForUsers.mockResolvedValue([personStub.withName, personStub.hidden]); personMock.getNumberOfPeople.mockResolvedValue(2); await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({ total: 2, @@ -154,7 +195,7 @@ describe(PersonService.name, () => { }, ], }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { + expect(personMock.getAllForUsers).toHaveBeenCalledWith([authStub.admin.user.id], { minimumFaceCount: 3, withHidden: true, }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 6fbc409bf8766..1b71f35613eda 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -19,6 +19,7 @@ import { IMachineLearningRepository, IMediaRepository, IMoveRepository, + IPartnerRepository, IPersonRepository, ISearchRepository, IStorageRepository, @@ -57,6 +58,7 @@ export class PersonService { @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, @Inject(IMediaRepository) private mediaRepository: IMediaRepository, + @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IPersonRepository) private repository: IPersonRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @@ -78,11 +80,20 @@ export class PersonService { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { const { machineLearning } = await this.configCore.getConfig(); - const people = await this.repository.getAllForUser(auth.user.id, { + let userIds: string[]; + userIds = [auth.user.id]; + if (dto.withPartners) { + const partners = await this.partnerRepository.getAll(auth.user.id); + const partnersIds = partners + .filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id) //TODO should && partner.inTimeline be included, as in assets? + .map((partner) => partner.sharedById); + userIds.push(...partnersIds); + } + const people = await this.repository.getAllForUsers(userIds, { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden: dto.withHidden || false, }); - const total = await this.repository.getNumberOfPeople(auth.user.id); + const total = await this.repository.getNumberOfPeople(userIds); const persons: PersonResponseDto[] = people // with thumbnails .filter((person) => !!person.thumbnailPath) diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index 6aa70a2123afa..d2ddc5abe5b6a 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -36,6 +36,7 @@ export interface IAccessRepository { person: { checkFaceOwnerAccess(userId: string, assetFaceId: Set): Promise>; checkOwnerAccess(userId: string, personIds: Set): Promise>; + checkPartnerAccess(userId: string, personIds: Set): Promise>; }; partner: { diff --git a/server/src/domain/repositories/person.repository.ts b/server/src/domain/repositories/person.repository.ts index 80240091a90c5..044ec18fe98c3 100644 --- a/server/src/domain/repositories/person.repository.ts +++ b/server/src/domain/repositories/person.repository.ts @@ -11,6 +11,7 @@ export interface PersonSearchOptions { export interface PersonNameSearchOptions { withHidden?: boolean; + withPartners?: boolean; } export interface AssetFaceId { @@ -30,7 +31,7 @@ export interface PersonStatistics { export interface IPersonRepository { getAll(pagination: PaginationOptions, options?: FindManyOptions): Paginated; - getAllForUser(userId: string, options: PersonSearchOptions): Promise; + getAllForUsers(userIds: string[], options: PersonSearchOptions): Promise; getAllWithoutFaces(): Promise; getById(personId: string): Promise; getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise; @@ -54,7 +55,7 @@ export interface IPersonRepository { getRandomFace(personId: string): Promise; getStatistics(personId: string): Promise; reassignFace(assetFaceId: string, newPersonId: string): Promise; - getNumberOfPeople(userId: string): Promise; + getNumberOfPeople(userIds: string[]): Promise; reassignFaces(data: UpdateFacesData): Promise; update(entity: Partial): Promise; } diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index ad83d680018db..16a3a31d209bb 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -142,4 +142,18 @@ export const personStub = { faceAsset: null, isHidden: false, }), + partnerPerson: Object.freeze({ + id: 'person-4', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + ownerId: userStub.user1.id, + owner: userStub.user1, + name: 'Person 4', + birthDate: null, + thumbnailPath: '/path/to/thumbnail', + faces: [], + faceAssetId: null, + faceAsset: null, + isHidden: false, + }), }; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index a1f09aa75cb4d..383fbc3c8295e 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -50,6 +50,7 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => }, person: { + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), checkFaceOwnerAccess: jest.fn().mockResolvedValue(new Set()), checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 2a1ccdfe59f22..b3fcc06364e9a 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -4,7 +4,7 @@ export const newPersonRepositoryMock = (): jest.Mocked => { return { getById: jest.fn(), getAll: jest.fn(), - getAllForUser: jest.fn(), + getAllForUsers: jest.fn(), getAssets: jest.fn(), getAllWithoutFaces: jest.fn(), From 34081e5aab0205b1d389659fda728d33e82dc46a Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Mon, 12 Feb 2024 00:40:17 +0100 Subject: [PATCH 03/10] partner read access to person objects --- server/src/domain/access/access.core.ts | 4 +++- .../infra/repositories/access.repository.ts | 20 ++++++++++++++++++- .../infra/repositories/person.repository.ts | 12 +++++------ server/src/infra/sql/access.repository.sql | 10 ++++++++++ server/src/infra/sql/person.repository.sql | 6 +++--- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 8602701072748..ae7f3c0d54582 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -276,7 +276,9 @@ export class AccessCore { } case Permission.PERSON_READ: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); + const isOwner = await this.repository.person.checkOwnerAccess(auth.user.id, ids); + const isPartner = await this.repository.person.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); } case Permission.PERSON_WRITE: { diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index fea1985ced435..631eab09d0d02 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -368,9 +368,27 @@ class TimelineAccess implements ITimelineAccess { class PersonAccess implements IPersonAccess { constructor( private assetFaceRepository: Repository, + private partnerRepository: Repository, private personRepository: Repository, ) {} + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkPartnerAccess(userId: string, personIds: Set): Promise> { + if (personIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .innerJoinAndMapOne('sharedBy.persons', 'person', "person", "person.ownerId = partner.sharedById") //TODO: gives the correct SQL, but somehow feels wrong... + .select('person.id', 'personId') + .where('partner.sharedWithId = :userId', { userId }) + .andWhere('person.id IN (:...personIds)', { personIds: [...personIds] }) + .getRawMany() + .then((rows) => new Set(rows.map((row) => row.personId))); + } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, personIds: Set): Promise> { @@ -456,7 +474,7 @@ export class AccessRepository implements IAccessRepository { this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); this.authDevice = new AuthDeviceAccess(tokenRepository); this.library = new LibraryAccess(libraryRepository, partnerRepository); - this.person = new PersonAccess(assetFaceRepository, personRepository); + this.person = new PersonAccess(assetFaceRepository, partnerRepository, personRepository); this.partner = new PartnerAccess(partnerRepository); this.timeline = new TimelineAccess(partnerRepository); } diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 85423b74ddeb4..1a1b1bd5f3a0b 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -57,12 +57,12 @@ export class PersonRepository implements IPersonRepository { return paginate(this.personRepository, pagination, options); } - @GenerateSql({ params: [DummyValue.UUID] }) - getAllForUser(userId: string, options?: PersonSearchOptions): Promise { + @GenerateSql({ params: [[DummyValue.UUID]] }) + getAllForUsers(userIds: string[], options?: PersonSearchOptions): Promise { const queryBuilder = this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') - .where('person.ownerId = :userId', { userId }) + .where('person.ownerId IN (:...userIds)', { userIds }) .innerJoin('face.asset', 'asset') .andWhere('asset.isArchived = false') .orderBy('person.isHidden', 'ASC') @@ -206,12 +206,12 @@ export class PersonRepository implements IPersonRepository { }); } - @GenerateSql({ params: [DummyValue.UUID] }) - async getNumberOfPeople(userId: string): Promise { + @GenerateSql({ params: [[DummyValue.UUID]] }) + async getNumberOfPeople(userIds: string[]): Promise { return this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') - .where('person.ownerId = :userId', { userId }) + .where('person.ownerId IN (:...userIds)', { userIds }) .having('COUNT(face.assetId) != 0') .groupBy('person.id') .withDeleted() diff --git a/server/src/infra/sql/access.repository.sql b/server/src/infra/sql/access.repository.sql index 638be9f90b7ae..e159738dac97d 100644 --- a/server/src/infra/sql/access.repository.sql +++ b/server/src/infra/sql/access.repository.sql @@ -206,6 +206,16 @@ WHERE "partner"."sharedById" IN ($1) AND "partner"."sharedWithId" = $2 +-- AccessRepository.person.checkPartnerAccess +SELECT + "person"."id" AS "personId" +FROM + "partners" "partner" + INNER JOIN "person" "person" ON "person"."ownerId" = "partner"."sharedById" +WHERE + "partner"."sharedWithId" = $1 + AND "person"."id" IN ($2) + -- AccessRepository.person.checkOwnerAccess SELECT "PersonEntity"."id" AS "PersonEntity_id" diff --git a/server/src/infra/sql/person.repository.sql b/server/src/infra/sql/person.repository.sql index bd4a523e86209..435582fe1bcb5 100644 --- a/server/src/infra/sql/person.repository.sql +++ b/server/src/infra/sql/person.repository.sql @@ -7,7 +7,7 @@ SET WHERE "personId" = $2 --- PersonRepository.getAllForUser +-- PersonRepository.getAllForUsers SELECT "person"."id" AS "person_id", "person"."createdAt" AS "person_createdAt", @@ -24,7 +24,7 @@ FROM INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE - "person"."ownerId" = $1 + "person"."ownerId" IN ($1) AND "asset"."isArchived" = false AND "person"."isHidden" = false GROUP BY @@ -349,7 +349,7 @@ FROM "person" "person" LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" WHERE - "person"."ownerId" = $1 + "person"."ownerId" IN ($1) HAVING COUNT("face"."assetId") != 0 From 4863ed3fd6463810c2275d69998de5672d0c3cc7 Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Mon, 12 Feb 2024 00:40:22 +0100 Subject: [PATCH 04/10] web: explore persons, ask for partner-shared persons too --- web/src/routes/(user)/explore/+page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/(user)/explore/+page.ts b/web/src/routes/(user)/explore/+page.ts index 4894714a2410a..04286c817eb68 100644 --- a/web/src/routes/(user)/explore/+page.ts +++ b/web/src/routes/(user)/explore/+page.ts @@ -4,7 +4,7 @@ import type { PageLoad } from './$types'; export const load = (async () => { await authenticate(); - const [items, response] = await Promise.all([getExploreData(), getAllPeople({ withHidden: false })]); + const [items, response] = await Promise.all([getExploreData(), getAllPeople({ withHidden: false, withPartners: true })]); return { items, From b7f634c6795109247178aceb27fb0114947a4921 Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Mon, 12 Feb 2024 00:40:30 +0100 Subject: [PATCH 05/10] make open-api --- mobile/openapi/doc/PersonApi.md | 6 ++++-- mobile/openapi/lib/api/person_api.dart | 13 +++++++++--- mobile/openapi/test/person_api_test.dart | 2 +- open-api/immich-openapi-specs.json | 3 ++- open-api/typescript-sdk/axios-client/api.ts | 23 ++++++++++++++++----- open-api/typescript-sdk/fetch-client.ts | 6 ++++-- 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index f9e3100186232..0a0b2f4bfae1c 100644 --- a/mobile/openapi/doc/PersonApi.md +++ b/mobile/openapi/doc/PersonApi.md @@ -73,7 +73,7 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getAllPeople** -> PeopleResponseDto getAllPeople(withHidden) +> PeopleResponseDto getAllPeople(withHidden, withPartners) @@ -97,9 +97,10 @@ import 'package:openapi/api.dart'; final api_instance = PersonApi(); final withHidden = true; // bool | +final withPartners = true; // bool | try { - final result = api_instance.getAllPeople(withHidden); + final result = api_instance.getAllPeople(withHidden, withPartners); print(result); } catch (e) { print('Exception when calling PersonApi->getAllPeople: $e\n'); @@ -111,6 +112,7 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **withHidden** | **bool**| | [optional] [default to false] + **withPartners** | **bool**| | [optional] [default to false] ### Return type diff --git a/mobile/openapi/lib/api/person_api.dart b/mobile/openapi/lib/api/person_api.dart index 02dae625bb91d..4bd00569b50b2 100644 --- a/mobile/openapi/lib/api/person_api.dart +++ b/mobile/openapi/lib/api/person_api.dart @@ -61,7 +61,9 @@ class PersonApi { /// Parameters: /// /// * [bool] withHidden: - Future getAllPeopleWithHttpInfo({ bool? withHidden, }) async { + /// + /// * [bool] withPartners: + Future getAllPeopleWithHttpInfo({ bool? withHidden, bool? withPartners, }) async { // ignore: prefer_const_declarations final path = r'/person'; @@ -75,6 +77,9 @@ class PersonApi { if (withHidden != null) { queryParams.addAll(_queryParams('', 'withHidden', withHidden)); } + if (withPartners != null) { + queryParams.addAll(_queryParams('', 'withPartners', withPartners)); + } const contentTypes = []; @@ -93,8 +98,10 @@ class PersonApi { /// Parameters: /// /// * [bool] withHidden: - Future getAllPeople({ bool? withHidden, }) async { - final response = await getAllPeopleWithHttpInfo( withHidden: withHidden, ); + /// + /// * [bool] withPartners: + Future getAllPeople({ bool? withHidden, bool? withPartners, }) async { + final response = await getAllPeopleWithHttpInfo( withHidden: withHidden, withPartners: withPartners, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/test/person_api_test.dart b/mobile/openapi/test/person_api_test.dart index dd112eeaaee34..606fe8d3c2703 100644 --- a/mobile/openapi/test/person_api_test.dart +++ b/mobile/openapi/test/person_api_test.dart @@ -22,7 +22,7 @@ void main() { // TODO }); - //Future getAllPeople({ bool withHidden }) async + //Future getAllPeople({ bool withHidden, bool withPartners }) async test('test getAllPeople', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dc23275363a19..1d22d55049cae 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4011,6 +4011,7 @@ "required": false, "in": "query", "schema": { + "default": false, "type": "boolean" } } @@ -10522,4 +10523,4 @@ } } } -} +} \ No newline at end of file diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index 622820c7528d2..0af66060748ee 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -14256,10 +14256,11 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio /** * * @param {boolean} [withHidden] + * @param {boolean} [withPartners] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllPeople: async (withHidden?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + getAllPeople: async (withHidden?: boolean, withPartners?: boolean, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/person`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -14285,6 +14286,10 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['withHidden'] = withHidden; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -14676,11 +14681,12 @@ export const PersonApiFp = function(configuration?: Configuration) { /** * * @param {boolean} [withHidden] + * @param {boolean} [withPartners] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllPeople(withHidden?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(withHidden, options); + async getAllPeople(withHidden?: boolean, withPartners?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(withHidden, withPartners, options); const index = configuration?.serverIndex ?? 0; const operationBasePath = operationServerMap['PersonApi.getAllPeople']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); @@ -14809,7 +14815,7 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat * @throws {RequiredError} */ getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.getAllPeople(requestParameters.withHidden, options).then((request) => request(axios, basePath)); + return localVarFp.getAllPeople(requestParameters.withHidden, requestParameters.withPartners, options).then((request) => request(axios, basePath)); }, /** * @@ -14898,6 +14904,13 @@ export interface PersonApiGetAllPeopleRequest { * @memberof PersonApiGetAllPeople */ readonly withHidden?: boolean + + /** + * + * @type {boolean} + * @memberof PersonApiGetAllPeople + */ + readonly withPartners?: boolean } /** @@ -15058,7 +15071,7 @@ export class PersonApi extends BaseAPI { * @memberof PersonApi */ public getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: RawAxiosRequestConfig) { - return PersonApiFp(this.configuration).getAllPeople(requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath)); + return PersonApiFp(this.configuration).getAllPeople(requestParameters.withHidden, requestParameters.withPartners, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index 9b3a359863e59..e2d32d15cab56 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -2020,14 +2020,16 @@ export function updatePartner({ id, updatePartnerDto }: { body: updatePartnerDto }))); } -export function getAllPeople({ withHidden }: { +export function getAllPeople({ withHidden, withPartners }: { withHidden?: boolean; + withPartners?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: PeopleResponseDto; }>(`/person${QS.query(QS.explode({ - withHidden + withHidden, + withPartners }))}`, { ...opts })); From bcdf5e3fad631657fa2b10431de2a3d73ee9f4ba Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Mon, 12 Feb 2024 16:00:01 +0100 Subject: [PATCH 06/10] Force-include partners when time-bucket person --- server/src/domain/asset/asset.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 325bb8ea4c2c3..75f5e88089e0a 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -241,7 +241,7 @@ export class AssetService { if (userId) { userIds = [userId]; - if (dto.withPartners) { + if (dto.withPartners || dto.personId !== undefined) { const partners = await this.partnerRepository.getAll(auth.user.id); const partnersIds = partners .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) From 9ec0a67ecca48f135378fe8a812c13cf2c59cfaf Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Mon, 12 Feb 2024 16:06:47 +0100 Subject: [PATCH 07/10] lint and friends --- server/src/domain/person/person.service.spec.ts | 4 ++-- server/src/domain/person/person.service.ts | 11 +++++------ server/src/infra/repositories/access.repository.ts | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index d91232a7f1858..9b8ba47a59f70 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -132,7 +132,7 @@ describe(PersonService.name, () => { withHidden: false, }); }); - it('should get partner\'s people when requested ', async () => { + it("should get partner's people when requested ", async () => { personMock.getAllForUsers.mockResolvedValue([personStub.withName, personStub.partnerPerson]); personMock.getNumberOfPeople.mockResolvedValue(2); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); @@ -146,7 +146,7 @@ describe(PersonService.name, () => { birthDate: null, thumbnailPath: '/path/to/thumbnail', isHidden: false, - } + }, ], }); expect(personMock.getAllForUsers).toHaveBeenCalledWith([authStub.admin.user.id, authStub.user1.user.id], { diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 1b71f35613eda..a5dde94fd01d9 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -80,14 +80,13 @@ export class PersonService { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { const { machineLearning } = await this.configCore.getConfig(); - let userIds: string[]; - userIds = [auth.user.id]; + const userIds: string[] = [auth.user.id]; if (dto.withPartners) { - const partners = await this.partnerRepository.getAll(auth.user.id); - const partnersIds = partners - .filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id) //TODO should && partner.inTimeline be included, as in assets? + const partners = await this.partnerRepository.getAll(auth.user.id); + const partnersIds = partners + .filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id) //TODO should && partner.inTimeline be included, as in assets? .map((partner) => partner.sharedById); - userIds.push(...partnersIds); + userIds.push(...partnersIds); } const people = await this.repository.getAllForUsers(userIds, { minimumFaceCount: machineLearning.facialRecognition.minFaces, diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index 631eab09d0d02..8c81e1c2b79de 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -381,7 +381,7 @@ class PersonAccess implements IPersonAccess { return this.partnerRepository .createQueryBuilder('partner') - .innerJoinAndMapOne('sharedBy.persons', 'person', "person", "person.ownerId = partner.sharedById") //TODO: gives the correct SQL, but somehow feels wrong... + .innerJoinAndMapOne('sharedBy.persons', 'person', 'person', 'person.ownerId = partner.sharedById') //TODO: gives the correct SQL, but somehow feels wrong... .select('person.id', 'personId') .where('partner.sharedWithId = :userId', { userId }) .andWhere('person.id IN (:...personIds)', { personIds: [...personIds] }) From 282014970e1bf345d7fa451cf64501ef2bce84d1 Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Tue, 13 Feb 2024 21:59:20 +0000 Subject: [PATCH 08/10] remove todos --- server/src/domain/person/person.service.ts | 2 +- server/src/infra/repositories/access.repository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index a5dde94fd01d9..e2856f44d37ce 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -84,7 +84,7 @@ export class PersonService { if (dto.withPartners) { const partners = await this.partnerRepository.getAll(auth.user.id); const partnersIds = partners - .filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id) //TODO should && partner.inTimeline be included, as in assets? + .filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id) .map((partner) => partner.sharedById); userIds.push(...partnersIds); } diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index 8c81e1c2b79de..98d7fe9d89fe3 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -381,7 +381,7 @@ class PersonAccess implements IPersonAccess { return this.partnerRepository .createQueryBuilder('partner') - .innerJoinAndMapOne('sharedBy.persons', 'person', 'person', 'person.ownerId = partner.sharedById') //TODO: gives the correct SQL, but somehow feels wrong... + .innerJoinAndMapOne('sharedBy.persons', 'person', 'person', 'person.ownerId = partner.sharedById') .select('person.id', 'personId') .where('partner.sharedWithId = :userId', { userId }) .andWhere('person.id IN (:...personIds)', { personIds: [...personIds] }) From 81cb0a966484d22f8b620d43c7ae139972290ce0 Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Wed, 21 Feb 2024 00:44:40 +0100 Subject: [PATCH 09/10] Request withPartners on people page too --- web/src/routes/(user)/people/+page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/(user)/people/+page.ts b/web/src/routes/(user)/people/+page.ts index c7ac50a493247..3a4a7718945fb 100644 --- a/web/src/routes/(user)/people/+page.ts +++ b/web/src/routes/(user)/people/+page.ts @@ -5,7 +5,7 @@ import type { PageLoad } from './$types'; export const load = (async () => { await authenticate(); - const people = await getAllPeople({ withHidden: true }); + const people = await getAllPeople({ withHidden: true, withPartners: true }); return { people, meta: { From fa04b0293a09c17b26252a3c62a0894ce5a193c7 Mon Sep 17 00:00:00 2001 From: Torbjorn Tyridal Date: Wed, 21 Feb 2024 01:38:27 +0100 Subject: [PATCH 10/10] expose ownerId in personResponse make it possible for frontend to visualize the fact that this is a shared person f.ex with a small icon or hiding actions like merge, change name etc that will fail due to share beeing read-only. --- mobile/openapi/doc/PersonResponseDto.md | 1 + mobile/openapi/doc/PersonWithFacesResponseDto.md | 1 + mobile/openapi/lib/model/person_response_dto.dart | 10 +++++++++- .../lib/model/person_with_faces_response_dto.dart | 10 +++++++++- mobile/openapi/test/person_response_dto_test.dart | 5 +++++ .../test/person_with_faces_response_dto_test.dart | 5 +++++ open-api/immich-openapi-specs.json | 8 ++++++++ open-api/typescript-sdk/axios-client/api.ts | 12 ++++++++++++ open-api/typescript-sdk/fetch-client.ts | 2 ++ server/src/domain/person/person.dto.ts | 2 ++ server/src/domain/person/person.service.spec.ts | 5 +++++ web/src/routes/(user)/explore/+page.ts | 5 ++++- 12 files changed, 63 insertions(+), 3 deletions(-) diff --git a/mobile/openapi/doc/PersonResponseDto.md b/mobile/openapi/doc/PersonResponseDto.md index c2acbacd1b5ab..046fef71d4903 100644 --- a/mobile/openapi/doc/PersonResponseDto.md +++ b/mobile/openapi/doc/PersonResponseDto.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **id** | **String** | | **isHidden** | **bool** | | **name** | **String** | | +**ownerId** | **String** | | **thumbnailPath** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/PersonWithFacesResponseDto.md b/mobile/openapi/doc/PersonWithFacesResponseDto.md index ddef73618cee8..c5dc85e59ca67 100644 --- a/mobile/openapi/doc/PersonWithFacesResponseDto.md +++ b/mobile/openapi/doc/PersonWithFacesResponseDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **id** | **String** | | **isHidden** | **bool** | | **name** | **String** | | +**ownerId** | **String** | | **thumbnailPath** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index 30edc062beef5..84f178de50dfd 100644 --- a/mobile/openapi/lib/model/person_response_dto.dart +++ b/mobile/openapi/lib/model/person_response_dto.dart @@ -17,6 +17,7 @@ class PersonResponseDto { required this.id, required this.isHidden, required this.name, + required this.ownerId, required this.thumbnailPath, }); @@ -28,6 +29,8 @@ class PersonResponseDto { String name; + String ownerId; + String thumbnailPath; @override @@ -36,6 +39,7 @@ class PersonResponseDto { other.id == id && other.isHidden == isHidden && other.name == name && + other.ownerId == ownerId && other.thumbnailPath == thumbnailPath; @override @@ -45,10 +49,11 @@ class PersonResponseDto { (id.hashCode) + (isHidden.hashCode) + (name.hashCode) + + (ownerId.hashCode) + (thumbnailPath.hashCode); @override - String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]'; + String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, ownerId=$ownerId, thumbnailPath=$thumbnailPath]'; Map toJson() { final json = {}; @@ -60,6 +65,7 @@ class PersonResponseDto { json[r'id'] = this.id; json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; + json[r'ownerId'] = this.ownerId; json[r'thumbnailPath'] = this.thumbnailPath; return json; } @@ -76,6 +82,7 @@ class PersonResponseDto { id: mapValueOfType(json, r'id')!, isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, + ownerId: mapValueOfType(json, r'ownerId')!, thumbnailPath: mapValueOfType(json, r'thumbnailPath')!, ); } @@ -128,6 +135,7 @@ class PersonResponseDto { 'id', 'isHidden', 'name', + 'ownerId', 'thumbnailPath', }; } diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index 67ac02ca02a5c..2b8fe721fe83b 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -18,6 +18,7 @@ class PersonWithFacesResponseDto { required this.id, required this.isHidden, required this.name, + required this.ownerId, required this.thumbnailPath, }); @@ -31,6 +32,8 @@ class PersonWithFacesResponseDto { String name; + String ownerId; + String thumbnailPath; @override @@ -40,6 +43,7 @@ class PersonWithFacesResponseDto { other.id == id && other.isHidden == isHidden && other.name == name && + other.ownerId == ownerId && other.thumbnailPath == thumbnailPath; @override @@ -50,10 +54,11 @@ class PersonWithFacesResponseDto { (id.hashCode) + (isHidden.hashCode) + (name.hashCode) + + (ownerId.hashCode) + (thumbnailPath.hashCode); @override - String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]'; + String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, ownerId=$ownerId, thumbnailPath=$thumbnailPath]'; Map toJson() { final json = {}; @@ -66,6 +71,7 @@ class PersonWithFacesResponseDto { json[r'id'] = this.id; json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; + json[r'ownerId'] = this.ownerId; json[r'thumbnailPath'] = this.thumbnailPath; return json; } @@ -83,6 +89,7 @@ class PersonWithFacesResponseDto { id: mapValueOfType(json, r'id')!, isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, + ownerId: mapValueOfType(json, r'ownerId')!, thumbnailPath: mapValueOfType(json, r'thumbnailPath')!, ); } @@ -136,6 +143,7 @@ class PersonWithFacesResponseDto { 'id', 'isHidden', 'name', + 'ownerId', 'thumbnailPath', }; } diff --git a/mobile/openapi/test/person_response_dto_test.dart b/mobile/openapi/test/person_response_dto_test.dart index 0ba73061177b6..4ec2d960e4ee2 100644 --- a/mobile/openapi/test/person_response_dto_test.dart +++ b/mobile/openapi/test/person_response_dto_test.dart @@ -36,6 +36,11 @@ void main() { // TODO }); + // String ownerId + test('to test the property `ownerId`', () async { + // TODO + }); + // String thumbnailPath test('to test the property `thumbnailPath`', () async { // TODO diff --git a/mobile/openapi/test/person_with_faces_response_dto_test.dart b/mobile/openapi/test/person_with_faces_response_dto_test.dart index 7f7e0f89acbd1..7d228e56c53cb 100644 --- a/mobile/openapi/test/person_with_faces_response_dto_test.dart +++ b/mobile/openapi/test/person_with_faces_response_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // String ownerId + test('to test the property `ownerId`', () async { + // TODO + }); + // String thumbnailPath test('to test the property `thumbnailPath`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 1d22d55049cae..bdb71d2be0172 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8676,6 +8676,9 @@ "name": { "type": "string" }, + "ownerId": { + "type": "string" + }, "thumbnailPath": { "type": "string" } @@ -8685,6 +8688,7 @@ "id", "isHidden", "name", + "ownerId", "thumbnailPath" ], "type": "object" @@ -8745,6 +8749,9 @@ "name": { "type": "string" }, + "ownerId": { + "type": "string" + }, "thumbnailPath": { "type": "string" } @@ -8755,6 +8762,7 @@ "id", "isHidden", "name", + "ownerId", "thumbnailPath" ], "type": "object" diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index 0af66060748ee..444134d6bbf39 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -2894,6 +2894,12 @@ export interface PersonResponseDto { * @memberof PersonResponseDto */ 'name': string; + /** + * + * @type {string} + * @memberof PersonResponseDto + */ + 'ownerId': string; /** * * @type {string} @@ -2981,6 +2987,12 @@ export interface PersonWithFacesResponseDto { * @memberof PersonWithFacesResponseDto */ 'name': string; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'ownerId': string; /** * * @type {string} diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index e2d32d15cab56..fc53abd1bdb17 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -94,6 +94,7 @@ export type PersonWithFacesResponseDto = { id: string; isHidden: boolean; name: string; + ownerId: string; thumbnailPath: string; }; export type SmartInfoResponseDto = { @@ -395,6 +396,7 @@ export type PersonResponseDto = { id: string; isHidden: boolean; name: string; + ownerId: string; thumbnailPath: string; }; export type AssetFaceResponseDto = { diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index ef27ff0343fa9..aabb42557c6d1 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -77,6 +77,7 @@ export class PersonResponseDto { birthDate!: Date | null; thumbnailPath!: string; isHidden!: boolean; + ownerId!: string; } export class PersonWithFacesResponseDto extends PersonResponseDto { @@ -143,6 +144,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto { birthDate: person.birthDate, thumbnailPath: person.thumbnailPath, isHidden: person.isHidden, + ownerId: person.ownerId, }; } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 9b8ba47a59f70..f70bf48d6b42d 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -48,6 +48,7 @@ const responseDto: PersonResponseDto = { birthDate: null, thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, + ownerId: authStub.admin.user.id, }; const statistics = { assets: 3 }; @@ -146,6 +147,7 @@ describe(PersonService.name, () => { birthDate: null, thumbnailPath: '/path/to/thumbnail', isHidden: false, + ownerId: authStub.user1.user.id, }, ], }); @@ -192,6 +194,7 @@ describe(PersonService.name, () => { birthDate: null, thumbnailPath: '/path/to/thumbnail.jpg', isHidden: true, + ownerId: personStub.noName.ownerId, }, ], }); @@ -325,6 +328,7 @@ describe(PersonService.name, () => { birthDate: new Date('1976-06-30'), thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, + ownerId: personStub.noName.ownerId, }); expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); @@ -525,6 +529,7 @@ describe(PersonService.name, () => { isHidden: personStub.noName.isHidden, id: personStub.noName.id, name: personStub.noName.name, + ownerId: personStub.noName.ownerId, thumbnailPath: personStub.noName.thumbnailPath, }); diff --git a/web/src/routes/(user)/explore/+page.ts b/web/src/routes/(user)/explore/+page.ts index 04286c817eb68..a7b5c7a53c949 100644 --- a/web/src/routes/(user)/explore/+page.ts +++ b/web/src/routes/(user)/explore/+page.ts @@ -4,7 +4,10 @@ import type { PageLoad } from './$types'; export const load = (async () => { await authenticate(); - const [items, response] = await Promise.all([getExploreData(), getAllPeople({ withHidden: false, withPartners: true })]); + const [items, response] = await Promise.all([ + getExploreData(), + getAllPeople({ withHidden: false, withPartners: true }), + ]); return { items,