feat(server): random assets API (#4184)

* feat(server): get random assets API

* Fix tests

* Use correct validation annotation

* Fix offset use in query

* Update API specs

* Fix typo

* Random assets e2e tests

* Improve e2e tests
This commit is contained in:
Daniele Ricci 2023-09-23 17:28:55 +02:00 committed by GitHub
parent fc64be6603
commit 014d164d99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 429 additions and 3 deletions

View file

@ -78,6 +78,7 @@ export interface IAssetRepository {
getByUserId(pagination: PaginationOptions, userId: string): Paginated<AssetEntity>;
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity>;
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getByLibraryId(libraryIds: string[]): Promise<AssetEntity[]>;

View file

@ -284,6 +284,11 @@ export class AssetService {
return mapStats(stats);
}
async getRandom(authUser: AuthUserDto, count: number): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.getRandom(authUser.id, count);
return assets.map((a) => mapAsset(a));
}
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);

View file

@ -1,4 +1,5 @@
import { IsBoolean, IsString } from 'class-validator';
import { Type } from 'class-transformer';
import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator';
import { Optional } from '../../domain.util';
import { BulkIdsDto } from '../response-dto';
@ -25,3 +26,11 @@ export class UpdateAssetDto {
@IsString()
description?: string;
}
export class RandomAssetsDto {
@Optional()
@IsInt()
@IsPositive()
@Type(() => Number)
count?: number;
}

View file

@ -13,6 +13,7 @@ import {
MapMarkerResponseDto,
MemoryLaneDto,
MemoryLaneResponseDto,
RandomAssetsDto,
TimeBucketAssetDto,
TimeBucketDto,
TimeBucketResponseDto,
@ -41,6 +42,11 @@ export class AssetController {
return this.service.getMemoryLane(authUser, dto);
}
@Get('random')
getRandom(@AuthUser() authUser: AuthUserDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(authUser, dto.count ?? 1);
}
@SharedLinkRoute()
@Post('download/info')
getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {

View file

@ -429,6 +429,17 @@ export class AssetRepository implements IAssetRepository {
return result;
}
getRandom(ownerId: string, count: number): Promise<AssetEntity[]> {
// can't use queryBuilder because of custom OFFSET clause
return this.repository.query(
`SELECT *
FROM assets
WHERE "ownerId" = $1
OFFSET FLOOR(RANDOM() * (SELECT GREATEST(COUNT(*) - 1, 0) FROM ASSETS)) LIMIT $2`,
[ownerId, count],
);
}
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
const truncateValue = truncateMap[options.size];