refactor(server): asset stats (#3253)

* refactor(server): asset stats

* chore: open api
This commit is contained in:
Jason Rasmussen
2023-07-14 09:30:17 -04:00
committed by GitHub
parent 05e1a6d949
commit f952bc0b64
29 changed files with 601 additions and 844 deletions

View File

@@ -1,6 +1,13 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { Paginated, PaginationOptions } from '../domain.util';
export type AssetStats = Record<AssetType, number>;
export interface AssetStatsOptions {
isFavorite?: boolean;
isArchived?: boolean;
}
export interface AssetSearchOptions {
isVisible?: boolean;
type?: AssetType;
@@ -55,4 +62,5 @@ export interface IAssetRepository {
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
}

View File

@@ -1,3 +1,4 @@
import { AssetType } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common';
import {
assetEntityStub,
@@ -10,9 +11,9 @@ import {
import { when } from 'jest-when';
import { Readable } from 'stream';
import { IStorageRepository } from '../storage';
import { IAssetRepository } from './asset.repository';
import { AssetStats, IAssetRepository } from './asset.repository';
import { AssetService } from './asset.service';
import { DownloadResponseDto } from './index';
import { AssetStatsResponseDto, DownloadResponseDto } from './dto';
import { mapAsset } from './response-dto';
const downloadResponse: DownloadResponseDto = {
@@ -25,6 +26,19 @@ const downloadResponse: DownloadResponseDto = {
],
};
const stats: AssetStats = {
[AssetType.IMAGE]: 10,
[AssetType.VIDEO]: 23,
[AssetType.AUDIO]: 0,
[AssetType.OTHER]: 0,
};
const statResponse: AssetStatsResponseDto = {
images: 10,
videos: 23,
total: 33,
};
describe(AssetService.name, () => {
let sut: AssetService;
let accessMock: IAccessRepositoryMock;
@@ -287,4 +301,30 @@ describe(AssetService.name, () => {
});
});
});
describe('getStatistics', () => {
it('should get the statistics for a user, excluding archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: false });
});
it('should get the statistics for a user for archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: true });
});
it('should get the statistics for a user for favorite assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isFavorite: true });
});
it('should get the statistics for a user for all assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {});
});
});
});

View File

@@ -9,6 +9,7 @@ import { HumanReadableSize, usePagination } from '../domain.util';
import { ImmichReadStream, IStorageRepository } from '../storage';
import { IAssetRepository } from './asset.repository';
import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
import { MapMarkerDto } from './dto/map-marker.dto';
import { mapAsset, MapMarkerResponseDto } from './response-dto';
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
@@ -155,4 +156,9 @@ export class AssetService {
throw new BadRequestException('assetIds, albumId, or userId is required');
}
async getStatistics(authUser: AuthUserDto, dto: AssetStatsDto) {
const stats = await this.assetRepository.getStatistics(authUser.id, dto);
return mapStats(stats);
}
}

View File

@@ -0,0 +1,37 @@
import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { toBoolean } from '../../domain.util';
import { AssetStats } from '../asset.repository';
export class AssetStatsDto {
@IsBoolean()
@Transform(toBoolean)
@IsOptional()
isArchived?: boolean;
@IsBoolean()
@Transform(toBoolean)
@IsOptional()
isFavorite?: boolean;
}
export class AssetStatsResponseDto {
@ApiProperty({ type: 'integer' })
images!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer' })
total!: number;
}
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
return {
images: stats[AssetType.IMAGE],
videos: stats[AssetType.VIDEO],
total: Object.values(stats).reduce((total, value) => total + value, 0),
};
};

View File

@@ -1,4 +1,5 @@
export * from './asset-ids.dto';
export * from './asset-statistics.dto';
export * from './download.dto';
export * from './map-marker.dto';
export * from './memory-lane.dto';