mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat(server): search by is favorite (#1400)
* feat(server): search by is favorite * chore: regenerate api * fix: boolean transform * chore: remove console log Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
eade36ee82
commit
b7d34079d9
13 changed files with 98 additions and 57 deletions
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
|
|
@ -1,17 +1,11 @@
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsOptional, IsBoolean } from 'class-validator';
|
import { IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
import { toBoolean } from '../../../utils/transform.util';
|
||||||
|
|
||||||
export class GetAlbumsDto {
|
export class GetAlbumsDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@Transform(({ value }) => {
|
@Transform(toBoolean)
|
||||||
if (value == 'true') {
|
|
||||||
return true;
|
|
||||||
} else if (value == 'false') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* true: only shared albums
|
* true: only shared albums
|
||||||
* false: only non-shared own albums
|
* false: only non-shared own albums
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-as
|
||||||
import { In } from 'typeorm/find-options/operator/In';
|
import { In } from 'typeorm/find-options/operator/In';
|
||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
import { ITagRepository } from '../tag/tag.repository';
|
import { ITagRepository } from '../tag/tag.repository';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(
|
create(
|
||||||
|
|
@ -28,7 +29,7 @@ export interface IAssetRepository {
|
||||||
livePhotoAssetEntity?: AssetEntity,
|
livePhotoAssetEntity?: AssetEntity,
|
||||||
): Promise<AssetEntity>;
|
): Promise<AssetEntity>;
|
||||||
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
||||||
getAllByUserId(userId: string, skip?: number): Promise<AssetEntity[]>;
|
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
||||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||||
getById(assetId: string): Promise<AssetEntity>;
|
getById(assetId: string): Promise<AssetEntity>;
|
||||||
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
|
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
|
||||||
|
|
@ -244,17 +245,23 @@ export class AssetRepository implements IAssetRepository {
|
||||||
* Get all assets belong to the user on the database
|
* Get all assets belong to the user on the database
|
||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
async getAllByUserId(userId: string, skip?: number): Promise<AssetEntity[]> {
|
async getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
|
||||||
const query = this.assetRepository
|
return this.assetRepository.find({
|
||||||
.createQueryBuilder('asset')
|
where: {
|
||||||
.where('asset.userId = :userId', { userId: userId })
|
userId,
|
||||||
.andWhere('asset.resizePath is not NULL')
|
resizePath: Not(IsNull()),
|
||||||
.andWhere('asset.isVisible = true')
|
isVisible: true,
|
||||||
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
isFavorite: dto.isFavorite,
|
||||||
.leftJoinAndSelect('asset.tags', 'tags')
|
},
|
||||||
.skip(skip || 0)
|
relations: {
|
||||||
.orderBy('asset.createdAt', 'DESC');
|
exifInfo: true,
|
||||||
return await query.getMany();
|
tags: true,
|
||||||
|
},
|
||||||
|
skip: dto.skip || 0,
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import { DownloadFilesDto } from './dto/download-files.dto';
|
||||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||||
import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
||||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||||
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiTags('Asset')
|
@ApiTags('Asset')
|
||||||
|
|
@ -219,9 +220,11 @@ export class AssetController {
|
||||||
required: false,
|
required: false,
|
||||||
schema: { type: 'string' },
|
schema: { type: 'string' },
|
||||||
})
|
})
|
||||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> {
|
getAllAssets(
|
||||||
const assets = await this.assetService.getAllAssets(authUser);
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
return assets;
|
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
|
||||||
|
): Promise<AssetResponseDto[]> {
|
||||||
|
return this.assetService.getAllAssets(authUser, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import { DownloadFilesDto } from './dto/download-files.dto';
|
||||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||||
import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
||||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||||
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
|
|
@ -200,8 +201,8 @@ export class AssetService {
|
||||||
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
|
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> {
|
public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||||
const assets = await this._assetRepository.getAllByUserId(authUser.id);
|
const assets = await this._assetRepository.getAllByUserId(authUser.id, dto);
|
||||||
|
|
||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +239,7 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadLibrary(user: AuthUserDto, dto: DownloadDto) {
|
public async downloadLibrary(user: AuthUserDto, dto: DownloadDto) {
|
||||||
const assets = await this._assetRepository.getAllByUserId(user.id, dto.skip);
|
const assets = await this._assetRepository.getAllByUserId(user.id, dto);
|
||||||
|
|
||||||
return this.downloadService.downloadArchive(dto.name || `library`, assets);
|
return this.downloadService.downloadArchive(dto.name || `library`, assets);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts
Normal file
15
server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { IsBoolean, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
|
||||||
|
import { toBoolean } from '../../../utils/transform.util';
|
||||||
|
|
||||||
|
export class AssetSearchDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform(toBoolean)
|
||||||
|
isFavorite?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
skip?: number;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { IsNotEmpty } from 'class-validator';
|
|
||||||
|
|
||||||
export class GetAssetDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
deviceId!: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +1,18 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsOptional } from 'class-validator';
|
import { IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
import { toBoolean } from '../../../utils/transform.util';
|
||||||
|
|
||||||
export class ServeFileDto {
|
export class ServeFileDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@Transform(({ value }) => {
|
@Transform(toBoolean)
|
||||||
if (value == 'true') {
|
|
||||||
return true;
|
|
||||||
} else if (value == 'false') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
@ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' })
|
@ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' })
|
||||||
isThumb?: boolean;
|
isThumb?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@Transform(({ value }) => {
|
@Transform(toBoolean)
|
||||||
if (value == 'true') {
|
|
||||||
return true;
|
|
||||||
} else if (value == 'false') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
@ApiProperty({ type: Boolean, title: 'Is request made from web' })
|
@ApiProperty({ type: Boolean, title: 'Is request made from web' })
|
||||||
isWeb?: boolean;
|
isWeb?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
server/apps/immich/src/utils/transform.util.ts
Normal file
8
server/apps/immich/src/utils/transform.util.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const toBoolean = ({ value }: { value: string }) => {
|
||||||
|
if (value == 'true') {
|
||||||
|
return true;
|
||||||
|
} else if (value == 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
@ -1352,6 +1352,22 @@
|
||||||
"operationId": "getAllAssets",
|
"operationId": "getAllAssets",
|
||||||
"description": "Get all AssetEntity belong to the user",
|
"description": "Get all AssetEntity belong to the user",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skip",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "if-none-match",
|
"name": "if-none-match",
|
||||||
"in": "header",
|
"in": "header",
|
||||||
|
|
|
||||||
30
web/src/api/open-api/api.ts
generated
30
web/src/api/open-api/api.ts
generated
|
|
@ -3846,11 +3846,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {number} [skip]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets: async (ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getAllAssets: async (isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
const localVarPath = `/asset`;
|
const localVarPath = `/asset`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
|
@ -3867,6 +3869,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (isFavorite !== undefined) {
|
||||||
|
localVarQueryParameter['isFavorite'] = isFavorite;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip !== undefined) {
|
||||||
|
localVarQueryParameter['skip'] = skip;
|
||||||
|
}
|
||||||
|
|
||||||
if (ifNoneMatch !== undefined && ifNoneMatch !== null) {
|
if (ifNoneMatch !== undefined && ifNoneMatch !== null) {
|
||||||
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
|
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
|
||||||
}
|
}
|
||||||
|
|
@ -4504,12 +4514,14 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {number} [skip]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
async getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(ifNoneMatch, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, skip, ifNoneMatch, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|
@ -4729,12 +4741,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {number} [skip]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets(ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
|
getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
|
||||||
return localVarFp.getAllAssets(ifNoneMatch, options).then((request) => request(axios, basePath));
|
return localVarFp.getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get a single asset\'s information
|
* Get a single asset\'s information
|
||||||
|
|
@ -4953,13 +4967,15 @@ export class AssetApi extends BaseAPI {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {number} [skip]
|
||||||
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
* @param {string} [ifNoneMatch] ETag of data already cached on the client
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig) {
|
public getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getAllAssets(ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue