From 2c54b506b3cfcbe2829d0c539be46a9785c636cb Mon Sep 17 00:00:00 2001 From: Sergey Katsubo Date: Thu, 13 Nov 2025 00:38:03 +0300 Subject: [PATCH] fix(server): include the previous year in memories for January 1, 2, 3 (#23832) * Test memory creation in advance Use year 2035 to make sure it's in the future of current time of a test run * Use target year instead of current year when fetching assets during memory creation This fixes an edge case of creating memories in advance when target year is different from current year. Example: job runs on 2025-12-31 (current year is 2025) and creates memories to be shown on 2026-01-01 (target year is 2026). If using _current_ year in calculation then range of years is capped at (2025 - 1 = 2024) thus excluding 2025-01-01 from created memories. With _target_ year it is (2026 - 1 = 2025), so 2025-01-01 will be included in memories. * Update sql queries --- server/src/queries/asset.repository.sql | 10 ++--- server/src/repositories/asset.repository.ts | 9 +++-- .../specs/services/memory.service.spec.ts | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 23fd3caf3c..6cf3ec2f54 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -64,7 +64,7 @@ with from asset ), - date_part('year', current_date)::int - 1 + $3 ) as "year" ) select @@ -81,21 +81,21 @@ with where "asset_job_status"."previewAt" is not null and (asset."localDateTime" at time zone 'UTC')::date = today.date - and "asset"."ownerId" = any ($3::uuid[]) - and "asset"."visibility" = $4 + and "asset"."ownerId" = any ($4::uuid[]) + and "asset"."visibility" = $5 and exists ( select from "asset_file" where "assetId" = "asset"."id" - and "asset_file"."type" = $5 + and "asset_file"."type" = $6 ) and "asset"."deletedAt" is null order by (asset."localDateTime" at time zone 'UTC')::date desc limit - $6 + $7 ) as "a" on true inner join "asset_exif" on "a"."id" = "asset_exif"."assetId" ) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 8e793f9603..d3d9ada80f 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -73,9 +73,10 @@ export interface TimeBucketItem { count: number; } -export interface MonthDay { +export interface YearMonthDay { day: number; month: number; + year: number; } interface AssetExploreFieldOptions { @@ -259,8 +260,8 @@ export class AssetRepository { return this.db.insertInto('asset').values(assets).returningAll().execute(); } - @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) - getByDayOfYear(ownerIds: string[], { day, month }: MonthDay) { + @GenerateSql({ params: [DummyValue.UUID, { year: 2000, day: 1, month: 1 }] }) + getByDayOfYear(ownerIds: string[], { year, day, month }: YearMonthDay) { return this.db .with('res', (qb) => qb @@ -270,7 +271,7 @@ export class AssetRepository { eb .fn('generate_series', [ sql`(select date_part('year', min(("localDateTime" at time zone 'UTC')::date))::int from asset)`, - sql`date_part('year', current_date)::int - 1`, + sql`${year - 1}`, ]) .as('year'), ) diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index 12df2f130e..b3a3da6010 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -153,6 +153,46 @@ describe(MemoryService.name, () => { ); }); + it('should create a memory from an asset - in advance', async () => { + const { sut, ctx } = setup(); + const assetRepo = ctx.get(AssetRepository); + const memoryRepo = ctx.get(MemoryRepository); + const now = DateTime.fromObject({ year: 2035, month: 2, day: 26 }, { zone: 'utc' }) as DateTime; + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, localDateTime: now.minus({ years: 1 }).toISO() }); + await Promise.all([ + ctx.newExif({ assetId: asset.id, make: 'Canon' }), + ctx.newJobStatus({ assetId: asset.id }), + assetRepo.upsertFiles([ + { assetId: asset.id, type: AssetFileType.Preview, path: '/path/to/preview.jpg' }, + { assetId: asset.id, type: AssetFileType.Thumbnail, path: '/path/to/thumbnail.jpg' }, + ]), + ]); + + vi.setSystemTime(now.toJSDate()); + await sut.onMemoriesCreate(); + + const memories = await memoryRepo.search(user.id, {}); + expect(memories.length).toBe(1); + expect(memories[0]).toEqual( + expect.objectContaining({ + id: expect.any(String), + createdAt: expect.any(Date), + memoryAt: expect.any(Date), + updatedAt: expect.any(Date), + deletedAt: null, + ownerId: user.id, + assets: expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + isSaved: false, + showAt: now.startOf('day').toJSDate(), + hideAt: now.endOf('day').toJSDate(), + seenAt: null, + type: 'on_this_day', + data: { year: 2034 }, + }), + ); + }); + it('should not generate a memory twice for the same day', async () => { const { sut, ctx } = setup(); const assetRepo = ctx.get(AssetRepository);