feat(server): asset search endpoint (#4931)

* feat(server): GET /assets endpoint

* chore: open api

* chore: use dumb name

* feat: search by make, model, lens, city, state, country

* chore: open api

* chore: pagination validation and tests

* chore: pr feedback
This commit is contained in:
Jason Rasmussen
2023-11-14 17:47:15 -05:00
committed by GitHub
parent 7a8f8e5472
commit 753dab8b3c
24 changed files with 3151 additions and 94 deletions
+30
View File
@@ -30,6 +30,8 @@ import {
AssetIdsDto,
AssetJobName,
AssetJobsDto,
AssetOrder,
AssetSearchDto,
AssetStatsDto,
DownloadArchiveInfo,
DownloadInfoDto,
@@ -91,6 +93,34 @@ export class AssetService {
this.configCore = SystemConfigCore.create(configRepository);
}
search(authUser: AuthUserDto, dto: AssetSearchDto) {
let checksum: Buffer | undefined = undefined;
if (dto.checksum) {
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
checksum = Buffer.from(dto.checksum, encoding);
}
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;
const order = dto.order ? enumToOrder[dto.order] : undefined;
return this.assetRepository
.search({
...dto,
order,
checksum,
ownerId: authUser.id,
})
.then((assets) =>
assets.map((asset) =>
mapAsset(asset, {
stripMetadata: false,
withStack: true,
}),
),
);
}
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
this.access.requireUploadAccess(authUser);
+155 -2
View File
@@ -1,8 +1,161 @@
import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator';
import { Optional, ValidateUUID } from '../../domain.util';
import { IsBoolean, IsEnum, IsInt, IsPositive, IsString, Min } from 'class-validator';
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util';
import { BulkIdsDto } from '../response-dto';
export enum AssetOrder {
ASC = 'asc',
DESC = 'desc',
}
export class AssetSearchDto {
@ValidateUUID({ optional: true })
id?: string;
@ValidateUUID({ optional: true })
libraryId?: string;
@IsString()
@Optional()
deviceAssetId?: string;
@IsString()
@Optional()
deviceId?: string;
@IsEnum(AssetType)
@Optional()
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type?: AssetType;
@IsString()
@Optional()
checksum?: string;
@QueryBoolean({ optional: true })
isArchived?: boolean;
@QueryBoolean({ optional: true })
isEncoded?: boolean;
@QueryBoolean({ optional: true })
isExternal?: boolean;
@QueryBoolean({ optional: true })
isFavorite?: boolean;
@QueryBoolean({ optional: true })
isMotion?: boolean;
@QueryBoolean({ optional: true })
isOffline?: boolean;
@QueryBoolean({ optional: true })
isReadOnly?: boolean;
@QueryBoolean({ optional: true })
isVisible?: boolean;
@QueryBoolean({ optional: true })
withDeleted?: boolean;
@QueryBoolean({ optional: true })
withStacked?: boolean;
@QueryBoolean({ optional: true })
withExif?: boolean;
@QueryBoolean({ optional: true })
withPeople?: boolean;
@QueryDate({ optional: true })
createdBefore?: Date;
@QueryDate({ optional: true })
createdAfter?: Date;
@QueryDate({ optional: true })
updatedBefore?: Date;
@QueryDate({ optional: true })
updatedAfter?: Date;
@QueryDate({ optional: true })
trashedBefore?: Date;
@QueryDate({ optional: true })
trashedAfter?: Date;
@QueryDate({ optional: true })
takenBefore?: Date;
@QueryDate({ optional: true })
takenAfter?: Date;
@IsString()
@Optional()
originalFileName?: string;
@IsString()
@Optional()
originalPath?: string;
@IsString()
@Optional()
resizePath?: string;
@IsString()
@Optional()
webpPath?: string;
@IsString()
@Optional()
encodedVideoPath?: string;
@IsString()
@Optional()
city?: string;
@IsString()
@Optional()
state?: string;
@IsString()
@Optional()
country?: string;
@IsString()
@Optional()
make?: string;
@IsString()
@Optional()
model?: string;
@IsString()
@Optional()
lensModel?: string;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
size?: number;
}
export class AssetBulkUpdateDto extends BulkIdsDto {
@Optional()
@IsBoolean()
+28 -1
View File
@@ -1,6 +1,17 @@
import { applyDecorators } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID, ValidateIf, ValidationOptions } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateIf,
ValidationOptions,
} from 'class-validator';
import { CronJob } from 'cron';
import { basename, extname } from 'node:path';
import sanitize from 'sanitize-filename';
@@ -33,6 +44,22 @@ interface IValue {
value?: string;
}
export const QueryBoolean = ({ optional }: { optional?: boolean }) => {
const decorators = [IsBoolean(), Transform(toBoolean)];
if (optional) {
decorators.push(Optional());
}
return applyDecorators(...decorators);
};
export const QueryDate = ({ optional }: { optional?: boolean }) => {
const decorators = [IsDate(), Type(() => Date)];
if (optional) {
decorators.push(Optional());
}
return applyDecorators(...decorators);
};
export const toBoolean = ({ value }: IValue) => {
if (value == 'true') {
return true;
@@ -11,11 +11,58 @@ export interface AssetStatsOptions {
}
export interface AssetSearchOptions {
isVisible?: boolean;
trashedBefore?: Date;
id?: string;
libraryId?: string;
deviceAssetId?: string;
deviceId?: string;
ownerId?: string;
type?: AssetType;
order?: 'ASC' | 'DESC';
checksum?: Buffer;
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
withDeleted?: boolean;
withStacked?: boolean;
withExif?: boolean;
withPeople?: boolean;
createdBefore?: Date;
createdAfter?: Date;
updatedBefore?: Date;
updatedAfter?: Date;
trashedBefore?: Date;
trashedAfter?: Date;
takenBefore?: Date;
takenAfter?: Date;
originalFileName?: string;
originalPath?: string;
resizePath?: string;
webpPath?: string;
encodedVideoPath?: string;
city?: string;
state?: string;
country?: string;
make?: string;
model?: string;
lensModel?: string;
/** defaults to 'DESC' */
order?: 'ASC' | 'DESC';
/** defaults to 1 */
page?: number;
/** defaults to 250 */
size?: number;
}
export interface LivePhotoSearchOptions {
@@ -127,4 +174,5 @@ export interface IAssetRepository {
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
search(options: AssetSearchOptions): Promise<AssetEntity[]>;
}