From 0db259f4c218383cca3effbbe7f1e7ab9db0eb20 Mon Sep 17 00:00:00 2001 From: "andy :)" Date: Fri, 3 Jul 2026 09:53:56 -0400 Subject: [PATCH 1/4] Update sorting Updates the sorting to sort 1. First by timeCreatedAt, but then 2. By filename. --- server/src/repositories/asset.repository.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ca00245c27..cd8e98481b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -904,7 +904,8 @@ export class AssetRepository { : sql`(asset."localDateTime" AT TIME ZONE 'UTC')::date`, order, ) - .orderBy('asset.fileCreatedAt', order), + .orderBy('asset.fileCreatedAt', order) + .orderBy('asset.originalFileName', order), ) .with('agg', (qb) => qb From f96e01c24cde4e362673b75d56d2eb3a5eee6aac Mon Sep 17 00:00:00 2001 From: "andy :)" Date: Fri, 3 Jul 2026 11:30:57 -0400 Subject: [PATCH 2/4] Push tsts --- .../repositories/asset.repository.spec.ts | 175 +++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/server/test/medium/specs/repositories/asset.repository.spec.ts b/server/test/medium/specs/repositories/asset.repository.spec.ts index 2e449ae801..d37fc0c2bb 100644 --- a/server/test/medium/specs/repositories/asset.repository.spec.ts +++ b/server/test/medium/specs/repositories/asset.repository.spec.ts @@ -1,5 +1,5 @@ import { Kysely } from 'kysely'; -import { AssetOrder, AssetVisibility } from 'src/enum'; +import { AssetOrder, AssetOrderBy, AssetVisibility } from 'src/enum'; import { AssetRepository } from 'src/repositories/asset.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { DB } from 'src/schema'; @@ -77,6 +77,179 @@ describe(AssetRepository.name, () => { }), ); }); + + it('should order assets by originalFileName when fileCreatedAt is the same (takenAt)', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + // create all the fake photos + const [ + { asset: time1DSC0001Asset }, + { asset: time1DSC0002Asset }, + { asset: time2DSC0003Asset }, + { asset: time2DSC0004Asset }] = + await Promise.all([ + // both at 12:30AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0001.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0002.jpg', + }), + // both at 1:45AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0003.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0004.jpg', + }) + ]); + + // even though im not gonna do anything with these it's required! + await Promise.all([ + ctx.newExif({ assetId: time1DSC0001Asset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: time1DSC0002Asset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: time2DSC0003Asset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: time2DSC0004Asset.id, timeZone: 'UTC+2' }), + ]); + + // check the values given by the bucket when descending + const descendingBucket = await sut.getTimeBucket( + '2026-03-01', + { order: AssetOrder.Desc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.TakenAt }, + auth, + ); + // make sure they're ordered correctly + expect(JSON.parse(descendingBucket.assets)).toEqual( + expect.objectContaining({ + id: [ + time2DSC0004Asset.id, + time2DSC0003Asset.id, + time1DSC0002Asset.id, + time1DSC0001Asset.id + ], + }), + ); + + // now do the same when ascending + const ascendingBucket = await sut.getTimeBucket( + '2026-03-01', + { order: AssetOrder.Asc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.TakenAt }, + auth, + ); + expect(JSON.parse(ascendingBucket.assets)).toEqual( + expect.objectContaining({ + id: [ + time1DSC0001Asset.id, + time1DSC0002Asset.id, + time2DSC0003Asset.id, + time2DSC0004Asset.id + ], + }), + ); + }); + + it('should order assets by originalFileName when fileCreatedAt is the same (createdAt)', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + // create all the fake photos + const [ + { asset: time1DSC0001Asset }, + { asset: time1DSC0002Asset }, + { asset: time2DSC0003Asset }, + { asset: time2DSC0004Asset }] = + await Promise.all([ + // createdAt = uploadedAt, fileCreatedAt = file metadata + // both at 12:30AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + createdAt: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0001.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + createdAt: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0002.jpg', + }), + // both at 1:45AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + createdAt: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0003.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + createdAt: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0004.jpg', + }) + ]); + + // even though im not gonna do anything with these it's required! + await Promise.all([ + ctx.newExif({ assetId: time1DSC0001Asset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: time1DSC0002Asset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: time2DSC0003Asset.id, timeZone: 'UTC+2' }), + ctx.newExif({ assetId: time2DSC0004Asset.id, timeZone: 'UTC+2' }), + ]); + + // check the values given by the bucket when descending + const descendingBucket = await sut.getTimeBucket( + '2026-03-01', + { order: AssetOrder.Desc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.CreatedAt }, + auth, + ); + // make sure they're ordered correctly + expect(JSON.parse(descendingBucket.assets)).toEqual( + expect.objectContaining({ + id: [ + time2DSC0004Asset.id, + time2DSC0003Asset.id, + time1DSC0002Asset.id, + time1DSC0001Asset.id + ], + }), + ); + + // now do the same when ascending + const ascendingBucket = await sut.getTimeBucket( + '2026-03-01', + { order: AssetOrder.Asc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.CreatedAt }, + auth, + ); + expect(JSON.parse(ascendingBucket.assets)).toEqual( + expect.objectContaining({ + id: [ + time1DSC0001Asset.id, + time1DSC0002Asset.id, + time2DSC0003Asset.id, + time2DSC0004Asset.id + ], + }), + ); + }); }); describe('upsertExif', () => { From 025ea03da1df5b0474be760821217b40ccf7aef1 Mon Sep 17 00:00:00 2001 From: "andy :)" Date: Tue, 7 Jul 2026 15:20:33 -0400 Subject: [PATCH 3/4] Sync SQL --- server/src/queries/asset.repository.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index cc694dd63a..a3dc9f89aa 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -444,7 +444,8 @@ with ) order by (asset."localDateTime" AT TIME ZONE 'UTC')::date desc, - "asset"."fileCreatedAt" desc + "asset"."fileCreatedAt" desc, + "asset"."originalFileName" desc ), "agg" as ( select From c4a6357a88686a63e6c3e94bfdb02a2a63640148 Mon Sep 17 00:00:00 2001 From: "andy :)" Date: Tue, 7 Jul 2026 15:20:42 -0400 Subject: [PATCH 4/4] Formatting fixes --- .../repositories/asset.repository.spec.ts | 194 +++++++++--------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/server/test/medium/specs/repositories/asset.repository.spec.ts b/server/test/medium/specs/repositories/asset.repository.spec.ts index d37fc0c2bb..bd6d89aa31 100644 --- a/server/test/medium/specs/repositories/asset.repository.spec.ts +++ b/server/test/medium/specs/repositories/asset.repository.spec.ts @@ -85,39 +85,39 @@ describe(AssetRepository.name, () => { // create all the fake photos const [ - { asset: time1DSC0001Asset }, - { asset: time1DSC0002Asset }, + { asset: time1DSC0001Asset }, + { asset: time1DSC0002Asset }, { asset: time2DSC0003Asset }, - { asset: time2DSC0004Asset }] = - await Promise.all([ - // both at 12:30AM - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), - localDateTime: new Date('2026-03-09T00:30:00.000Z'), - originalFileName: 'DSC0001.jpg', - }), - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), - localDateTime: new Date('2026-03-09T00:30:00.000Z'), - originalFileName: 'DSC0002.jpg', - }), - // both at 1:45AM - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), - localDateTime: new Date('2026-03-09T01:45:00.000Z'), - originalFileName: 'DSC0003.jpg', - }), - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), - localDateTime: new Date('2026-03-09T01:45:00.000Z'), - originalFileName: 'DSC0004.jpg', - }) - ]); - + { asset: time2DSC0004Asset }, + ] = await Promise.all([ + // both at 12:30AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0001.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0002.jpg', + }), + // both at 1:45AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0003.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0004.jpg', + }), + ]); + // even though im not gonna do anything with these it's required! await Promise.all([ ctx.newExif({ assetId: time1DSC0001Asset.id, timeZone: 'UTC+2' }), @@ -129,35 +129,35 @@ describe(AssetRepository.name, () => { // check the values given by the bucket when descending const descendingBucket = await sut.getTimeBucket( '2026-03-01', - { order: AssetOrder.Desc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.TakenAt }, + { + order: AssetOrder.Desc, + userIds: [user.id], + visibility: AssetVisibility.Timeline, + orderBy: AssetOrderBy.TakenAt, + }, auth, ); // make sure they're ordered correctly expect(JSON.parse(descendingBucket.assets)).toEqual( expect.objectContaining({ - id: [ - time2DSC0004Asset.id, - time2DSC0003Asset.id, - time1DSC0002Asset.id, - time1DSC0001Asset.id - ], + id: [time2DSC0004Asset.id, time2DSC0003Asset.id, time1DSC0002Asset.id, time1DSC0001Asset.id], }), ); // now do the same when ascending const ascendingBucket = await sut.getTimeBucket( '2026-03-01', - { order: AssetOrder.Asc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.TakenAt }, + { + order: AssetOrder.Asc, + userIds: [user.id], + visibility: AssetVisibility.Timeline, + orderBy: AssetOrderBy.TakenAt, + }, auth, ); expect(JSON.parse(ascendingBucket.assets)).toEqual( expect.objectContaining({ - id: [ - time1DSC0001Asset.id, - time1DSC0002Asset.id, - time2DSC0003Asset.id, - time2DSC0004Asset.id - ], + id: [time1DSC0001Asset.id, time1DSC0002Asset.id, time2DSC0003Asset.id, time2DSC0004Asset.id], }), ); }); @@ -169,44 +169,44 @@ describe(AssetRepository.name, () => { // create all the fake photos const [ - { asset: time1DSC0001Asset }, - { asset: time1DSC0002Asset }, + { asset: time1DSC0001Asset }, + { asset: time1DSC0002Asset }, { asset: time2DSC0003Asset }, - { asset: time2DSC0004Asset }] = - await Promise.all([ - // createdAt = uploadedAt, fileCreatedAt = file metadata - // both at 12:30AM - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), - localDateTime: new Date('2026-03-09T00:30:00.000Z'), - createdAt: new Date('2026-03-09T00:30:00.000Z'), - originalFileName: 'DSC0001.jpg', - }), - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), - localDateTime: new Date('2026-03-09T00:30:00.000Z'), - createdAt: new Date('2026-03-09T00:30:00.000Z'), - originalFileName: 'DSC0002.jpg', - }), - // both at 1:45AM - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), - localDateTime: new Date('2026-03-09T01:45:00.000Z'), - createdAt: new Date('2026-03-09T01:45:00.000Z'), - originalFileName: 'DSC0003.jpg', - }), - ctx.newAsset({ - ownerId: user.id, - fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), - localDateTime: new Date('2026-03-09T01:45:00.000Z'), - createdAt: new Date('2026-03-09T01:45:00.000Z'), - originalFileName: 'DSC0004.jpg', - }) - ]); - + { asset: time2DSC0004Asset }, + ] = await Promise.all([ + // createdAt = uploadedAt, fileCreatedAt = file metadata + // both at 12:30AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + createdAt: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0001.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T00:30:00.000Z'), + localDateTime: new Date('2026-03-09T00:30:00.000Z'), + createdAt: new Date('2026-03-09T00:30:00.000Z'), + originalFileName: 'DSC0002.jpg', + }), + // both at 1:45AM + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + createdAt: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0003.jpg', + }), + ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: new Date('2026-03-09T01:45:00.000Z'), + localDateTime: new Date('2026-03-09T01:45:00.000Z'), + createdAt: new Date('2026-03-09T01:45:00.000Z'), + originalFileName: 'DSC0004.jpg', + }), + ]); + // even though im not gonna do anything with these it's required! await Promise.all([ ctx.newExif({ assetId: time1DSC0001Asset.id, timeZone: 'UTC+2' }), @@ -218,35 +218,35 @@ describe(AssetRepository.name, () => { // check the values given by the bucket when descending const descendingBucket = await sut.getTimeBucket( '2026-03-01', - { order: AssetOrder.Desc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.CreatedAt }, + { + order: AssetOrder.Desc, + userIds: [user.id], + visibility: AssetVisibility.Timeline, + orderBy: AssetOrderBy.CreatedAt, + }, auth, ); // make sure they're ordered correctly expect(JSON.parse(descendingBucket.assets)).toEqual( expect.objectContaining({ - id: [ - time2DSC0004Asset.id, - time2DSC0003Asset.id, - time1DSC0002Asset.id, - time1DSC0001Asset.id - ], + id: [time2DSC0004Asset.id, time2DSC0003Asset.id, time1DSC0002Asset.id, time1DSC0001Asset.id], }), ); // now do the same when ascending const ascendingBucket = await sut.getTimeBucket( '2026-03-01', - { order: AssetOrder.Asc, userIds: [user.id], visibility: AssetVisibility.Timeline, orderBy: AssetOrderBy.CreatedAt }, + { + order: AssetOrder.Asc, + userIds: [user.id], + visibility: AssetVisibility.Timeline, + orderBy: AssetOrderBy.CreatedAt, + }, auth, ); expect(JSON.parse(ascendingBucket.assets)).toEqual( expect.objectContaining({ - id: [ - time1DSC0001Asset.id, - time1DSC0002Asset.id, - time2DSC0003Asset.id, - time2DSC0004Asset.id - ], + id: [time1DSC0001Asset.id, time1DSC0002Asset.id, time2DSC0003Asset.id, time2DSC0004Asset.id], }), ); });