Show on-this-day memories from the last 20 years with automatic playback
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user