mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	refactor(server): update asset endpoint (#3973)
* refactor(server): update asset * chore: open api
This commit is contained in:
		
							parent
							
								
									26bc889f8d
								
							
						
					
					
						commit
						454737ca79
					
				
							
								
								
									
										14
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -3417,12 +3417,6 @@ export interface UpdateAssetDto {
 | 
				
			|||||||
     * @memberof UpdateAssetDto
 | 
					     * @memberof UpdateAssetDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'isFavorite'?: boolean;
 | 
					    'isFavorite'?: boolean;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {Array<string>}
 | 
					 | 
				
			||||||
     * @memberof UpdateAssetDto
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'tagIds'?: Array<string>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
@ -6299,7 +6293,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Update an asset
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {string} id 
 | 
				
			||||||
         * @param {UpdateAssetDto} updateAssetDto 
 | 
					         * @param {UpdateAssetDto} updateAssetDto 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -6778,7 +6772,7 @@ export const AssetApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Update an asset
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {string} id 
 | 
				
			||||||
         * @param {UpdateAssetDto} updateAssetDto 
 | 
					         * @param {UpdateAssetDto} updateAssetDto 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -7035,7 +7029,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
 | 
				
			|||||||
            return localVarFp.serveFile(requestParameters.id, requestParameters.isThumb, requestParameters.isWeb, requestParameters.key, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.serveFile(requestParameters.id, requestParameters.isThumb, requestParameters.isWeb, requestParameters.key, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Update an asset
 | 
					         * 
 | 
				
			||||||
         * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
					         * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
@ -7952,7 +7946,7 @@ export class AssetApi extends BaseAPI {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Update an asset
 | 
					     * 
 | 
				
			||||||
     * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
					     * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
     * @throws {RequiredError}
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							@ -1368,8 +1368,6 @@ Name | Type | Description  | Notes
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Update an asset
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Example
 | 
					### Example
 | 
				
			||||||
```dart
 | 
					```dart
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UpdateAssetDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UpdateAssetDto.md
									
									
									
										generated
									
									
									
								
							@ -11,7 +11,6 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**description** | **String** |  | [optional] 
 | 
					**description** | **String** |  | [optional] 
 | 
				
			||||||
**isArchived** | **bool** |  | [optional] 
 | 
					**isArchived** | **bool** |  | [optional] 
 | 
				
			||||||
**isFavorite** | **bool** |  | [optional] 
 | 
					**isFavorite** | **bool** |  | [optional] 
 | 
				
			||||||
**tagIds** | **List<String>** |  | [optional] [default to const []]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							@ -1384,10 +1384,7 @@ class AssetApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Update an asset
 | 
					  /// Performs an HTTP 'PUT /asset/{id}' operation and returns the [Response].
 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// Note: This method returns the HTTP [Response].
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [String] id (required):
 | 
					  /// * [String] id (required):
 | 
				
			||||||
@ -1419,8 +1416,6 @@ class AssetApi {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Update an asset
 | 
					 | 
				
			||||||
  ///
 | 
					 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [String] id (required):
 | 
					  /// * [String] id (required):
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								mobile/openapi/lib/model/update_asset_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/lib/model/update_asset_dto.dart
									
									
									
										generated
									
									
									
								
							@ -16,7 +16,6 @@ class UpdateAssetDto {
 | 
				
			|||||||
    this.description,
 | 
					    this.description,
 | 
				
			||||||
    this.isArchived,
 | 
					    this.isArchived,
 | 
				
			||||||
    this.isFavorite,
 | 
					    this.isFavorite,
 | 
				
			||||||
    this.tagIds = const [],
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -43,25 +42,21 @@ class UpdateAssetDto {
 | 
				
			|||||||
  ///
 | 
					  ///
 | 
				
			||||||
  bool? isFavorite;
 | 
					  bool? isFavorite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<String> tagIds;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
 | 
				
			||||||
     other.description == description &&
 | 
					     other.description == description &&
 | 
				
			||||||
     other.isArchived == isArchived &&
 | 
					     other.isArchived == isArchived &&
 | 
				
			||||||
     other.isFavorite == isFavorite &&
 | 
					     other.isFavorite == isFavorite;
 | 
				
			||||||
     other.tagIds == tagIds;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (description == null ? 0 : description!.hashCode) +
 | 
					    (description == null ? 0 : description!.hashCode) +
 | 
				
			||||||
    (isArchived == null ? 0 : isArchived!.hashCode) +
 | 
					    (isArchived == null ? 0 : isArchived!.hashCode) +
 | 
				
			||||||
    (isFavorite == null ? 0 : isFavorite!.hashCode) +
 | 
					    (isFavorite == null ? 0 : isFavorite!.hashCode);
 | 
				
			||||||
    (tagIds.hashCode);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite, tagIds=$tagIds]';
 | 
					  String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -80,7 +75,6 @@ class UpdateAssetDto {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
    //  json[r'isFavorite'] = null;
 | 
					    //  json[r'isFavorite'] = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      json[r'tagIds'] = this.tagIds;
 | 
					 | 
				
			||||||
    return json;
 | 
					    return json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -95,9 +89,6 @@ class UpdateAssetDto {
 | 
				
			|||||||
        description: mapValueOfType<String>(json, r'description'),
 | 
					        description: mapValueOfType<String>(json, r'description'),
 | 
				
			||||||
        isArchived: mapValueOfType<bool>(json, r'isArchived'),
 | 
					        isArchived: mapValueOfType<bool>(json, r'isArchived'),
 | 
				
			||||||
        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
 | 
					        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
 | 
				
			||||||
        tagIds: json[r'tagIds'] is List
 | 
					 | 
				
			||||||
            ? (json[r'tagIds'] as List).cast<String>()
 | 
					 | 
				
			||||||
            : const [],
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							@ -144,8 +144,6 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Update an asset
 | 
					 | 
				
			||||||
    //
 | 
					 | 
				
			||||||
    //Future<AssetResponseDto> updateAsset(String id, UpdateAssetDto updateAssetDto) async
 | 
					    //Future<AssetResponseDto> updateAsset(String id, UpdateAssetDto updateAssetDto) async
 | 
				
			||||||
    test('test updateAsset', () async {
 | 
					    test('test updateAsset', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/update_asset_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/update_asset_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -31,11 +31,6 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // List<String> tagIds (default value: const [])
 | 
					 | 
				
			||||||
    test('to test the property `tagIds`', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2020,7 +2020,6 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "/asset/{id}": {
 | 
					    "/asset/{id}": {
 | 
				
			||||||
      "put": {
 | 
					      "put": {
 | 
				
			||||||
        "description": "Update an asset",
 | 
					 | 
				
			||||||
        "operationId": "updateAsset",
 | 
					        "operationId": "updateAsset",
 | 
				
			||||||
        "parameters": [
 | 
					        "parameters": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
@ -7424,18 +7423,6 @@
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          "isFavorite": {
 | 
					          "isFavorite": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "tagIds": {
 | 
					 | 
				
			||||||
            "example": [
 | 
					 | 
				
			||||||
              "bf973405-3f2a-48d2-a687-2ed4167164be",
 | 
					 | 
				
			||||||
              "dd41870b-5d00-46d2-924e-1d8489a0aa0f",
 | 
					 | 
				
			||||||
              "fad77c3f-deef-4e7e-9608-14c1aa4e559a"
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            "items": {
 | 
					 | 
				
			||||||
              "type": "string"
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "title": "Array of tag IDs to add to the asset",
 | 
					 | 
				
			||||||
            "type": "array"
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { AssetEntity, AssetType } from '@app/infra/entities';
 | 
					import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Paginated, PaginationOptions } from '../domain.util';
 | 
					import { Paginated, PaginationOptions } from '../domain.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AssetStats = Record<AssetType, number>;
 | 
					export type AssetStats = Record<AssetType, number>;
 | 
				
			||||||
@ -86,4 +86,5 @@ export interface IAssetRepository {
 | 
				
			|||||||
  getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
 | 
					  getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
 | 
				
			||||||
  getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
 | 
					  getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
 | 
				
			||||||
  getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
 | 
					  getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
 | 
				
			||||||
 | 
					  upsertExif(exif: Partial<ExifEntity>): Promise<void>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -519,6 +519,30 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('update', () => {
 | 
				
			||||||
 | 
					    it('should require asset write access for the id', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
 | 
				
			||||||
 | 
					        BadRequestException,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should update the asset', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.save.mockResolvedValue(assetStub.image);
 | 
				
			||||||
 | 
					      await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should update the exif description', async () => {
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetMock.save.mockResolvedValue(assetStub.image);
 | 
				
			||||||
 | 
					      await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
 | 
				
			||||||
 | 
					      expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('updateAll', () => {
 | 
					  describe('updateAll', () => {
 | 
				
			||||||
    it('should require asset write access for all ids', async () => {
 | 
					    it('should require asset write access for all ids', async () => {
 | 
				
			||||||
      accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,7 @@ import {
 | 
				
			|||||||
  MemoryLaneDto,
 | 
					  MemoryLaneDto,
 | 
				
			||||||
  TimeBucketAssetDto,
 | 
					  TimeBucketAssetDto,
 | 
				
			||||||
  TimeBucketDto,
 | 
					  TimeBucketDto,
 | 
				
			||||||
 | 
					  UpdateAssetDto,
 | 
				
			||||||
  mapStats,
 | 
					  mapStats,
 | 
				
			||||||
} from './dto';
 | 
					} from './dto';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -279,6 +280,19 @@ export class AssetService {
 | 
				
			|||||||
    return mapStats(stats);
 | 
					    return mapStats(stats);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
 | 
				
			||||||
 | 
					    await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { description, ...rest } = dto;
 | 
				
			||||||
 | 
					    if (description !== undefined) {
 | 
				
			||||||
 | 
					      await this.assetRepository.upsertExif({ assetId: id, description });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const asset = await this.assetRepository.save({ id, ...rest });
 | 
				
			||||||
 | 
					    await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
 | 
				
			||||||
 | 
					    return mapAsset(asset);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) {
 | 
					  async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) {
 | 
				
			||||||
    const { ids, ...options } = dto;
 | 
					    const { ids, ...options } = dto;
 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
 | 
					    await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { IsBoolean } from 'class-validator';
 | 
					import { IsBoolean, IsString } from 'class-validator';
 | 
				
			||||||
import { Optional } from '../../domain.util';
 | 
					import { Optional } from '../../domain.util';
 | 
				
			||||||
import { BulkIdsDto } from '../response-dto';
 | 
					import { BulkIdsDto } from '../response-dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,3 +11,17 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
 | 
				
			|||||||
  @IsBoolean()
 | 
					  @IsBoolean()
 | 
				
			||||||
  isArchived?: boolean;
 | 
					  isArchived?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpdateAssetDto {
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  isFavorite?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  isArchived?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { MoreThan } from 'typeorm';
 | 
					import { MoreThan } from 'typeorm';
 | 
				
			||||||
@ -7,7 +7,6 @@ import { Repository } from 'typeorm/repository/Repository';
 | 
				
			|||||||
import { AssetSearchDto } from './dto/asset-search.dto';
 | 
					import { AssetSearchDto } from './dto/asset-search.dto';
 | 
				
			||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 | 
					import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 | 
				
			||||||
import { SearchPropertiesDto } from './dto/search-properties.dto';
 | 
					import { SearchPropertiesDto } from './dto/search-properties.dto';
 | 
				
			||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
 | 
					 | 
				
			||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 | 
					import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 | 
				
			||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 | 
					import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,7 +25,6 @@ export interface IAssetRepository {
 | 
				
			|||||||
    asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
 | 
					    asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
 | 
				
			||||||
  ): Promise<AssetEntity>;
 | 
					  ): Promise<AssetEntity>;
 | 
				
			||||||
  remove(asset: AssetEntity): Promise<void>;
 | 
					  remove(asset: AssetEntity): Promise<void>;
 | 
				
			||||||
  update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
 | 
					 | 
				
			||||||
  getAllByUserId(userId: string, dto: AssetSearchDto): 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>;
 | 
				
			||||||
@ -42,10 +40,7 @@ export const IAssetRepository = 'IAssetRepository';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AssetRepository implements IAssetRepository {
 | 
					export class AssetRepository implements IAssetRepository {
 | 
				
			||||||
  constructor(
 | 
					  constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
 | 
				
			||||||
    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
					 | 
				
			||||||
    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
 | 
					 | 
				
			||||||
  ) {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
 | 
					  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
 | 
				
			||||||
    return this.assetRepository
 | 
					    return this.assetRepository
 | 
				
			||||||
@ -164,40 +159,6 @@ export class AssetRepository implements IAssetRepository {
 | 
				
			|||||||
    await this.assetRepository.remove(asset);
 | 
					    await this.assetRepository.remove(asset);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Update asset
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
 | 
					 | 
				
			||||||
    asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
 | 
					 | 
				
			||||||
    asset.isArchived = dto.isArchived ?? asset.isArchived;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (asset.exifInfo != null) {
 | 
					 | 
				
			||||||
      if (dto.description !== undefined) {
 | 
					 | 
				
			||||||
        asset.exifInfo.description = dto.description;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      await this.exifRepository.save(asset.exifInfo);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const exifInfo = new ExifEntity();
 | 
					 | 
				
			||||||
      if (dto.description !== undefined) {
 | 
					 | 
				
			||||||
        exifInfo.description = dto.description;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      exifInfo.asset = asset;
 | 
					 | 
				
			||||||
      await this.exifRepository.save(exifInfo);
 | 
					 | 
				
			||||||
      asset.exifInfo = exifInfo;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.assetRepository.update(asset.id, {
 | 
					 | 
				
			||||||
      isFavorite: asset.isFavorite,
 | 
					 | 
				
			||||||
      isArchived: asset.isArchived,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return this.assetRepository.findOneOrFail({
 | 
					 | 
				
			||||||
      where: {
 | 
					 | 
				
			||||||
        id: asset.id,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get assets by device's Id on the database
 | 
					   * Get assets by device's Id on the database
 | 
				
			||||||
   * @param ownerId
 | 
					   * @param ownerId
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,6 @@ import {
 | 
				
			|||||||
  Param,
 | 
					  Param,
 | 
				
			||||||
  ParseFilePipe,
 | 
					  ParseFilePipe,
 | 
				
			||||||
  Post,
 | 
					  Post,
 | 
				
			||||||
  Put,
 | 
					 | 
				
			||||||
  Query,
 | 
					  Query,
 | 
				
			||||||
  Response,
 | 
					  Response,
 | 
				
			||||||
  UploadedFiles,
 | 
					  UploadedFiles,
 | 
				
			||||||
@ -33,7 +32,6 @@ import { DeviceIdDto } from './dto/device-id.dto';
 | 
				
			|||||||
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
 | 
					import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
 | 
				
			||||||
import { SearchAssetDto } from './dto/search-asset.dto';
 | 
					import { SearchAssetDto } from './dto/search-asset.dto';
 | 
				
			||||||
import { ServeFileDto } from './dto/serve-file.dto';
 | 
					import { ServeFileDto } from './dto/serve-file.dto';
 | 
				
			||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
 | 
					 | 
				
			||||||
import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
 | 
					import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
 | 
				
			||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 | 
					import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 | 
				
			||||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 | 
					import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 | 
				
			||||||
@ -194,18 +192,6 @@ export class AssetController {
 | 
				
			|||||||
    return this.assetService.getAssetById(authUser, id);
 | 
					    return this.assetService.getAssetById(authUser, id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Update an asset
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @Put('/:id')
 | 
					 | 
				
			||||||
  updateAsset(
 | 
					 | 
				
			||||||
    @AuthUser() authUser: AuthUserDto,
 | 
					 | 
				
			||||||
    @Param() { id }: UUIDParamDto,
 | 
					 | 
				
			||||||
    @Body(ValidationPipe) dto: UpdateAssetDto,
 | 
					 | 
				
			||||||
  ): Promise<AssetResponseDto> {
 | 
					 | 
				
			||||||
    return this.assetService.updateAsset(authUser, id, dto);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Delete('/')
 | 
					  @Delete('/')
 | 
				
			||||||
  deleteAsset(
 | 
					  deleteAsset(
 | 
				
			||||||
    @AuthUser() authUser: AuthUserDto,
 | 
					    @AuthUser() authUser: AuthUserDto,
 | 
				
			||||||
 | 
				
			|||||||
@ -96,7 +96,6 @@ describe('AssetService', () => {
 | 
				
			|||||||
      create: jest.fn(),
 | 
					      create: jest.fn(),
 | 
				
			||||||
      remove: jest.fn(),
 | 
					      remove: jest.fn(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      update: jest.fn(),
 | 
					 | 
				
			||||||
      getAllByUserId: jest.fn(),
 | 
					      getAllByUserId: jest.fn(),
 | 
				
			||||||
      getAllByDeviceId: jest.fn(),
 | 
					      getAllByDeviceId: jest.fn(),
 | 
				
			||||||
      getById: jest.fn(),
 | 
					      getById: jest.fn(),
 | 
				
			||||||
 | 
				
			|||||||
@ -41,7 +41,6 @@ import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-ass
 | 
				
			|||||||
import { SearchAssetDto } from './dto/search-asset.dto';
 | 
					import { SearchAssetDto } from './dto/search-asset.dto';
 | 
				
			||||||
import { SearchPropertiesDto } from './dto/search-properties.dto';
 | 
					import { SearchPropertiesDto } from './dto/search-properties.dto';
 | 
				
			||||||
import { ServeFileDto } from './dto/serve-file.dto';
 | 
					import { ServeFileDto } from './dto/serve-file.dto';
 | 
				
			||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AssetBulkUploadCheckResponseDto,
 | 
					  AssetBulkUploadCheckResponseDto,
 | 
				
			||||||
  AssetRejectReason,
 | 
					  AssetRejectReason,
 | 
				
			||||||
@ -203,21 +202,6 @@ export class AssetService {
 | 
				
			|||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
 | 
					 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, assetId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const asset = await this._assetRepository.getById(assetId);
 | 
					 | 
				
			||||||
    if (!asset) {
 | 
					 | 
				
			||||||
      throw new BadRequestException('Asset not found');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updatedAsset = await this._assetRepository.update(authUser.id, asset, dto);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetId] } });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return mapAsset(updatedAsset);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
 | 
					  async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId);
 | 
					    await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,33 +0,0 @@
 | 
				
			|||||||
import { Optional } from '@app/domain';
 | 
					 | 
				
			||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { IsArray, IsBoolean, IsNotEmpty, IsString } from 'class-validator';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class UpdateAssetDto {
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  isFavorite?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  isArchived?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsArray()
 | 
					 | 
				
			||||||
  @IsString({ each: true })
 | 
					 | 
				
			||||||
  @IsNotEmpty({ each: true })
 | 
					 | 
				
			||||||
  @ApiProperty({
 | 
					 | 
				
			||||||
    isArray: true,
 | 
					 | 
				
			||||||
    type: String,
 | 
					 | 
				
			||||||
    title: 'Array of tag IDs to add to the asset',
 | 
					 | 
				
			||||||
    example: [
 | 
					 | 
				
			||||||
      'bf973405-3f2a-48d2-a687-2ed4167164be',
 | 
					 | 
				
			||||||
      'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
 | 
					 | 
				
			||||||
      'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  tagIds?: string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  description?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { DomainModule } from '@app/domain';
 | 
					import { DomainModule } from '@app/domain';
 | 
				
			||||||
import { InfraModule } from '@app/infra';
 | 
					import { InfraModule } from '@app/infra';
 | 
				
			||||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
 | 
					import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
 | 
				
			||||||
import { APP_GUARD } from '@nestjs/core';
 | 
					import { APP_GUARD } from '@nestjs/core';
 | 
				
			||||||
import { ScheduleModule } from '@nestjs/schedule';
 | 
					import { ScheduleModule } from '@nestjs/schedule';
 | 
				
			||||||
@ -35,7 +35,7 @@ import {
 | 
				
			|||||||
    //
 | 
					    //
 | 
				
			||||||
    DomainModule.register({ imports: [InfraModule] }),
 | 
					    DomainModule.register({ imports: [InfraModule] }),
 | 
				
			||||||
    ScheduleModule.forRoot(),
 | 
					    ScheduleModule.forRoot(),
 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
					    TypeOrmModule.forFeature([AssetEntity]),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [
 | 
					  controllers: [
 | 
				
			||||||
    AssetController,
 | 
					    AssetController,
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ import {
 | 
				
			|||||||
  TimeBucketAssetDto,
 | 
					  TimeBucketAssetDto,
 | 
				
			||||||
  TimeBucketDto,
 | 
					  TimeBucketDto,
 | 
				
			||||||
  TimeBucketResponseDto,
 | 
					  TimeBucketResponseDto,
 | 
				
			||||||
 | 
					  UpdateAssetDto as UpdateDto,
 | 
				
			||||||
} from '@app/domain';
 | 
					} from '@app/domain';
 | 
				
			||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
 | 
					import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
 | 
				
			||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
 | 
					import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
@ -90,4 +91,13 @@ export class AssetController {
 | 
				
			|||||||
  updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
 | 
					  updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
 | 
				
			||||||
    return this.service.updateAll(authUser, dto);
 | 
					    return this.service.updateAll(authUser, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Put(':id')
 | 
				
			||||||
 | 
					  updateAsset(
 | 
				
			||||||
 | 
					    @AuthUser() authUser: AuthUserDto,
 | 
				
			||||||
 | 
					    @Param() { id }: UUIDParamDto,
 | 
				
			||||||
 | 
					    @Body() dto: UpdateDto,
 | 
				
			||||||
 | 
					  ): Promise<AssetResponseDto> {
 | 
				
			||||||
 | 
					    return this.service.update(authUser, id, dto);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import { APIKeyEntity } from './api-key.entity';
 | 
				
			|||||||
import { AssetFaceEntity } from './asset-face.entity';
 | 
					import { AssetFaceEntity } from './asset-face.entity';
 | 
				
			||||||
import { AssetEntity } from './asset.entity';
 | 
					import { AssetEntity } from './asset.entity';
 | 
				
			||||||
import { AuditEntity } from './audit.entity';
 | 
					import { AuditEntity } from './audit.entity';
 | 
				
			||||||
 | 
					import { ExifEntity } from './exif.entity';
 | 
				
			||||||
import { PartnerEntity } from './partner.entity';
 | 
					import { PartnerEntity } from './partner.entity';
 | 
				
			||||||
import { PersonEntity } from './person.entity';
 | 
					import { PersonEntity } from './person.entity';
 | 
				
			||||||
import { SharedLinkEntity } from './shared-link.entity';
 | 
					import { SharedLinkEntity } from './shared-link.entity';
 | 
				
			||||||
@ -33,6 +34,7 @@ export const databaseEntities = [
 | 
				
			|||||||
  AssetEntity,
 | 
					  AssetEntity,
 | 
				
			||||||
  AssetFaceEntity,
 | 
					  AssetFaceEntity,
 | 
				
			||||||
  AuditEntity,
 | 
					  AuditEntity,
 | 
				
			||||||
 | 
					  ExifEntity,
 | 
				
			||||||
  PartnerEntity,
 | 
					  PartnerEntity,
 | 
				
			||||||
  PersonEntity,
 | 
					  PersonEntity,
 | 
				
			||||||
  SharedLinkEntity,
 | 
					  SharedLinkEntity,
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ import { Injectable } from '@nestjs/common';
 | 
				
			|||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { DateTime } from 'luxon';
 | 
					import { DateTime } from 'luxon';
 | 
				
			||||||
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
 | 
					import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
import { AssetEntity, AssetType } from '../entities';
 | 
					import { AssetEntity, AssetType, ExifEntity } from '../entities';
 | 
				
			||||||
import OptionalBetween from '../utils/optional-between.util';
 | 
					import OptionalBetween from '../utils/optional-between.util';
 | 
				
			||||||
import { paginate } from '../utils/pagination.util';
 | 
					import { paginate } from '../utils/pagination.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,7 +29,14 @@ const truncateMap: Record<TimeBucketSize, string> = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AssetRepository implements IAssetRepository {
 | 
					export class AssetRepository implements IAssetRepository {
 | 
				
			||||||
  constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(AssetEntity) private repository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async upsertExif(exif: Partial<ExifEntity>): Promise<void> {
 | 
				
			||||||
 | 
					    await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
 | 
					  getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
 | 
				
			||||||
    // For reference of a correct approach although slower
 | 
					    // For reference of a correct approach although slower
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,11 @@
 | 
				
			|||||||
import { DomainModule } from '@app/domain';
 | 
					import { DomainModule } from '@app/domain';
 | 
				
			||||||
import { InfraModule } from '@app/infra';
 | 
					import { InfraModule } from '@app/infra';
 | 
				
			||||||
import { ExifEntity } from '@app/infra/entities';
 | 
					 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
					 | 
				
			||||||
import { AppService } from './app.service';
 | 
					import { AppService } from './app.service';
 | 
				
			||||||
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
 | 
					import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [DomainModule.register({ imports: [InfraModule] })],
 | 
				
			||||||
    //
 | 
					 | 
				
			||||||
    DomainModule.register({ imports: [InfraModule] }),
 | 
					 | 
				
			||||||
    TypeOrmModule.forFeature([ExifEntity]),
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  providers: [MetadataExtractionProcessor, AppService],
 | 
					  providers: [MetadataExtractionProcessor, AppService],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class MicroservicesModule {}
 | 
					export class MicroservicesModule {}
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,6 @@ import {
 | 
				
			|||||||
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Inject, Logger } from '@nestjs/common';
 | 
					import { Inject, Logger } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					 | 
				
			||||||
import tz_lookup from '@photostructure/tz-lookup';
 | 
					import tz_lookup from '@photostructure/tz-lookup';
 | 
				
			||||||
import { exiftool, Tags } from 'exiftool-vendored';
 | 
					import { exiftool, Tags } from 'exiftool-vendored';
 | 
				
			||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
 | 
					import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
 | 
				
			||||||
@ -26,7 +25,6 @@ import { Duration } from 'luxon';
 | 
				
			|||||||
import fs from 'node:fs';
 | 
					import fs from 'node:fs';
 | 
				
			||||||
import path from 'node:path';
 | 
					import path from 'node:path';
 | 
				
			||||||
import sharp from 'sharp';
 | 
					import sharp from 'sharp';
 | 
				
			||||||
import { Repository } from 'typeorm/repository/Repository';
 | 
					 | 
				
			||||||
import { promisify } from 'util';
 | 
					import { promisify } from 'util';
 | 
				
			||||||
import { parseLatitude, parseLongitude } from '../utils/exif/coordinates';
 | 
					import { parseLatitude, parseLongitude } from '../utils/exif/coordinates';
 | 
				
			||||||
import { exifTimeZone, exifToDate } from '../utils/exif/date-time';
 | 
					import { exifTimeZone, exifToDate } from '../utils/exif/date-time';
 | 
				
			||||||
@ -65,7 +63,6 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
    @Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository,
 | 
					    @Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository,
 | 
				
			||||||
    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
					    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    configService: ConfigService,
 | 
					    configService: ConfigService,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
@ -407,7 +404,7 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
 | 
					    await this.assetRepository.upsertExif(newExif);
 | 
				
			||||||
    await this.assetRepository.save({
 | 
					    await this.assetRepository.save({
 | 
				
			||||||
      id: asset.id,
 | 
					      id: asset.id,
 | 
				
			||||||
      fileCreatedAt: fileCreatedAt || undefined,
 | 
					      fileCreatedAt: fileCreatedAt || undefined,
 | 
				
			||||||
@ -506,7 +503,7 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
 | 
					    await this.assetRepository.upsertExif(newExif);
 | 
				
			||||||
    await this.assetRepository.save({ id: asset.id, duration: durationString, fileCreatedAt });
 | 
					    await this.assetRepository.save({ id: asset.id, duration: durationString, fileCreatedAt });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										136
									
								
								server/test/e2e/asset.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								server/test/e2e/asset.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					import { IAssetRepository, LoginResponseDto } from '@app/domain';
 | 
				
			||||||
 | 
					import { AppModule, AssetController } from '@app/immich';
 | 
				
			||||||
 | 
					import { AssetEntity, AssetType } from '@app/infra/entities';
 | 
				
			||||||
 | 
					import { INestApplication } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { randomBytes } from 'crypto';
 | 
				
			||||||
 | 
					import request from 'supertest';
 | 
				
			||||||
 | 
					import { errorStub, uuidStub } from '../fixtures';
 | 
				
			||||||
 | 
					import { api, db } from '../test-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const user1Dto = {
 | 
				
			||||||
 | 
					  email: 'user1@immich.app',
 | 
				
			||||||
 | 
					  password: 'Password123',
 | 
				
			||||||
 | 
					  firstName: 'User 1',
 | 
				
			||||||
 | 
					  lastName: 'Test',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const user2Dto = {
 | 
				
			||||||
 | 
					  email: 'user2@immich.app',
 | 
				
			||||||
 | 
					  password: 'Password123',
 | 
				
			||||||
 | 
					  firstName: 'User 2',
 | 
				
			||||||
 | 
					  lastName: 'Test',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let assetCount = 0;
 | 
				
			||||||
 | 
					const createAsset = (repository: IAssetRepository, loginResponse: LoginResponseDto): Promise<AssetEntity> => {
 | 
				
			||||||
 | 
					  const id = assetCount++;
 | 
				
			||||||
 | 
					  return repository.save({
 | 
				
			||||||
 | 
					    ownerId: loginResponse.userId,
 | 
				
			||||||
 | 
					    checksum: randomBytes(20),
 | 
				
			||||||
 | 
					    originalPath: `/tests/test_${id}`,
 | 
				
			||||||
 | 
					    deviceAssetId: `test_${id}`,
 | 
				
			||||||
 | 
					    deviceId: 'e2e-test',
 | 
				
			||||||
 | 
					    fileCreatedAt: new Date(),
 | 
				
			||||||
 | 
					    fileModifiedAt: new Date(),
 | 
				
			||||||
 | 
					    type: AssetType.IMAGE,
 | 
				
			||||||
 | 
					    originalFileName: `test_${id}`,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe(`${AssetController.name} (e2e)`, () => {
 | 
				
			||||||
 | 
					  let app: INestApplication;
 | 
				
			||||||
 | 
					  let server: any;
 | 
				
			||||||
 | 
					  let assetRepository: IAssetRepository;
 | 
				
			||||||
 | 
					  let user1: LoginResponseDto;
 | 
				
			||||||
 | 
					  let user2: LoginResponseDto;
 | 
				
			||||||
 | 
					  let asset1: AssetEntity;
 | 
				
			||||||
 | 
					  let asset2: AssetEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeAll(async () => {
 | 
				
			||||||
 | 
					    const moduleFixture: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      imports: [AppModule],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app = await moduleFixture.createNestApplication().init();
 | 
				
			||||||
 | 
					    server = app.getHttpServer();
 | 
				
			||||||
 | 
					    assetRepository = app.get<IAssetRepository>(IAssetRepository);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    await db.reset();
 | 
				
			||||||
 | 
					    await api.adminSignUp(server);
 | 
				
			||||||
 | 
					    const admin = await api.adminLogin(server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await api.userApi.create(server, admin.accessToken, user1Dto);
 | 
				
			||||||
 | 
					    user1 = await api.login(server, { email: user1Dto.email, password: user1Dto.password });
 | 
				
			||||||
 | 
					    asset1 = await createAsset(assetRepository, user1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await api.userApi.create(server, admin.accessToken, user2Dto);
 | 
				
			||||||
 | 
					    user2 = await api.login(server, { email: user2Dto.email, password: user2Dto.password });
 | 
				
			||||||
 | 
					    asset2 = await createAsset(assetRepository, user2);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterAll(async () => {
 | 
				
			||||||
 | 
					    await db.disconnect();
 | 
				
			||||||
 | 
					    await app.close();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('PUT /asset/:id', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(server).put(`/asset/:${uuidStub.notFound}`);
 | 
				
			||||||
 | 
					      expect(status).toBe(401);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorStub.unauthorized);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should require a valid id', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(server)
 | 
				
			||||||
 | 
					        .put(`/asset/${uuidStub.invalid}`)
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					      expect(status).toBe(400);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorStub.badRequest);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should require access', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(server)
 | 
				
			||||||
 | 
					        .put(`/asset/${asset2.id}`)
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
				
			||||||
 | 
					      expect(status).toBe(400);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorStub.noPermission);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should favorite an asset', async () => {
 | 
				
			||||||
 | 
					      expect(asset1).toMatchObject({ isFavorite: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { status, body } = await request(server)
 | 
				
			||||||
 | 
					        .put(`/asset/${asset1.id}`)
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
 | 
					        .send({ isFavorite: true });
 | 
				
			||||||
 | 
					      expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
 | 
				
			||||||
 | 
					      expect(status).toEqual(200);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should archive an asset', async () => {
 | 
				
			||||||
 | 
					      expect(asset1).toMatchObject({ isArchived: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { status, body } = await request(server)
 | 
				
			||||||
 | 
					        .put(`/asset/${asset1.id}`)
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
 | 
					        .send({ isArchived: true });
 | 
				
			||||||
 | 
					      expect(body).toMatchObject({ id: asset1.id, isArchived: true });
 | 
				
			||||||
 | 
					      expect(status).toEqual(200);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should set the description', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(server)
 | 
				
			||||||
 | 
					        .put(`/asset/${asset1.id}`)
 | 
				
			||||||
 | 
					        .set('Authorization', `Bearer ${user1.accessToken}`)
 | 
				
			||||||
 | 
					        .send({ description: 'Test asset description' });
 | 
				
			||||||
 | 
					      expect(body).toMatchObject({
 | 
				
			||||||
 | 
					        id: asset1.id,
 | 
				
			||||||
 | 
					        exifInfo: expect.objectContaining({ description: 'Test asset description' }),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(status).toEqual(200);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										5
									
								
								server/test/fixtures/error.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								server/test/fixtures/error.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -24,6 +24,11 @@ export const errorStub = {
 | 
				
			|||||||
    statusCode: 400,
 | 
					    statusCode: 400,
 | 
				
			||||||
    message: expect.any(Array),
 | 
					    message: expect.any(Array),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  noPermission: {
 | 
				
			||||||
 | 
					    error: 'Bad Request',
 | 
				
			||||||
 | 
					    statusCode: 400,
 | 
				
			||||||
 | 
					    message: expect.stringContaining('Not found or no'),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  incorrectLogin: {
 | 
					  incorrectLogin: {
 | 
				
			||||||
    error: 'Unauthorized',
 | 
					    error: 'Unauthorized',
 | 
				
			||||||
    statusCode: 401,
 | 
					    statusCode: 401,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { IAssetRepository } from '@app/domain';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
 | 
					export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
 | 
					    upsertExif: jest.fn(),
 | 
				
			||||||
    getByDate: jest.fn(),
 | 
					    getByDate: jest.fn(),
 | 
				
			||||||
    getByIds: jest.fn().mockResolvedValue([]),
 | 
					    getByIds: jest.fn().mockResolvedValue([]),
 | 
				
			||||||
    getByAlbumId: jest.fn(),
 | 
					    getByAlbumId: jest.fn(),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -3417,12 +3417,6 @@ export interface UpdateAssetDto {
 | 
				
			|||||||
     * @memberof UpdateAssetDto
 | 
					     * @memberof UpdateAssetDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'isFavorite'?: boolean;
 | 
					    'isFavorite'?: boolean;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {Array<string>}
 | 
					 | 
				
			||||||
     * @memberof UpdateAssetDto
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'tagIds'?: Array<string>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
@ -6299,7 +6293,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Update an asset
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {string} id 
 | 
				
			||||||
         * @param {UpdateAssetDto} updateAssetDto 
 | 
					         * @param {UpdateAssetDto} updateAssetDto 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -6778,7 +6772,7 @@ export const AssetApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Update an asset
 | 
					         * 
 | 
				
			||||||
         * @param {string} id 
 | 
					         * @param {string} id 
 | 
				
			||||||
         * @param {UpdateAssetDto} updateAssetDto 
 | 
					         * @param {UpdateAssetDto} updateAssetDto 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -7035,7 +7029,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
 | 
				
			|||||||
            return localVarFp.serveFile(requestParameters.id, requestParameters.isThumb, requestParameters.isWeb, requestParameters.key, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.serveFile(requestParameters.id, requestParameters.isThumb, requestParameters.isWeb, requestParameters.key, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Update an asset
 | 
					         * 
 | 
				
			||||||
         * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
					         * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
@ -7952,7 +7946,7 @@ export class AssetApi extends BaseAPI {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Update an asset
 | 
					     * 
 | 
				
			||||||
     * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
					     * @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
     * @throws {RequiredError}
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user