You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02: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:
		
							
								
								
									
										8
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -381,7 +381,7 @@ Name | Type | Description  | Notes | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **getAllAssets** | ||||
| > List<AssetResponseDto> getAllAssets(ifNoneMatch) | ||||
| > List<AssetResponseDto> getAllAssets(isFavorite, skip, ifNoneMatch) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -398,10 +398,12 @@ import 'package:openapi/api.dart'; | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AssetApi(); | ||||
| final isFavorite = true; // bool |  | ||||
| final skip = 8.14; // num |  | ||||
| final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.getAllAssets(ifNoneMatch); | ||||
|     final result = api_instance.getAllAssets(isFavorite, skip, ifNoneMatch); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->getAllAssets: $e\n'); | ||||
| @@ -412,6 +414,8 @@ try { | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **isFavorite** | **bool**|  | [optional]  | ||||
|  **skip** | **num**|  | [optional]  | ||||
|  **ifNoneMatch** | **String**| ETag of data already cached on the client | [optional]  | ||||
| 
 | ||||
| ### Return type | ||||
|   | ||||
							
								
								
									
										21
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -409,9 +409,13 @@ class AssetApi { | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [num] skip: | ||||
|   /// | ||||
|   /// * [String] ifNoneMatch: | ||||
|   ///   ETag of data already cached on the client | ||||
|   Future<Response> getAllAssetsWithHttpInfo({ String? ifNoneMatch, }) async { | ||||
|   Future<Response> getAllAssetsWithHttpInfo({ bool? isFavorite, num? skip, String? ifNoneMatch, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset'; | ||||
| 
 | ||||
| @@ -422,6 +426,13 @@ class AssetApi { | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     if (isFavorite != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); | ||||
|     } | ||||
|     if (skip != null) { | ||||
|       queryParams.addAll(_queryParams('', 'skip', skip)); | ||||
|     } | ||||
| 
 | ||||
|     if (ifNoneMatch != null) { | ||||
|       headerParams[r'if-none-match'] = parameterToString(ifNoneMatch); | ||||
|     } | ||||
| @@ -444,10 +455,14 @@ class AssetApi { | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [num] skip: | ||||
|   /// | ||||
|   /// * [String] ifNoneMatch: | ||||
|   ///   ETag of data already cached on the client | ||||
|   Future<List<AssetResponseDto>?> getAllAssets({ String? ifNoneMatch, }) async { | ||||
|     final response = await getAllAssetsWithHttpInfo( ifNoneMatch: ifNoneMatch, ); | ||||
|   Future<List<AssetResponseDto>?> getAllAssets({ bool? isFavorite, num? skip, String? ifNoneMatch, }) async { | ||||
|     final response = await getAllAssetsWithHttpInfo( isFavorite: isFavorite, skip: skip, ifNoneMatch: ifNoneMatch, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -68,7 +68,7 @@ void main() { | ||||
| 
 | ||||
|     // Get all AssetEntity belong to the user | ||||
|     // | ||||
|     //Future<List<AssetResponseDto>> getAllAssets({ String ifNoneMatch }) async | ||||
|     //Future<List<AssetResponseDto>> getAllAssets({ bool isFavorite, num skip, String ifNoneMatch }) async | ||||
|     test('test getAllAssets', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
| @@ -1,17 +1,11 @@ | ||||
| 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 { | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @Transform(({ value }) => { | ||||
|     if (value == 'true') { | ||||
|       return true; | ||||
|     } else if (value == 'false') { | ||||
|       return false; | ||||
|     } | ||||
|     return value; | ||||
|   }) | ||||
|   @Transform(toBoolean) | ||||
|   /** | ||||
|    * true: only shared 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 { UpdateAssetDto } from './dto/update-asset.dto'; | ||||
| 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 { | ||||
|   create( | ||||
| @@ -28,7 +29,7 @@ export interface IAssetRepository { | ||||
|     livePhotoAssetEntity?: AssetEntity, | ||||
|   ): 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[]>; | ||||
|   getById(assetId: string): Promise<AssetEntity>; | ||||
|   getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>; | ||||
| @@ -244,17 +245,23 @@ export class AssetRepository implements IAssetRepository { | ||||
|    * Get all assets belong to the user on the database | ||||
|    * @param userId | ||||
|    */ | ||||
|   async getAllByUserId(userId: string, skip?: number): Promise<AssetEntity[]> { | ||||
|     const query = this.assetRepository | ||||
|       .createQueryBuilder('asset') | ||||
|       .where('asset.userId = :userId', { userId: userId }) | ||||
|       .andWhere('asset.resizePath is not NULL') | ||||
|       .andWhere('asset.isVisible = true') | ||||
|       .leftJoinAndSelect('asset.exifInfo', 'exifInfo') | ||||
|       .leftJoinAndSelect('asset.tags', 'tags') | ||||
|       .skip(skip || 0) | ||||
|       .orderBy('asset.createdAt', 'DESC'); | ||||
|     return await query.getMany(); | ||||
|   async getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]> { | ||||
|     return this.assetRepository.find({ | ||||
|       where: { | ||||
|         userId, | ||||
|         resizePath: Not(IsNull()), | ||||
|         isVisible: true, | ||||
|         isFavorite: dto.isFavorite, | ||||
|       }, | ||||
|       relations: { | ||||
|         exifInfo: true, | ||||
|         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 { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; | ||||
| import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; | ||||
| import { AssetSearchDto } from './dto/asset-search.dto'; | ||||
|  | ||||
| @ApiBearerAuth() | ||||
| @ApiTags('Asset') | ||||
| @@ -219,9 +220,11 @@ export class AssetController { | ||||
|     required: false, | ||||
|     schema: { type: 'string' }, | ||||
|   }) | ||||
|   async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> { | ||||
|     const assets = await this.assetService.getAllAssets(authUser); | ||||
|     return assets; | ||||
|   getAllAssets( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, | ||||
|   ): Promise<AssetResponseDto[]> { | ||||
|     return this.assetService.getAllAssets(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @Authenticated() | ||||
|   | ||||
| @@ -54,6 +54,7 @@ import { DownloadFilesDto } from './dto/download-files.dto'; | ||||
| import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; | ||||
| import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; | ||||
| import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; | ||||
| import { AssetSearchDto } from './dto/asset-search.dto'; | ||||
|  | ||||
| const fileInfo = promisify(stat); | ||||
|  | ||||
| @@ -200,8 +201,8 @@ export class AssetService { | ||||
|     return this._assetRepository.getAllByDeviceId(authUser.id, deviceId); | ||||
|   } | ||||
|  | ||||
|   public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> { | ||||
|     const assets = await this._assetRepository.getAllByUserId(authUser.id); | ||||
|   public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> { | ||||
|     const assets = await this._assetRepository.getAllByUserId(authUser.id, dto); | ||||
|  | ||||
|     return assets.map((asset) => mapAsset(asset)); | ||||
|   } | ||||
| @@ -238,7 +239,7 @@ export class AssetService { | ||||
|   } | ||||
|  | ||||
|   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); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										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 { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { toBoolean } from '../../../utils/transform.util'; | ||||
|  | ||||
| export class ServeFileDto { | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @Transform(({ value }) => { | ||||
|     if (value == 'true') { | ||||
|       return true; | ||||
|     } else if (value == 'false') { | ||||
|       return false; | ||||
|     } | ||||
|     return value; | ||||
|   }) | ||||
|   @Transform(toBoolean) | ||||
|   @ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' }) | ||||
|   isThumb?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @Transform(({ value }) => { | ||||
|     if (value == 'true') { | ||||
|       return true; | ||||
|     } else if (value == 'false') { | ||||
|       return false; | ||||
|     } | ||||
|     return value; | ||||
|   }) | ||||
|   @Transform(toBoolean) | ||||
|   @ApiProperty({ type: Boolean, title: 'Is request made from web' }) | ||||
|   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", | ||||
|         "description": "Get all AssetEntity belong to the user", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "isFavorite", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "skip", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "number" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "if-none-match", | ||||
|             "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 | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [ifNoneMatch] ETag of data already cached on the client | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @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`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
| @@ -3867,6 +3869,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
|             if (isFavorite !== undefined) { | ||||
|                 localVarQueryParameter['isFavorite'] = isFavorite; | ||||
|             } | ||||
| 
 | ||||
|             if (skip !== undefined) { | ||||
|                 localVarQueryParameter['skip'] = skip; | ||||
|             } | ||||
| 
 | ||||
|             if (ifNoneMatch !== undefined && ifNoneMatch !== null) { | ||||
|                 localVarHeaderParameter['if-none-match'] = String(ifNoneMatch); | ||||
|             } | ||||
| @@ -4504,12 +4514,14 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|         }, | ||||
|         /** | ||||
|          * 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 {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(ifNoneMatch, options); | ||||
|         async getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, skip, ifNoneMatch, options); | ||||
|             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 | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {number} [skip]  | ||||
|          * @param {string} [ifNoneMatch] ETag of data already cached on the client | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllAssets(ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getAllAssets(ifNoneMatch, options).then((request) => request(axios, basePath)); | ||||
|         getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Get a single asset\'s information | ||||
| @@ -4953,13 +4967,15 @@ export class AssetApi extends BaseAPI { | ||||
| 
 | ||||
|     /** | ||||
|      * 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 {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getAllAssets(ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); | ||||
|     public getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user