From 27f69b39b2a17b52653989c55c5e8e99648ab3fc Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:49:35 +0100 Subject: [PATCH] fix(server): use correct day ordering in timeline buckets (#26821) * fix(web): sort timeline day groups received from server * fix(server): use correct day ordering in timeline buckets --- server/src/queries/asset.repository.sql | 1 + server/src/repositories/asset.repository.ts | 4 +- .../repositories/asset.repository.spec.ts | 57 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 632fb823c6..a74a05f466 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -438,6 +438,7 @@ with and "stack"."primaryAssetId" != "asset"."id" ) order by + (asset."localDateTime" AT TIME ZONE 'UTC')::date desc, "asset"."fileCreatedAt" desc ), "agg" as ( diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index e971a995e6..82534dbfa3 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -744,6 +744,7 @@ export class AssetRepository { params: [DummyValue.TIME_BUCKET, { withStacked: true }, { user: { id: DummyValue.UUID } }], }) getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) { + const order = options.order ?? 'desc'; const query = this.db .with('cte', (qb) => qb @@ -841,7 +842,8 @@ export class AssetRepository { ) .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) - .orderBy('asset.fileCreatedAt', options.order ?? 'desc'), + .orderBy(sql`(asset."localDateTime" AT TIME ZONE 'UTC')::date`, order) + .orderBy('asset.fileCreatedAt', order), ) .with('agg', (qb) => qb diff --git a/server/test/medium/specs/repositories/asset.repository.spec.ts b/server/test/medium/specs/repositories/asset.repository.spec.ts index 97f503e9ed..896489672e 100644 --- a/server/test/medium/specs/repositories/asset.repository.spec.ts +++ b/server/test/medium/specs/repositories/asset.repository.spec.ts @@ -1,9 +1,11 @@ import { Kysely } from 'kysely'; +import { AssetOrder, AssetVisibility } from 'src/enum'; import { AssetRepository } from 'src/repositories/asset.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { DB } from 'src/schema'; import { BaseService } from 'src/services/base.service'; import { newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; @@ -22,6 +24,61 @@ beforeAll(async () => { }); describe(AssetRepository.name, () => { + describe('getTimeBucket', () => { + it('should order assets by local day first and fileCreatedAt within each day', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const [{ asset: previousLocalDayAsset }, { asset: nextLocalDayEarlierAsset }, { asset: nextLocalDayLaterAsset }] = + await Promise.all([ + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-08T22:30:00.000Z'), + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-08T23:30:00.000Z'), + localDateTime: new Date('2026-03-09T01:30:00.000Z'), + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-08T23:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + }), + ]); + + await Promise.all([ + ctx.newExif({ assetId: previousLocalDayAsset.id, timeZone: 'UTC-2' }), + ctx.newExif({ assetId: nextLocalDayEarlierAsset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: nextLocalDayLaterAsset.id, timeZone: 'UTC+2' }), + ]); + + const descendingBucket = await sut.getTimeBucket( + '2026-03-01', + { order: AssetOrder.Desc, userIds: [user.id], visibility: AssetVisibility.Timeline }, + auth, + ); + expect(JSON.parse(descendingBucket.assets)).toEqual( + expect.objectContaining({ + id: [nextLocalDayLaterAsset.id, nextLocalDayEarlierAsset.id, previousLocalDayAsset.id], + }), + ); + + const ascendingBucket = await sut.getTimeBucket( + '2026-03-01', + { order: AssetOrder.Asc, userIds: [user.id], visibility: AssetVisibility.Timeline }, + auth, + ); + expect(JSON.parse(ascendingBucket.assets)).toEqual( + expect.objectContaining({ + id: [previousLocalDayAsset.id, nextLocalDayEarlierAsset.id, nextLocalDayLaterAsset.id], + }), + ); + }); + }); + describe('upsertExif', () => { it('should append to locked columns', async () => { const { ctx, sut } = setup();