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), '[]') diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index d313d682bd..88d23f780b 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(withFacesAndPeople) .$if(withTags, (eb) => eb.select((eb) => jsonArrayFrom( diff --git a/server/test/mappers.ts b/server/test/mappers.ts index 40ae78fe26..897ccb84fa 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 new file mode 100644 index 0000000000..e09199456f --- /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, 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 }); + + 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); + }); + }); +});