From 3dc1ba14f5f57f8459c73488ba145dba7715a237 Mon Sep 17 00:00:00 2001 From: LautjeDS <78675630+LautjeDS@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:08:16 +0200 Subject: [PATCH 1/5] Fixed faces not showing up in the web ui for assets in a stack --- server/src/repositories/stack.repository.ts | 3 ++- server/src/services/stack.service.spec.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index d313d682bd..b434259354 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -6,7 +6,7 @@ import { columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; import { DB } from 'src/schema'; import { StackTable } from 'src/schema/tables/stack.table'; -import { asUuid, withDefaultVisibility } from 'src/utils/database'; +import { asUuid, withDefaultVisibility, withFacesAndPeople } from 'src/utils/database'; export interface StackSearch { ownerId: string; @@ -27,6 +27,7 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) => { .as('exifInfo'), (join) => join.onTrue(), ) + .select((eb) => withFacesAndPeople(eb)) .$if(withTags, (eb) => eb.select((eb) => jsonArrayFrom( diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index d47d634f4f..933e8e405b 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -111,6 +111,24 @@ describe(StackService.name, () => { expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); }); + + it('should include face data on stack assets', async () => { + const auth = AuthFactory.create(); + const stack = StackFactory.from() + .primaryAsset({}, (builder) => builder.exif().face()) + .asset({}, (builder) => builder.exif().face()) + .build(); + + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(getForStack(stack)); + + const response = await sut.get(auth, stack.id); + + expect(response.assets).toHaveLength(2); + for (const asset of response.assets) { + expect(asset.unassignedFaces).toHaveLength(1); + } + }); }); describe('update', () => { From d61226503cc1071140f97a50693730584733503e Mon Sep 17 00:00:00 2001 From: LautjeDS <78675630+LautjeDS@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:18:21 +0200 Subject: [PATCH 2/5] Moved test from service to repository --- server/src/services/stack.service.spec.ts | 18 ------- .../repositories/stack.repository.spec.ts | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 server/test/medium/specs/repositories/stack.repository.spec.ts diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index 933e8e405b..d47d634f4f 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -111,24 +111,6 @@ describe(StackService.name, () => { expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); }); - - it('should include face data on stack assets', async () => { - const auth = AuthFactory.create(); - const stack = StackFactory.from() - .primaryAsset({}, (builder) => builder.exif().face()) - .asset({}, (builder) => builder.exif().face()) - .build(); - - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); - mocks.stack.getById.mockResolvedValue(getForStack(stack)); - - const response = await sut.get(auth, stack.id); - - expect(response.assets).toHaveLength(2); - for (const asset of response.assets) { - expect(asset.unassignedFaces).toHaveLength(1); - } - }); }); describe('update', () => { diff --git a/server/test/medium/specs/repositories/stack.repository.spec.ts b/server/test/medium/specs/repositories/stack.repository.spec.ts new file mode 100644 index 0000000000..a1a0ae95df --- /dev/null +++ b/server/test/medium/specs/repositories/stack.repository.spec.ts @@ -0,0 +1,47 @@ +import { Kysely } from 'kysely'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { StackRepository } from 'src/repositories/stack.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(StackRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(StackRepository.name, () => { + describe('getById', () => { + it('should return faces for stack assets', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset: primaryAsset } = await ctx.newAsset({ ownerId: user.id }); + const { asset: secondaryAsset } = await ctx.newAsset({ ownerId: user.id }); + + await ctx.newExif({ assetId: primaryAsset.id }); + await ctx.newExif({ assetId: secondaryAsset.id }); + + const { person } = await ctx.newPerson({ ownerId: user.id }); + await ctx.newAssetFace({ assetId: primaryAsset.id, personId: person.id }); + + const { result: createdStack } = await ctx.newStack({ ownerId: user.id }, [primaryAsset.id, secondaryAsset.id]); + + const stack = await sut.getById(createdStack.id); + + expect(stack).toBeDefined(); + expect(stack?.assets[0]?.faces).toHaveLength(1); + expect(stack?.assets[0]?.faces[0]?.person?.id).toBe(person.id); + }); + }); +}); From 37590ccb3878c3472a06250bf21ee780ce1146cc Mon Sep 17 00:00:00 2001 From: Laurens <78675630+LautjeDS@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:50:21 +0200 Subject: [PATCH 3/5] Update server/src/repositories/stack.repository.ts Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- server/src/repositories/stack.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index b434259354..88d23f780b 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -27,7 +27,7 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) => { .as('exifInfo'), (join) => join.onTrue(), ) - .select((eb) => withFacesAndPeople(eb)) + .select(withFacesAndPeople) .$if(withTags, (eb) => eb.select((eb) => jsonArrayFrom( From f3015fc21ddb2f97516b05eb874d19ecbe5b3430 Mon Sep 17 00:00:00 2001 From: LautjeDS <78675630+LautjeDS@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:58:55 +0200 Subject: [PATCH 4/5] Fixed getStack mapper, avoiding type errors, and stack repo medium test --- server/test/mappers.ts | 6 ++++++ .../test/medium/specs/repositories/stack.repository.spec.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/server/test/mappers.ts b/server/test/mappers.ts index ed2c9431f3..b3b8e3c469 100644 --- a/server/test/mappers.ts +++ b/server/test/mappers.ts @@ -203,6 +203,12 @@ export const getForStack = (stack: ReturnType) => ({ assets: stack.assets.map((asset) => ({ ...getDehydrated(asset), exifInfo: getDehydrated(asset.exifInfo), + faces: asset.faces + .filter((face): face is typeof face & { person: NonNullable } => face.person !== null) + .map((face) => ({ + ...getDehydrated(face), + person: getDehydrated(face.person), + })), })), }); diff --git a/server/test/medium/specs/repositories/stack.repository.spec.ts b/server/test/medium/specs/repositories/stack.repository.spec.ts index a1a0ae95df..e09199456f 100644 --- a/server/test/medium/specs/repositories/stack.repository.spec.ts +++ b/server/test/medium/specs/repositories/stack.repository.spec.ts @@ -29,8 +29,8 @@ describe(StackRepository.name, () => { const { asset: primaryAsset } = await ctx.newAsset({ ownerId: user.id }); const { asset: secondaryAsset } = await ctx.newAsset({ ownerId: user.id }); - await ctx.newExif({ assetId: primaryAsset.id }); - await ctx.newExif({ assetId: secondaryAsset.id }); + await ctx.newExif({ assetId: primaryAsset.id, fileSizeInByte: 123 }); + await ctx.newExif({ assetId: secondaryAsset.id, fileSizeInByte: 456 }); const { person } = await ctx.newPerson({ ownerId: user.id }); await ctx.newAssetFace({ assetId: primaryAsset.id, personId: person.id }); From 967beab654c06dc82594ac1f990e572a38b1cd11 Mon Sep 17 00:00:00 2001 From: LautjeDS <78675630+LautjeDS@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:33:56 +0200 Subject: [PATCH 5/5] Updated stack queries --- server/src/queries/stack.repository.sql | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index b5f1dc7d18..78bc45346b 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -10,6 +10,30 @@ select ( select "asset".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_face".*, + "person" as "person" + from + "asset_face" + left join lateral ( + select + "person".* + from + "person" + where + "asset_face"."personId" = "person"."id" + ) as "person" on true + where + "asset_face"."assetId" = "asset"."id" + and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true + ) as agg + ) as "faces", to_json("exifInfo") as "exifInfo" from "asset" @@ -76,6 +100,30 @@ select ( select "asset".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_face".*, + "person" as "person" + from + "asset_face" + left join lateral ( + select + "person".* + from + "person" + where + "asset_face"."personId" = "person"."id" + ) as "person" on true + where + "asset_face"."assetId" = "asset"."id" + and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true + ) as agg + ) as "faces", ( select coalesce(json_agg(agg), '[]')