From 2879ded8b04455af02370635a0a0f5c543f67c38 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:35:39 -0400 Subject: [PATCH] better query, add medium test --- server/src/repositories/asset.repository.ts | 18 ++----- .../specs/services/timeline.service.spec.ts | 52 ++++++++++++++++++- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 4d3068104f..5250284fde 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -592,7 +592,7 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.TIME_BUCKET, { withStacked: true }], }) - getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth?: AuthDto) { + getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) { const query = this.db .with('cte', (qb) => qb @@ -602,7 +602,7 @@ export class AssetRepository { 'asset.duration', 'asset.id', 'asset.visibility', - 'asset.isFavorite', + sql`asset."isFavorite" and asset."ownerId" = ${auth.user.id}`.as('isFavorite'), sql`asset.type = 'IMAGE'`.as('isImage'), sql`asset."deletedAt" is not null`.as('isTrashed'), 'asset.livePhotoVideoId', @@ -691,19 +691,7 @@ export class AssetRepository { eb.fn.coalesce(eb.fn('array_agg', ['duration']), sql.lit('{}')).as('duration'), eb.fn.coalesce(eb.fn('array_agg', ['id']), sql.lit('{}')).as('id'), eb.fn.coalesce(eb.fn('array_agg', ['visibility']), sql.lit('{}')).as('visibility'), - eb.fn - .coalesce( - eb.fn('array_agg', [ - eb - .case() - .when(eb.ref('ownerId'), '=', auth?.user.id || '') - .then(eb.ref('isFavorite')) - .else(sql.lit(false)) - .end(), - ]), - sql.lit('{}'), - ) - .as('isFavorite'), + eb.fn.coalesce(eb.fn('array_agg', ['isFavorite']), sql.lit('{}')).as('isFavorite'), eb.fn.coalesce(eb.fn('array_agg', ['isImage']), sql.lit('{}')).as('isImage'), // TODO: isTrashed is redundant as it will always be all true or false depending on the options eb.fn.coalesce(eb.fn('array_agg', ['isTrashed']), sql.lit('{}')).as('isTrashed'), diff --git a/server/test/medium/specs/services/timeline.service.spec.ts b/server/test/medium/specs/services/timeline.service.spec.ts index fa4a75e869..eaca4dcc14 100644 --- a/server/test/medium/specs/services/timeline.service.spec.ts +++ b/server/test/medium/specs/services/timeline.service.spec.ts @@ -4,6 +4,7 @@ import { AssetVisibility } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PartnerRepository } from 'src/repositories/partner.repository'; import { DB } from 'src/schema'; import { TimelineService } from 'src/services/timeline.service'; import { newMediumService } from 'test/medium.factory'; @@ -15,7 +16,7 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(TimelineService, { database: db || defaultDatabase, - real: [AssetRepository, AccessRepository], + real: [AssetRepository, AccessRepository, PartnerRepository], mock: [LoggingRepository], }); }; @@ -155,5 +156,54 @@ describe(TimelineService.name, () => { const response = JSON.parse(rawResponse); expect(response).toEqual(expect.objectContaining({ isTrashed: [true] })); }); + + it('should return false for favorite status unless asset owner', async () => { + const { sut, ctx } = setup(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newUser().then(async ({ user }) => { + const result = await ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('1970-02-12'), + localDateTime: new Date('1970-02-12'), + isFavorite: true, + }); + await ctx.newExif({ assetId: result.asset.id, make: 'Canon' }); + return result; + }), + ctx.newUser().then(async ({ user }) => { + const result = await ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('1970-02-13'), + localDateTime: new Date('1970-02-13'), + isFavorite: true, + }); + await ctx.newExif({ assetId: result.asset.id, make: 'Canon' }); + return result; + }), + ]); + + await Promise.all([ + ctx.newPartner({ sharedById: asset1.ownerId, sharedWithId: asset2.ownerId }), + ctx.newPartner({ sharedById: asset2.ownerId, sharedWithId: asset1.ownerId }), + ]); + + const auth1 = factory.auth({ user: { id: asset1.ownerId } }); + const rawResponse1 = await sut.getTimeBucket(auth1, { + timeBucket: '1970-02-01', + withPartners: true, + visibility: AssetVisibility.Timeline, + }); + const response1 = JSON.parse(rawResponse1); + expect(response1).toEqual(expect.objectContaining({ id: [asset2.id, asset1.id], isFavorite: [false, true] })); + + const auth2 = factory.auth({ user: { id: asset2.ownerId } }); + const rawResponse2 = await sut.getTimeBucket(auth2, { + timeBucket: '1970-02-01', + withPartners: true, + visibility: AssetVisibility.Timeline, + }); + const response2 = JSON.parse(rawResponse2); + expect(response2).toEqual(expect.objectContaining({ id: [asset2.id, asset1.id], isFavorite: [true, false] })); + }); }); });