refactor(server): update asset endpoint (#3973)
* refactor(server): update asset * chore: open api
This commit is contained in:
parent
26bc889f8d
commit
454737ca79
27 changed files with 237 additions and 184 deletions
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…
Reference in a new issue