Browse Source

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>
Jason Rasmussen 2 years ago
parent
commit
b7d34079d9

+ 6 - 2
mobile/openapi/doc/AssetApi.md

@@ -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

+ 18 - 3
mobile/openapi/lib/api/asset_api.dart

@@ -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));
     }

+ 1 - 1
mobile/openapi/test/asset_api_test.dart

@@ -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
     });

+ 3 - 9
server/apps/immich/src/api-v1/album/dto/get-albums.dto.ts

@@ -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

+ 20 - 13
server/apps/immich/src/api-v1/asset/asset-repository.ts

@@ -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',
+      },
+    });
   }
 
   /**

+ 6 - 3
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -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()

+ 4 - 3
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -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 - 0
server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts

@@ -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;
+}

+ 0 - 6
server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts

@@ -1,6 +0,0 @@
-import { IsNotEmpty } from 'class-validator';
-
-export class GetAssetDto {
-  @IsNotEmpty()
-  deviceId!: string;
-}

+ 3 - 16
server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts

@@ -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 - 0
server/apps/immich/src/utils/transform.util.ts

@@ -0,0 +1,8 @@
+export const toBoolean = ({ value }: { value: string }) => {
+  if (value == 'true') {
+    return true;
+  } else if (value == 'false') {
+    return false;
+  }
+  return value;
+};

+ 16 - 0
server/immich-openapi-specs.json

@@ -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",

+ 23 - 7
web/src/api/open-api/api.ts

@@ -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));
     }
 
     /**