diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index ec44259aac..4f21fe41d6 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/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 getAllAssets(ifNoneMatch) +> List getAllAssets(isFavorite, skip, ifNoneMatch) @@ -398,10 +398,12 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('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 diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index bf74816735..e6fa7b0d5e 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/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 getAllAssetsWithHttpInfo({ String? ifNoneMatch, }) async { + Future getAllAssetsWithHttpInfo({ bool? isFavorite, num? skip, String? ifNoneMatch, }) async { // ignore: prefer_const_declarations final path = r'/asset'; @@ -422,6 +426,13 @@ class AssetApi { final headerParams = {}; final formParams = {}; + 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?> getAllAssets({ String? ifNoneMatch, }) async { - final response = await getAllAssetsWithHttpInfo( ifNoneMatch: ifNoneMatch, ); + Future?> 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)); } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index fa5c0a9bdb..086bbca010 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -68,7 +68,7 @@ void main() { // Get all AssetEntity belong to the user // - //Future> getAllAssets({ String ifNoneMatch }) async + //Future> getAllAssets({ bool isFavorite, num skip, String ifNoneMatch }) async test('test getAllAssets', () async { // TODO }); diff --git a/server/apps/immich/src/api-v1/album/dto/get-albums.dto.ts b/server/apps/immich/src/api-v1/album/dto/get-albums.dto.ts index 725992ba5f..fe03831f70 100644 --- a/server/apps/immich/src/api-v1/album/dto/get-albums.dto.ts +++ b/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 diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 741cd3854e..af02ff6ab0 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/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; update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise; - getAllByUserId(userId: string, skip?: number): Promise; + getAllByUserId(userId: string, dto: AssetSearchDto): Promise; getAllByDeviceId(userId: string, deviceId: string): Promise; getById(assetId: string): Promise; getLocationsByUserId(userId: string): Promise; @@ -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 { - 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 { + 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', + }, + }); } /** diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 14c9cac5c7..226cda6b3c 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/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 { - const assets = await this.assetService.getAllAssets(authUser); - return assets; + getAllAssets( + @GetAuthUser() authUser: AuthUserDto, + @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, + ): Promise { + return this.assetService.getAllAssets(authUser, dto); } @Authenticated() diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index ce2d22d44d..830e49a19e 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/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 { - const assets = await this._assetRepository.getAllByUserId(authUser.id); + public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise { + 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); } diff --git a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts new file mode 100644 index 0000000000..0d1ba7ccfc --- /dev/null +++ b/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; +} diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts deleted file mode 100644 index d3b27c1c0b..0000000000 --- a/server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsNotEmpty } from 'class-validator'; - -export class GetAssetDto { - @IsNotEmpty() - deviceId!: string; -} diff --git a/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts b/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts index 65eb478899..a4c01b07c1 100644 --- a/server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts +++ b/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; } diff --git a/server/apps/immich/src/utils/transform.util.ts b/server/apps/immich/src/utils/transform.util.ts new file mode 100644 index 0000000000..92ce5bf64e --- /dev/null +++ b/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; +}; diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 212271b3a9..a3d74b7d34 100644 --- a/server/immich-openapi-specs.json +++ b/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", diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 27ca348332..65f631f1de 100644 --- a/web/src/api/open-api/api.ts +++ b/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 => { + getAllAssets: async (isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { 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>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(ifNoneMatch, options); + async getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + 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> { - return localVarFp.getAllAssets(ifNoneMatch, options).then((request) => request(axios, basePath)); + getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { + 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)); } /**