Skip to content

Commit

Permalink
add test cases for getMemoryLane
Browse files Browse the repository at this point in the history
  • Loading branch information
ExceptionsOccur committed Jan 19, 2025
1 parent 8ab8387 commit 3eb022f
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 91 deletions.
165 changes: 82 additions & 83 deletions server/src/repositories/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,92 +86,91 @@ export class AssetRepository implements IAssetRepository {

@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
getByDayOfYear(ownerIds: string[], albumIds: string[], { day, month }: MonthDay): Promise<DayOfYearAssets[]> {
return this.db
.with('res', (qb) =>
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 assets)`,
sql`date_part('year', current_date)::int - 1`,
])
.as('year'),
)
.select((eb) => eb.fn('make_date', [sql`year::int`, sql`${month}::int`, sql`${day}::int`]).as('date')),
)
.selectFrom('today')
.innerJoinLateral(
(qb) =>
return (
this.db
.with('res', (qb) =>
qb
.with('today', (qb) =>
qb
.selectFrom('assets')
.selectAll('assets')
.innerJoin('asset_job_status', 'assets.id', 'asset_job_status.assetId')
.where('asset_job_status.previewAt', 'is not', null)
.where(sql`(assets."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
.where(({eb, and, or}) => or([
// assets in share albums and not ownerIds own
and([
eb.exists((qb)=>
.selectFrom((eb) =>
eb
.fn('generate_series', [
sql`(select date_part('year', min(("localDateTime" at time zone 'UTC')::date))::int from assets)`,
sql`date_part('year', current_date)::int - 1`,
])
.as('year'),
)
.select((eb) => eb.fn('make_date', [sql`year::int`, sql`${month}::int`, sql`${day}::int`]).as('date')),
)
.selectFrom('today')
.innerJoinLateral(
(qb) =>
qb
.selectFrom('assets')
.selectAll('assets')
.innerJoin('asset_job_status', 'assets.id', 'asset_job_status.assetId')
.where('asset_job_status.previewAt', 'is not', null)
.where(sql`(assets."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
.where(({ eb, and, or }) =>
or([
// assets in share albums and not ownerIds own
and([
eb.exists((qb) =>
qb
.selectFrom('albums_assets_assets')
.whereRef('albums_assets_assets.assetsId', '=', 'assets.id')
.where('albums_assets_assets.albumsId', '=', anyUuid(albumIds)),
),
eb('assets.ownerId', '!=', anyUuid(ownerIds)),
]),
// ownerIds own, include partnerIds
eb('assets.ownerId', '=', anyUuid(ownerIds)),
]),
)
.where('assets.isVisible', '=', true)
.where('assets.isArchived', '=', false)
.where((eb) =>
eb.exists((qb) =>
qb
.selectFrom('albums_assets_assets')
.whereRef('albums_assets_assets.assetsId', '=', 'assets.id')
.where('albums_assets_assets.albumsId', '=', anyUuid(albumIds))
.selectFrom('asset_files')
.whereRef('assetId', '=', 'assets.id')
.where('asset_files.type', '=', AssetFileType.PREVIEW),
),
eb('assets.ownerId', '!=', anyUuid(ownerIds))
]),
// ownerIds own, include partnerIds
eb('assets.ownerId', '=', anyUuid(ownerIds))
]))
.where('assets.isVisible', '=', true)
.where('assets.isArchived', '=', false)
.where((eb) =>
eb.exists((qb) =>
qb
.selectFrom('asset_files')
.whereRef('assetId', '=', 'assets.id')
.where('asset_files.type', '=', AssetFileType.PREVIEW),
),
)
.where('assets.deletedAt', 'is', null)
.limit(10)
.as('a'),
(join) => join.onTrue(),
)
.innerJoin('exif', 'a.id', 'exif.assetId')
.selectAll('a')
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')),
)
// If the same photo is uploaded twice by user A and user B respectively,
// when the photo appears in the sharing, the result will show duplicates
.with('unique_ids', (qb)=>
qb
.selectFrom('res')
.select([
'id',
sql`row_number() over (partition by checksum order by case when id = ${ownerIds[0]} then 1 else 2 end)`.as('rn')]),
)
.selectFrom('res')
.select(
sql<number>`((now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date) / 365`.as(
'yearsAgo',
),
)
.select((eb) => eb.fn('jsonb_agg', [eb.table('res')]).as('assets'))
.where((eb)=>
eb
.exists((qb)=>
qb
.selectFrom('unique_ids')
.whereRef('id', '=', 'res.id')
.where('rn', '=', 1)
)
)
.groupBy(sql`("localDateTime" at time zone 'UTC')::date`)
.orderBy(sql`("localDateTime" at time zone 'UTC')::date`, 'desc')
.limit(10)
.execute() as any as Promise<DayOfYearAssets[]>;
)
.where('assets.deletedAt', 'is', null)
.limit(10)
.as('a'),
(join) => join.onTrue(),
)
.innerJoin('exif', 'a.id', 'exif.assetId')
.selectAll('a')
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')),
)
// If the same photo is uploaded twice by user A and user B respectively,
// when the photo appears in the sharing, the result will show duplicates
.with('unique_ids', (qb) =>
qb
.selectFrom('res')
.select([
'id',
sql`row_number() over (partition by checksum order by case when id = ${ownerIds[0]} then 1 else 2 end)`.as(
'rn',
),
]),
)
.selectFrom('res')
.select(
sql<number>`((now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date) / 365`.as(
'yearsAgo',
),
)
.select((eb) => eb.fn('jsonb_agg', [eb.table('res')]).as('assets'))
.where((eb) => eb.exists((qb) => qb.selectFrom('unique_ids').whereRef('id', '=', 'res.id').where('rn', '=', 1)))
.groupBy(sql`("localDateTime" at time zone 'UTC')::date`)
.orderBy(sql`("localDateTime" at time zone 'UTC')::date`, 'desc')
.limit(10)
.execute() as any as Promise<DayOfYearAssets[]>
);
}

@GenerateSql({ params: [[DummyValue.UUID]] })
Expand Down
90 changes: 83 additions & 7 deletions server/src/services/asset.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { BadRequestException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
Expand All @@ -12,6 +15,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AssetService } from 'src/services/asset.service';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { faceStub } from 'test/fixtures/face.stub';
Expand Down Expand Up @@ -39,6 +43,7 @@ describe(AssetService.name, () => {

let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let albumMock: Mocked<IAlbumRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
let partnerMock: Mocked<IPartnerRepository>;
Expand All @@ -55,7 +60,7 @@ describe(AssetService.name, () => {
};

beforeEach(() => {
({ sut, accessMock, assetMock, eventMock, jobMock, partnerMock, stackMock, systemMock, userMock } =
({ sut, accessMock, assetMock, albumMock, eventMock, jobMock, partnerMock, stackMock, systemMock, userMock } =
newTestService(AssetService));

mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]);
Expand All @@ -72,10 +77,79 @@ describe(AssetService.name, () => {
});

it('should group the assets correctly', async () => {
const image1 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 0, 0, 0) };
const image2 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 1, 0, 0) };
const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) };
const image4 = { ...assetStub.image, localDateTime: new Date(2009, 1, 15) };
// image1 same as image5
// image2 same as image6
// image3 same as image7
// image4 not in any album
const image1 = {
...assetStub.image,
id: '1',
isFavorite: false,
localDateTime: new Date(2023, 1, 15, 0, 0, 0),
ownerId: userStub.admin.id,
};
const image2 = {
...assetStub.image1,
id: '2',
isFavorite: false,
localDateTime: new Date(2023, 1, 15, 1, 0, 0),
ownerId: userStub.admin.id,
};
const image3 = {
...assetStub.noResizePath,
id: '3',
isFavorite: false,
localDateTime: new Date(2016, 1, 15, 1, 0, 0),
ownerId: userStub.admin.id,
};
const image4 = {
...assetStub.noWebpPath,
id: '4',
isFavorite: false,
localDateTime: new Date(2010, 1, 15, 0, 0, 0),
ownerId: userStub.admin.id,
};
const image5 = {
...assetStub.image,
id: '5',
isFavorite: false,
localDateTime: new Date(2023, 1, 15, 0, 0, 0),
ownerId: userStub.user1.id,
};
const image6 = {
...assetStub.image1,
id: '6',
isFavorite: false,
localDateTime: new Date(2023, 1, 15, 0, 0, 0),
ownerId: userStub.user1.id,
};
const image7 = {
...assetStub.noResizePath,
id: '7',
isFavorite: false,
localDateTime: new Date(2016, 1, 15, 0, 0, 0),
ownerId: userStub.user2.id,
};
const album1: Readonly<AlbumEntity> = {
...albumStub.sharedWithMultiple,
id: 'a1',
assets: [image1, image2, image3],
};
const album2: Readonly<AlbumEntity> = {
...albumStub.sharedWithAdmin,
id: 'a2',
ownerId: userStub.user1.id,
owner: userStub.user1,
assets: [image5, image6],
};
const album3: Readonly<AlbumEntity> = {
...albumStub.sharedWithAdmin,
id: 'a3',
ownerId: userStub.user2.id,
owner: userStub.user2,
assets: [image7],
};
albumMock.getShared.mockResolvedValue([album1, album2, album3]);

partnerMock.getAll.mockResolvedValue([]);
assetMock.getByDayOfYear.mockResolvedValue([
Expand All @@ -99,7 +173,9 @@ describe(AssetService.name, () => {
{ yearsAgo: 15, title: '15 years ago', assets: [mapAsset(image4)] },
]);

expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
expect(assetMock.getByDayOfYear.mock.calls).toEqual([
[[authStub.admin.user.id], ['a1', 'a2', 'a3'], { day: 15, month: 1 }],
]);
});

it('should get memories with partners with inTimeline enabled', async () => {
Expand All @@ -109,7 +185,7 @@ describe(AssetService.name, () => {
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });

expect(assetMock.getByDayOfYear.mock.calls).toEqual([
[[authStub.admin.user.id, userStub.user1.id], { day: 15, month: 1 }],
[[authStub.admin.user.id, userStub.user1.id], [], { day: 15, month: 1 }],
]);
});
});
Expand Down
6 changes: 5 additions & 1 deletion server/src/services/asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ export class AssetService extends BaseService {
const userIds = [auth.user.id, ...partnerIds];
const shareAlbums = await this.albumRepository.getShared(auth.user.id);

const groups = await this.assetRepository.getByDayOfYear(userIds, shareAlbums.length > 0 ? shareAlbums.map(el=>el.id) : [], dto);
const groups = await this.assetRepository.getByDayOfYear(
userIds,
shareAlbums?.length > 0 ? shareAlbums.map((el) => el.id) : [],
dto,
);
return groups.map(({ yearsAgo, assets }) => ({
yearsAgo,
// TODO move this to clients
Expand Down

0 comments on commit 3eb022f

Please sign in to comment.