Show on-this-day memories from the last 20 years with automatic playback

This commit is contained in:
Codex
2026-10-18 19:00:06 +03:00
parent 04a7b4104f
commit ca7766fcd6
7 changed files with 526 additions and 48 deletions
+5 -16
View File
@@ -109,18 +109,7 @@ with
select
make_date(year::int, $1::int, $2::int) as "date"
from
generate_series(
(
select
date_part(
'year',
min(("localDateTime" at time zone 'UTC')::date)
)::int
from
asset
),
$3
) as "year"
generate_series($3::int, $4::int) as "year"
)
select
"a".*
@@ -135,21 +124,21 @@ with
inner join "asset_job_status" on "asset"."id" = "asset_job_status"."assetId"
where
(asset."localDateTime" at time zone 'UTC')::date = today.date
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."visibility" = $5
and "asset"."ownerId" = any ($5::uuid[])
and "asset"."visibility" = $6
and exists (
select
from
"asset_file"
where
"assetId" = "asset"."id"
and "asset_file"."type" = $6
and "asset_file"."type" = $7
)
and "asset"."deletedAt" is null
order by
(asset."localDateTime" at time zone 'UTC')::date desc
limit
$7
$8
) as "a" on true
)
select
+13 -9
View File
@@ -464,15 +464,19 @@ export class AssetRepository {
qb
.with('today', (qb) =>
qb
.selectFrom((eb) =>
eb
.fn('generate_series', [
sql`(select date_part('year', min(("localDateTime" at time zone 'UTC')::date))::int from asset)`,
sql`${year - 1}`,
])
.as('year'),
)
.select((eb) => eb.fn('make_date', [sql`year::int`, sql`${month}::int`, sql`${day}::int`]).as('date')),
.selectFrom((eb) => eb.fn('generate_series', [sql`${year - 20}::int`, sql`${year - 1}::int`]).as('year'))
.select((eb) => {
const date = eb.fn('make_date', [sql`year::int`, sql`${month}::int`, sql`${day}::int`]);
return (
month === 2 && day === 29
? eb
.case()
.when(sql<boolean>`year::int % 4 = 0 and (year::int % 100 != 0 or year::int % 400 = 0)`)
.then(date)
.end()
: date
).as('date');
}),
)
.selectFrom('today')
.innerJoinLateral(
@@ -1,5 +1,5 @@
import { Kysely } from 'kysely';
import { AssetOrder, AssetOrderBy, AssetVisibility } from 'src/enum.js';
import { AssetFileType, AssetOrder, AssetOrderBy, AssetVisibility } from 'src/enum.js';
import { AssetRepository } from 'src/repositories/asset.repository.js';
import { LoggingRepository } from 'src/repositories/logging.repository.js';
import { DB } from 'src/schema/index.js';
@@ -64,6 +64,67 @@ const keyframeRow = (assetId: string, n: number) => ({
});
describe(AssetRepository.name, () => {
describe('getByDayOfYear', () => {
it('should include every year from one through twenty years ago on the same local calendar day', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: otherUser } = await ctx.newUser();
const target = { year: 2026, month: 10, day: 18 };
const expectedAssetIds = [];
for (let yearsAgo = 0; yearsAgo <= 21; yearsAgo++) {
const { asset } = await ctx.newAsset({
ownerId: user.id,
localDateTime: `${target.year - yearsAgo}-10-18T12:00:00.000Z`,
});
await ctx.newJobStatus({ assetId: asset.id });
await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: '/preview.jpg' });
if (yearsAgo >= 1 && yearsAgo <= 20) {
expectedAssetIds.push(asset.id);
}
}
for (const dto of [
{ ownerId: user.id, localDateTime: '2025-10-17T23:59:59.999Z' },
{ ownerId: user.id, localDateTime: '2025-10-19T00:00:00.000Z' },
{ ownerId: otherUser.id, localDateTime: '2025-10-18T12:00:00.000Z' },
]) {
const { asset } = await ctx.newAsset(dto);
await ctx.newJobStatus({ assetId: asset.id });
await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: '/preview.jpg' });
}
const memories = await sut.getByDayOfYear([user.id], target);
expect(memories.map(({ year }) => year)).toEqual(Array.from({ length: 20 }, (_, index) => 2025 - index));
expect(memories.flatMap(({ assets }) => assets.map(({ id }) => id))).toEqual(expectedAssetIds);
});
it.each([2028, 2104])('should skip non-leap years on February 29th in %i', async (year) => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const expectedYears = [];
for (let yearsAgo = 1; yearsAgo <= 20; yearsAgo++) {
const assetYear = year - yearsAgo;
const isLeapYear = assetYear % 4 === 0 && (assetYear % 100 !== 0 || assetYear % 400 === 0);
const { asset } = await ctx.newAsset({
ownerId: user.id,
localDateTime: `${assetYear}-${isLeapYear ? '02-29' : '03-01'}T12:00:00.000Z`,
});
await ctx.newJobStatus({ assetId: asset.id });
await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: '/preview.jpg' });
if (isLeapYear) {
expectedYears.push(assetYear);
}
}
const memories = await sut.getByDayOfYear([user.id], { year, month: 2, day: 29 });
expect(memories.map(({ year }) => year)).toEqual(expectedYears);
});
});
describe('getTimeBucket', () => {
it('should order assets by local day first and fileCreatedAt within each day', async () => {
const { ctx, sut } = setup();