mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(server)!: add/remove album assets (#3109)
* refactor: add/remove album assets * chore: open api * feat: remove owned assets from album * refactor: move to bulk id req/res dto * chore: open api * chore: merge main * dev: mobile work * fix: adding asset from web not sync with mobile * remove print statement --------- Co-authored-by: Alex Tran <Alex.Tran@conductix.com>
This commit is contained in:
parent
ba71c83948
commit
b9cda59172
116
cli/src/api/open-api/api.ts
generated
116
cli/src/api/open-api/api.ts
generated
@ -99,44 +99,6 @@ export interface APIKeyUpdateDto {
|
|||||||
*/
|
*/
|
||||||
'name': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface AddAssetsDto
|
|
||||||
*/
|
|
||||||
export interface AddAssetsDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof AddAssetsDto
|
|
||||||
*/
|
|
||||||
'assetIds': Array<string>;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
export interface AddAssetsResponseDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {AlbumResponseDto}
|
|
||||||
* @memberof AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
'album'?: AlbumResponseDto;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
'alreadyInAlbum': Array<string>;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
* @memberof AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
'successfullyAdded': number;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -821,6 +783,19 @@ export const BulkIdResponseDtoErrorEnum = {
|
|||||||
|
|
||||||
export type BulkIdResponseDtoErrorEnum = typeof BulkIdResponseDtoErrorEnum[keyof typeof BulkIdResponseDtoErrorEnum];
|
export type BulkIdResponseDtoErrorEnum = typeof BulkIdResponseDtoErrorEnum[keyof typeof BulkIdResponseDtoErrorEnum];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface BulkIdsDto
|
||||||
|
*/
|
||||||
|
export interface BulkIdsDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof BulkIdsDto
|
||||||
|
*/
|
||||||
|
'ids': Array<string>;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -1927,19 +1902,6 @@ export interface QueueStatusDto {
|
|||||||
*/
|
*/
|
||||||
'isPaused': boolean;
|
'isPaused': boolean;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface RemoveAssetsDto
|
|
||||||
*/
|
|
||||||
export interface RemoveAssetsDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof RemoveAssetsDto
|
|
||||||
*/
|
|
||||||
'assetIds': Array<string>;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -3678,16 +3640,16 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {AddAssetsDto} addAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
addAssetsToAlbum: async (id: string, addAssetsDto: AddAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
addAssetsToAlbum: async (id: string, bulkIdsDto: BulkIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'id' is not null or undefined
|
// verify required parameter 'id' is not null or undefined
|
||||||
assertParamExists('addAssetsToAlbum', 'id', id)
|
assertParamExists('addAssetsToAlbum', 'id', id)
|
||||||
// verify required parameter 'addAssetsDto' is not null or undefined
|
// verify required parameter 'bulkIdsDto' is not null or undefined
|
||||||
assertParamExists('addAssetsToAlbum', 'addAssetsDto', addAssetsDto)
|
assertParamExists('addAssetsToAlbum', 'bulkIdsDto', bulkIdsDto)
|
||||||
const localVarPath = `/album/{id}/assets`
|
const localVarPath = `/album/{id}/assets`
|
||||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
@ -3721,7 +3683,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration)
|
localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
@ -3998,15 +3960,15 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {RemoveAssetsDto} removeAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
removeAssetFromAlbum: async (id: string, removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
removeAssetFromAlbum: async (id: string, bulkIdsDto: BulkIdsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'id' is not null or undefined
|
// verify required parameter 'id' is not null or undefined
|
||||||
assertParamExists('removeAssetFromAlbum', 'id', id)
|
assertParamExists('removeAssetFromAlbum', 'id', id)
|
||||||
// verify required parameter 'removeAssetsDto' is not null or undefined
|
// verify required parameter 'bulkIdsDto' is not null or undefined
|
||||||
assertParamExists('removeAssetFromAlbum', 'removeAssetsDto', removeAssetsDto)
|
assertParamExists('removeAssetFromAlbum', 'bulkIdsDto', bulkIdsDto)
|
||||||
const localVarPath = `/album/{id}/assets`
|
const localVarPath = `/album/{id}/assets`
|
||||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
@ -4036,7 +3998,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration)
|
localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
@ -4150,13 +4112,13 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {AddAssetsDto} addAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async addAssetsToAlbum(id: string, addAssetsDto: AddAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AddAssetsResponseDto>> {
|
async addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, addAssetsDto, key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, bulkIdsDto, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -4224,12 +4186,12 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {RemoveAssetsDto} removeAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async removeAssetFromAlbum(id: string, removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumResponseDto>> {
|
async removeAssetFromAlbum(id: string, bulkIdsDto: BulkIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, removeAssetsDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, bulkIdsDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -4270,8 +4232,8 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<AddAssetsResponseDto> {
|
addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||||
return localVarFp.addAssetsToAlbum(requestParameters.id, requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(axios, basePath));
|
return localVarFp.addAssetsToAlbum(requestParameters.id, requestParameters.bulkIdsDto, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -4332,8 +4294,8 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<AlbumResponseDto> {
|
removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||||
return localVarFp.removeAssetFromAlbum(requestParameters.id, requestParameters.removeAssetsDto, options).then((request) => request(axios, basePath));
|
return localVarFp.removeAssetFromAlbum(requestParameters.id, requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -4371,10 +4333,10 @@ export interface AlbumApiAddAssetsToAlbumRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {AddAssetsDto}
|
* @type {BulkIdsDto}
|
||||||
* @memberof AlbumApiAddAssetsToAlbum
|
* @memberof AlbumApiAddAssetsToAlbum
|
||||||
*/
|
*/
|
||||||
readonly addAssetsDto: AddAssetsDto
|
readonly bulkIdsDto: BulkIdsDto
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -4490,10 +4452,10 @@ export interface AlbumApiRemoveAssetFromAlbumRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {RemoveAssetsDto}
|
* @type {BulkIdsDto}
|
||||||
* @memberof AlbumApiRemoveAssetFromAlbum
|
* @memberof AlbumApiRemoveAssetFromAlbum
|
||||||
*/
|
*/
|
||||||
readonly removeAssetsDto: RemoveAssetsDto
|
readonly bulkIdsDto: BulkIdsDto
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4553,7 +4515,7 @@ export class AlbumApi extends BaseAPI {
|
|||||||
* @memberof AlbumApi
|
* @memberof AlbumApi
|
||||||
*/
|
*/
|
||||||
public addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig) {
|
public addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig) {
|
||||||
return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.bulkIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4629,7 +4591,7 @@ export class AlbumApi extends BaseAPI {
|
|||||||
* @memberof AlbumApi
|
* @memberof AlbumApi
|
||||||
*/
|
*/
|
||||||
public removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig) {
|
public removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig) {
|
||||||
return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.removeAssetsDto, options).then((request) => request(this.axios, this.basePath));
|
return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class AddAssetsResponse {
|
||||||
|
List<String> alreadyInAlbum;
|
||||||
|
int successfullyAdded;
|
||||||
|
|
||||||
|
AddAssetsResponse({
|
||||||
|
required this.alreadyInAlbum,
|
||||||
|
required this.successfullyAdded,
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssetsResponse copyWith({
|
||||||
|
List<String>? alreadyInAlbum,
|
||||||
|
int? successfullyAdded,
|
||||||
|
}) {
|
||||||
|
return AddAssetsResponse(
|
||||||
|
alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum,
|
||||||
|
successfullyAdded: successfullyAdded ?? this.successfullyAdded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'alreadyInAlbum': alreadyInAlbum,
|
||||||
|
'successfullyAdded': successfullyAdded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant AddAssetsResponse other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return listEquals(other.alreadyInAlbum, alreadyInAlbum) &&
|
||||||
|
other.successfullyAdded == successfullyAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
|
|
||||||
|
final albumDetailProvider =
|
||||||
|
StreamProvider.family<Album, int>((ref, albumId) async* {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) return;
|
||||||
|
final AlbumService service = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
|
await for (final a in service.watchAlbum(albumId)) {
|
||||||
|
if (a == null) {
|
||||||
|
throw Exception("Album with ID=$albumId does not exist anymore!");
|
||||||
|
}
|
||||||
|
await for (final _ in a.watchRenderList(GroupAssetsBy.none)) {
|
||||||
|
yield a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -3,12 +3,10 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
@ -72,19 +70,3 @@ final sharedAlbumProvider =
|
|||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider =
|
|
||||||
StreamProvider.family<Album, int>((ref, albumId) async* {
|
|
||||||
final user = ref.watch(currentUserProvider);
|
|
||||||
if (user == null) return;
|
|
||||||
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
|
|
||||||
|
|
||||||
await for (final a in sharedAlbumService.watchAlbum(albumId)) {
|
|
||||||
if (a == null) {
|
|
||||||
throw Exception("Album with ID=$albumId does not exist anymore!");
|
|
||||||
}
|
|
||||||
await for (final _ in a.watchRenderList(GroupAssetsBy.none)) {
|
|
||||||
yield a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/models/add_asset_response.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
@ -219,24 +220,43 @@ class AlbumService {
|
|||||||
yield* _db.albums.watchObject(albumId);
|
yield* _db.albums.watchObject(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
Future<AddAssetsResponse?> addAdditionalAssetToAlbum(
|
||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
Album album,
|
Album album,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
var result = await _apiService.albumApi.addAssetsToAlbum(
|
var response = await _apiService.albumApi.addAssetsToAlbum(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
AddAssetsDto(assetIds: assets.map((asset) => asset.remoteId!).toList()),
|
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
|
||||||
);
|
);
|
||||||
if (result != null && result.successfullyAdded > 0) {
|
|
||||||
album.assets.addAll(assets);
|
if (response != null) {
|
||||||
|
List<Asset> successAssets = [];
|
||||||
|
List<String> duplicatedAssets = [];
|
||||||
|
|
||||||
|
for (final result in response) {
|
||||||
|
if (result.success) {
|
||||||
|
successAssets
|
||||||
|
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
|
||||||
|
} else if (!result.success &&
|
||||||
|
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
||||||
|
duplicatedAssets.add(result.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
album.assets.addAll(successAssets);
|
||||||
await _db.writeTxn(() => album.assets.save());
|
await _db.writeTxn(() => album.assets.save());
|
||||||
|
|
||||||
|
return AddAssetsResponse(
|
||||||
|
alreadyInAlbum: duplicatedAssets,
|
||||||
|
successfullyAdded: successAssets.length,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
@ -314,8 +334,8 @@ class AlbumService {
|
|||||||
try {
|
try {
|
||||||
await _apiService.albumApi.removeAssetFromAlbum(
|
await _apiService.albumApi.removeAssetFromAlbum(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
RemoveAssetsDto(
|
BulkIdsDto(
|
||||||
assetIds: assets.map((e) => e.remoteId!).toList(growable: false),
|
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
album.assets.removeAll(assets);
|
album.assets.removeAll(assets);
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||||
@ -63,9 +64,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@ -99,7 +100,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
selectionDisabled();
|
selectionDisabled();
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
ref.invalidate(sharedAlbumDetailProvider(album.id));
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
@ -6,13 +6,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
@ -28,11 +27,20 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
final album = ref.watch(sharedAlbumDetailProvider(albumId));
|
final album = ref.watch(albumDetailProvider(albumId));
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
final selection = useState<Set<Asset>>({});
|
final selection = useState<Set<Asset>>({});
|
||||||
final multiSelectEnabled = useState(false);
|
final multiSelectEnabled = useState(false);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
// Fetch album updates, e.g., cover image
|
||||||
|
ref.invalidate(albumDetailProvider(albumId));
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Future<bool> onWillPop() async {
|
Future<bool> onWillPop() async {
|
||||||
if (multiSelectEnabled.value) {
|
if (multiSelectEnabled.value) {
|
||||||
selection.value = {};
|
selection.value = {};
|
||||||
@ -77,8 +85,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (addAssetsResult != null &&
|
if (addAssetsResult != null &&
|
||||||
addAssetsResult.successfullyAdded > 0) {
|
addAssetsResult.successfullyAdded > 0) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.invalidate(albumDetailProvider(albumId));
|
||||||
ref.invalidate(sharedAlbumDetailProvider(albumId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
@ -100,7 +107,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
.addAdditionalUserToAlbum(sharedUserIds, album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.invalidate(sharedAlbumDetailProvider(album.id));
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLoadingOverlayController.appLoader.hide();
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
|
@ -8,6 +8,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
@ -208,6 +209,9 @@ class HomePage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
toastType: ToastType.success,
|
toastType: ToastType.success,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
12
mobile/openapi/.openapi-generator/FILES
generated
12
mobile/openapi/.openapi-generator/FILES
generated
@ -8,8 +8,6 @@ doc/APIKeyCreateDto.md
|
|||||||
doc/APIKeyCreateResponseDto.md
|
doc/APIKeyCreateResponseDto.md
|
||||||
doc/APIKeyResponseDto.md
|
doc/APIKeyResponseDto.md
|
||||||
doc/APIKeyUpdateDto.md
|
doc/APIKeyUpdateDto.md
|
||||||
doc/AddAssetsDto.md
|
|
||||||
doc/AddAssetsResponseDto.md
|
|
||||||
doc/AddUsersDto.md
|
doc/AddUsersDto.md
|
||||||
doc/AdminSignupResponseDto.md
|
doc/AdminSignupResponseDto.md
|
||||||
doc/AlbumApi.md
|
doc/AlbumApi.md
|
||||||
@ -33,6 +31,7 @@ doc/AudioCodec.md
|
|||||||
doc/AuthDeviceResponseDto.md
|
doc/AuthDeviceResponseDto.md
|
||||||
doc/AuthenticationApi.md
|
doc/AuthenticationApi.md
|
||||||
doc/BulkIdResponseDto.md
|
doc/BulkIdResponseDto.md
|
||||||
|
doc/BulkIdsDto.md
|
||||||
doc/ChangePasswordDto.md
|
doc/ChangePasswordDto.md
|
||||||
doc/CheckDuplicateAssetDto.md
|
doc/CheckDuplicateAssetDto.md
|
||||||
doc/CheckDuplicateAssetResponseDto.md
|
doc/CheckDuplicateAssetResponseDto.md
|
||||||
@ -78,7 +77,6 @@ doc/PersonApi.md
|
|||||||
doc/PersonResponseDto.md
|
doc/PersonResponseDto.md
|
||||||
doc/PersonUpdateDto.md
|
doc/PersonUpdateDto.md
|
||||||
doc/QueueStatusDto.md
|
doc/QueueStatusDto.md
|
||||||
doc/RemoveAssetsDto.md
|
|
||||||
doc/SearchAlbumResponseDto.md
|
doc/SearchAlbumResponseDto.md
|
||||||
doc/SearchApi.md
|
doc/SearchApi.md
|
||||||
doc/SearchAssetDto.md
|
doc/SearchAssetDto.md
|
||||||
@ -150,8 +148,6 @@ lib/auth/authentication.dart
|
|||||||
lib/auth/http_basic_auth.dart
|
lib/auth/http_basic_auth.dart
|
||||||
lib/auth/http_bearer_auth.dart
|
lib/auth/http_bearer_auth.dart
|
||||||
lib/auth/oauth.dart
|
lib/auth/oauth.dart
|
||||||
lib/model/add_assets_dto.dart
|
|
||||||
lib/model/add_assets_response_dto.dart
|
|
||||||
lib/model/add_users_dto.dart
|
lib/model/add_users_dto.dart
|
||||||
lib/model/admin_signup_response_dto.dart
|
lib/model/admin_signup_response_dto.dart
|
||||||
lib/model/album_count_response_dto.dart
|
lib/model/album_count_response_dto.dart
|
||||||
@ -176,6 +172,7 @@ lib/model/asset_type_enum.dart
|
|||||||
lib/model/audio_codec.dart
|
lib/model/audio_codec.dart
|
||||||
lib/model/auth_device_response_dto.dart
|
lib/model/auth_device_response_dto.dart
|
||||||
lib/model/bulk_id_response_dto.dart
|
lib/model/bulk_id_response_dto.dart
|
||||||
|
lib/model/bulk_ids_dto.dart
|
||||||
lib/model/change_password_dto.dart
|
lib/model/change_password_dto.dart
|
||||||
lib/model/check_duplicate_asset_dto.dart
|
lib/model/check_duplicate_asset_dto.dart
|
||||||
lib/model/check_duplicate_asset_response_dto.dart
|
lib/model/check_duplicate_asset_response_dto.dart
|
||||||
@ -217,7 +214,6 @@ lib/model/people_update_item.dart
|
|||||||
lib/model/person_response_dto.dart
|
lib/model/person_response_dto.dart
|
||||||
lib/model/person_update_dto.dart
|
lib/model/person_update_dto.dart
|
||||||
lib/model/queue_status_dto.dart
|
lib/model/queue_status_dto.dart
|
||||||
lib/model/remove_assets_dto.dart
|
|
||||||
lib/model/search_album_response_dto.dart
|
lib/model/search_album_response_dto.dart
|
||||||
lib/model/search_asset_dto.dart
|
lib/model/search_asset_dto.dart
|
||||||
lib/model/search_asset_response_dto.dart
|
lib/model/search_asset_response_dto.dart
|
||||||
@ -260,8 +256,6 @@ lib/model/user_response_dto.dart
|
|||||||
lib/model/validate_access_token_response_dto.dart
|
lib/model/validate_access_token_response_dto.dart
|
||||||
lib/model/video_codec.dart
|
lib/model/video_codec.dart
|
||||||
pubspec.yaml
|
pubspec.yaml
|
||||||
test/add_assets_dto_test.dart
|
|
||||||
test/add_assets_response_dto_test.dart
|
|
||||||
test/add_users_dto_test.dart
|
test/add_users_dto_test.dart
|
||||||
test/admin_signup_response_dto_test.dart
|
test/admin_signup_response_dto_test.dart
|
||||||
test/album_api_test.dart
|
test/album_api_test.dart
|
||||||
@ -290,6 +284,7 @@ test/audio_codec_test.dart
|
|||||||
test/auth_device_response_dto_test.dart
|
test/auth_device_response_dto_test.dart
|
||||||
test/authentication_api_test.dart
|
test/authentication_api_test.dart
|
||||||
test/bulk_id_response_dto_test.dart
|
test/bulk_id_response_dto_test.dart
|
||||||
|
test/bulk_ids_dto_test.dart
|
||||||
test/change_password_dto_test.dart
|
test/change_password_dto_test.dart
|
||||||
test/check_duplicate_asset_dto_test.dart
|
test/check_duplicate_asset_dto_test.dart
|
||||||
test/check_duplicate_asset_response_dto_test.dart
|
test/check_duplicate_asset_response_dto_test.dart
|
||||||
@ -335,7 +330,6 @@ test/person_api_test.dart
|
|||||||
test/person_response_dto_test.dart
|
test/person_response_dto_test.dart
|
||||||
test/person_update_dto_test.dart
|
test/person_update_dto_test.dart
|
||||||
test/queue_status_dto_test.dart
|
test/queue_status_dto_test.dart
|
||||||
test/remove_assets_dto_test.dart
|
|
||||||
test/search_album_response_dto_test.dart
|
test/search_album_response_dto_test.dart
|
||||||
test/search_api_test.dart
|
test/search_api_test.dart
|
||||||
test/search_asset_dto_test.dart
|
test/search_asset_dto_test.dart
|
||||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AddAssetsResponseDto.md
generated
BIN
mobile/openapi/doc/AddAssetsResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AlbumApi.md
generated
BIN
mobile/openapi/doc/AlbumApi.md
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/doc/RemoveAssetsDto.md
generated
BIN
mobile/openapi/doc/RemoveAssetsDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/album_api.dart
generated
BIN
mobile/openapi/lib/api/album_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/add_assets_response_dto.dart
generated
BIN
mobile/openapi/lib/model/add_assets_response_dto.dart
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/remove_assets_dto.dart
generated
BIN
mobile/openapi/lib/model/remove_assets_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/add_assets_response_dto_test.dart
generated
BIN
mobile/openapi/test/add_assets_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/album_api_test.dart
generated
BIN
mobile/openapi/test/album_api_test.dart
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/test/remove_assets_dto_test.dart
generated
BIN
mobile/openapi/test/remove_assets_dto_test.dart
generated
Binary file not shown.
@ -278,7 +278,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/RemoveAssetsDto"
|
"$ref": "#/components/schemas/BulkIdsDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -289,7 +289,10 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/AlbumResponseDto"
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/BulkIdResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -336,7 +339,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/AddAssetsDto"
|
"$ref": "#/components/schemas/BulkIdsDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -347,7 +350,10 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/AddAssetsResponseDto"
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/BulkIdResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -4535,42 +4541,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"AddAssetsDto": {
|
|
||||||
"properties": {
|
|
||||||
"assetIds": {
|
|
||||||
"items": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"assetIds"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"AddAssetsResponseDto": {
|
|
||||||
"properties": {
|
|
||||||
"album": {
|
|
||||||
"$ref": "#/components/schemas/AlbumResponseDto"
|
|
||||||
},
|
|
||||||
"alreadyInAlbum": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
},
|
|
||||||
"successfullyAdded": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"successfullyAdded",
|
|
||||||
"alreadyInAlbum"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"AddUsersDto": {
|
"AddUsersDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"sharedUserIds": {
|
"sharedUserIds": {
|
||||||
@ -5093,6 +5063,21 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"BulkIdsDto": {
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"items": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ChangePasswordDto": {
|
"ChangePasswordDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"newPassword": {
|
"newPassword": {
|
||||||
@ -6055,21 +6040,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"RemoveAssetsDto": {
|
|
||||||
"properties": {
|
|
||||||
"assetIds": {
|
|
||||||
"items": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"assetIds"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SearchAlbumResponseDto": {
|
"SearchAlbumResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"count": {
|
"count": {
|
||||||
|
@ -12,9 +12,10 @@ export enum Permission {
|
|||||||
ASSET_DOWNLOAD = 'asset.download',
|
ASSET_DOWNLOAD = 'asset.download',
|
||||||
|
|
||||||
// ALBUM_CREATE = 'album.create',
|
// ALBUM_CREATE = 'album.create',
|
||||||
// ALBUM_READ = 'album.read',
|
ALBUM_READ = 'album.read',
|
||||||
ALBUM_UPDATE = 'album.update',
|
ALBUM_UPDATE = 'album.update',
|
||||||
ALBUM_DELETE = 'album.delete',
|
ALBUM_DELETE = 'album.delete',
|
||||||
|
ALBUM_REMOVE_ASSET = 'album.removeAsset',
|
||||||
ALBUM_SHARE = 'album.share',
|
ALBUM_SHARE = 'album.share',
|
||||||
ALBUM_DOWNLOAD = 'album.download',
|
ALBUM_DOWNLOAD = 'album.download',
|
||||||
|
|
||||||
@ -39,6 +40,16 @@ export class AccessCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hasAny(authUser: AuthUserDto, permissions: Array<{ permission: Permission; id: string }>) {
|
||||||
|
for (const { permission, id } of permissions) {
|
||||||
|
const hasAccess = await this.hasPermission(authUser, permission, id);
|
||||||
|
if (hasAccess) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
|
async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
|
||||||
ids = Array.isArray(ids) ? ids : [ids];
|
ids = Array.isArray(ids) ? ids : [ids];
|
||||||
|
|
||||||
@ -76,12 +87,11 @@ export class AccessCore {
|
|||||||
// TODO: fix this to not use authUser.id for shared link access control
|
// TODO: fix this to not use authUser.id for shared link access control
|
||||||
return this.repository.asset.hasOwnerAccess(authUser.id, id);
|
return this.repository.asset.hasOwnerAccess(authUser.id, id);
|
||||||
|
|
||||||
case Permission.ALBUM_DOWNLOAD: {
|
case Permission.ALBUM_READ:
|
||||||
return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id));
|
return this.repository.album.hasSharedLinkAccess(sharedLinkId, id);
|
||||||
}
|
|
||||||
|
|
||||||
// case Permission.ALBUM_READ:
|
case Permission.ALBUM_DOWNLOAD:
|
||||||
// return this.repository.album.hasSharedLinkAccess(sharedLinkId, id);
|
return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@ -122,8 +132,11 @@ export class AccessCore {
|
|||||||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
|
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
|
||||||
);
|
);
|
||||||
|
|
||||||
// case Permission.ALBUM_READ:
|
case Permission.ALBUM_READ:
|
||||||
// return this.repository.album.hasOwnerAccess(authUser.id, id);
|
return (
|
||||||
|
(await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
|
||||||
|
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
|
||||||
|
);
|
||||||
|
|
||||||
case Permission.ALBUM_UPDATE:
|
case Permission.ALBUM_UPDATE:
|
||||||
return this.repository.album.hasOwnerAccess(authUser.id, id);
|
return this.repository.album.hasOwnerAccess(authUser.id, id);
|
||||||
@ -140,13 +153,17 @@ export class AccessCore {
|
|||||||
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
|
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case Permission.ALBUM_REMOVE_ASSET:
|
||||||
|
return this.repository.album.hasOwnerAccess(authUser.id, id);
|
||||||
|
|
||||||
case Permission.LIBRARY_READ:
|
case Permission.LIBRARY_READ:
|
||||||
return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id));
|
return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id));
|
||||||
|
|
||||||
case Permission.LIBRARY_DOWNLOAD:
|
case Permission.LIBRARY_DOWNLOAD:
|
||||||
return authUser.id === id;
|
return authUser.id === id;
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ export interface AlbumAssetCount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IAlbumRepository {
|
export interface IAlbumRepository {
|
||||||
|
getById(id: string): Promise<AlbumEntity | null>;
|
||||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||||
hasAsset(id: string, assetId: string): Promise<boolean>;
|
hasAsset(id: string, assetId: string): Promise<boolean>;
|
||||||
@ -21,4 +22,5 @@ export interface IAlbumRepository {
|
|||||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||||
delete(album: AlbumEntity): Promise<void>;
|
delete(album: AlbumEntity): Promise<void>;
|
||||||
|
updateThumbnails(): Promise<number | undefined>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
albumStub,
|
albumStub,
|
||||||
|
assetStub,
|
||||||
authStub,
|
authStub,
|
||||||
IAccessRepositoryMock,
|
IAccessRepositoryMock,
|
||||||
newAccessRepositoryMock,
|
newAccessRepositoryMock,
|
||||||
@ -11,7 +12,7 @@ import {
|
|||||||
userStub,
|
userStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { IAssetRepository } from '../asset';
|
import { BulkIdErrorReason, IAssetRepository } from '../asset';
|
||||||
import { IJobRepository, JobName } from '../job';
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { IUserRepository } from '../user';
|
import { IUserRepository } from '../user';
|
||||||
import { IAlbumRepository } from './album.repository';
|
import { IAlbumRepository } from './album.repository';
|
||||||
@ -202,7 +203,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('should prevent updating an album that does not exist', async () => {
|
it('should prevent updating an album that does not exist', async () => {
|
||||||
albumMock.getByIds.mockResolvedValue([]);
|
albumMock.getById.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.update(authStub.user1, 'invalid-id', {
|
sut.update(authStub.user1, 'invalid-id', {
|
||||||
@ -224,7 +225,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should require a valid thumbnail asset id', async () => {
|
it('should require a valid thumbnail asset id', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.oneAsset]);
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
||||||
albumMock.hasAsset.mockResolvedValue(false);
|
albumMock.hasAsset.mockResolvedValue(false);
|
||||||
|
|
||||||
@ -241,7 +242,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should allow the owner to update the album', async () => {
|
it('should allow the owner to update the album', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.oneAsset]);
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
|
||||||
await sut.update(authStub.admin, albumStub.oneAsset.id, {
|
await sut.update(authStub.admin, albumStub.oneAsset.id, {
|
||||||
@ -263,7 +264,7 @@ describe(AlbumService.name, () => {
|
|||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
it('should throw an error for an album not found', async () => {
|
it('should throw an error for an album not found', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([]);
|
albumMock.getById.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@ -274,7 +275,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should not let a shared user delete the album', async () => {
|
it('should not let a shared user delete the album', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||||
|
|
||||||
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@ -285,7 +286,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should let the owner delete an album', async () => {
|
it('should let the owner delete an album', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.empty]);
|
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||||
|
|
||||||
await sut.delete(authStub.admin, albumStub.empty.id);
|
await sut.delete(authStub.admin, albumStub.empty.id);
|
||||||
|
|
||||||
@ -305,7 +306,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should throw an error if the userId is already added', async () => {
|
it('should throw an error if the userId is already added', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||||
await expect(
|
await expect(
|
||||||
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }),
|
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
@ -314,7 +315,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should throw an error if the userId does not exist', async () => {
|
it('should throw an error if the userId does not exist', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||||
userMock.get.mockResolvedValue(null);
|
userMock.get.mockResolvedValue(null);
|
||||||
await expect(
|
await expect(
|
||||||
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-3'] }),
|
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-3'] }),
|
||||||
@ -324,7 +325,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should add valid shared users', async () => {
|
it('should add valid shared users', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([_.cloneDeep(albumStub.sharedWithAdmin)]);
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
|
||||||
albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
|
albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||||
userMock.get.mockResolvedValue(userStub.user2);
|
userMock.get.mockResolvedValue(userStub.user2);
|
||||||
await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.id] });
|
await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.id] });
|
||||||
@ -339,14 +340,14 @@ describe(AlbumService.name, () => {
|
|||||||
describe('removeUser', () => {
|
describe('removeUser', () => {
|
||||||
it('should require a valid album id', async () => {
|
it('should require a valid album id', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([]);
|
albumMock.getById.mockResolvedValue(null);
|
||||||
await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||||
expect(albumMock.update).not.toHaveBeenCalled();
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove a shared user from an owned album', async () => {
|
it('should remove a shared user from an owned album', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithUser]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id),
|
sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id),
|
||||||
@ -362,7 +363,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => {
|
it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithMultiple]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.id),
|
sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.id),
|
||||||
@ -373,7 +374,7 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared user to remove themselves', async () => {
|
it('should allow a shared user to remove themselves', async () => {
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithUser]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||||
|
|
||||||
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.id);
|
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.id);
|
||||||
|
|
||||||
@ -386,7 +387,7 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared user to remove themselves using "me"', async () => {
|
it('should allow a shared user to remove themselves using "me"', async () => {
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithUser]);
|
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||||
|
|
||||||
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me');
|
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me');
|
||||||
|
|
||||||
@ -399,7 +400,7 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow the owner to be removed', async () => {
|
it('should not allow the owner to be removed', async () => {
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.empty]);
|
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||||
|
|
||||||
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.id)).rejects.toBeInstanceOf(
|
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.id)).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@ -409,7 +410,7 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error for a user not in the album', async () => {
|
it('should throw an error for a user not in the album', async () => {
|
||||||
albumMock.getByIds.mockResolvedValue([albumStub.empty]);
|
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||||
|
|
||||||
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, 'user-3')).rejects.toBeInstanceOf(
|
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, 'user-3')).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@ -418,4 +419,301 @@ describe(AlbumService.name, () => {
|
|||||||
expect(albumMock.update).not.toHaveBeenCalled();
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAlbumInfo', () => {
|
||||||
|
it('should get a shared album', async () => {
|
||||||
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await sut.get(authStub.admin, albumStub.oneAsset.id);
|
||||||
|
|
||||||
|
expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id);
|
||||||
|
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a shared album via a shared link', async () => {
|
||||||
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await sut.get(authStub.adminSharedLink, 'album-123');
|
||||||
|
|
||||||
|
expect(albumMock.getById).toHaveBeenCalledWith('album-123');
|
||||||
|
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
|
||||||
|
authStub.adminSharedLink.sharedLinkId,
|
||||||
|
'album-123',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a shared album via shared with user', async () => {
|
||||||
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await sut.get(authStub.user1, 'album-123');
|
||||||
|
|
||||||
|
expect(albumMock.getById).toHaveBeenCalledWith('album-123');
|
||||||
|
expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error for no access', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await expect(sut.get(authStub.admin, 'album-123')).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
|
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123');
|
||||||
|
expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addAssets', () => {
|
||||||
|
it('should allow the owner to add assets', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-1' },
|
||||||
|
{ success: true, id: 'asset-2' },
|
||||||
|
{ success: true, id: 'asset-3' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
||||||
|
albumThumbnailAssetId: 'asset-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set the thumbnail if the album has one already', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }));
|
||||||
|
|
||||||
|
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-1' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [{ id: 'asset-1' }],
|
||||||
|
albumThumbnailAssetId: 'asset-id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a shared user to add assets', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.addAssets(authStub.user1, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-1' },
|
||||||
|
{ success: true, id: 'asset-2' },
|
||||||
|
{ success: true, id: 'asset-3' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [{ id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
||||||
|
albumThumbnailAssetId: 'asset-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a shared link user to add assets', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
|
||||||
|
accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-1' },
|
||||||
|
{ success: true, id: 'asset-2' },
|
||||||
|
{ success: true, id: 'asset-3' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
||||||
|
albumThumbnailAssetId: 'asset-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
|
||||||
|
authStub.adminSharedLink.sharedLinkId,
|
||||||
|
'album-123',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow adding assets shared via partner sharing', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
|
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-1' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [assetStub.image, { id: 'asset-1' }],
|
||||||
|
albumThumbnailAssetId: 'asset-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip duplicate assets', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
|
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
|
{ success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip assets not shared with user', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
|
||||||
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
|
||||||
|
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||||
|
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
|
||||||
|
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow unauthorized access to the album', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||||
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false);
|
||||||
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
|
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalled();
|
||||||
|
expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow unauthorized shared link access to the album', async () => {
|
||||||
|
accessMock.album.hasSharedLinkAccess.mockResolvedValue(false);
|
||||||
|
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
|
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAssets', () => {
|
||||||
|
it('should allow the owner to remove assets', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-id' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [],
|
||||||
|
albumThumbnailAssetId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip assets not in the album', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty));
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
|
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip assets without user permission to remove', async () => {
|
||||||
|
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
|
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NO_PERMISSION },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset the thumbnail if it is removed', async () => {
|
||||||
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
|
{ success: true, id: 'asset-id' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(albumMock.update).toHaveBeenCalledWith({
|
||||||
|
id: 'album-123',
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
assets: [assetStub.withLocation],
|
||||||
|
albumThumbnailAssetId: assetStub.withLocation.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// // it('removes assets from shared album (shared with auth user)', async () => {
|
||||||
|
// // const albumEntity = _getOwnedSharedAlbum();
|
||||||
|
// // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
|
// // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
|
|
||||||
|
// // await expect(
|
||||||
|
// // sut.removeAssetsFromAlbum(
|
||||||
|
// // authUser,
|
||||||
|
// // {
|
||||||
|
// // ids: ['1'],
|
||||||
|
// // },
|
||||||
|
// // albumEntity.id,
|
||||||
|
// // ),
|
||||||
|
// // ).resolves.toBeUndefined();
|
||||||
|
// // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
||||||
|
// // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
||||||
|
// // ids: ['1'],
|
||||||
|
// // });
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// it('prevents removing assets from a not owned / shared album', async () => {
|
||||||
|
// const albumEntity = _getNotOwnedNotSharedAlbum();
|
||||||
|
|
||||||
|
// const albumResponse: AddAssetsResponseDto = {
|
||||||
|
// alreadyInAlbum: [],
|
||||||
|
// successfullyAdded: 1,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const albumId = albumEntity.id;
|
||||||
|
|
||||||
|
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
|
// albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
||||||
|
|
||||||
|
// await expect(sut.removeAssets(authUser, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
|
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { IAssetRepository, mapAsset } from '../asset';
|
import { AccessCore, IAccessRepository, Permission } from '../access';
|
||||||
|
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto, IAssetRepository, mapAsset } from '../asset';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { AccessCore, IAccessRepository, Permission } from '../index';
|
|
||||||
import { IJobRepository, JobName } from '../job';
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { IUserRepository } from '../user';
|
import { IUserRepository } from '../user';
|
||||||
import { AlbumCountResponseDto, AlbumResponseDto, mapAlbum } from './album-response.dto';
|
import { AlbumCountResponseDto, AlbumResponseDto, mapAlbum } from './album-response.dto';
|
||||||
@ -37,7 +37,11 @@ export class AlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
||||||
await this.updateInvalidThumbnails();
|
const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail();
|
||||||
|
for (const albumId of invalidAlbumIds) {
|
||||||
|
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
|
||||||
|
await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail });
|
||||||
|
}
|
||||||
|
|
||||||
let albums: AlbumEntity[];
|
let albums: AlbumEntity[];
|
||||||
if (assetId) {
|
if (assetId) {
|
||||||
@ -73,15 +77,10 @@ export class AlbumService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateInvalidThumbnails(): Promise<number> {
|
async get(authUser: AuthUserDto, id: string) {
|
||||||
const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail();
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
await this.albumRepository.updateThumbnails();
|
||||||
for (const albumId of invalidAlbumIds) {
|
return mapAlbum(await this.findOrFail(id));
|
||||||
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
|
|
||||||
await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail });
|
|
||||||
}
|
|
||||||
|
|
||||||
return invalidAlbumIds.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
|
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
|
||||||
@ -107,7 +106,7 @@ export class AlbumService {
|
|||||||
async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id);
|
||||||
|
|
||||||
const album = await this.get(id);
|
const album = await this.findOrFail(id);
|
||||||
|
|
||||||
if (dto.albumThumbnailAssetId) {
|
if (dto.albumThumbnailAssetId) {
|
||||||
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
|
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
|
||||||
@ -130,7 +129,7 @@ export class AlbumService {
|
|||||||
async delete(authUser: AuthUserDto, id: string): Promise<void> {
|
async delete(authUser: AuthUserDto, id: string): Promise<void> {
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id);
|
||||||
|
|
||||||
const [album] = await this.albumRepository.getByIds([id]);
|
const album = await this.albumRepository.getById(id);
|
||||||
if (!album) {
|
if (!album) {
|
||||||
throw new BadRequestException('Album not found');
|
throw new BadRequestException('Album not found');
|
||||||
}
|
}
|
||||||
@ -139,10 +138,88 @@ export class AlbumService {
|
|||||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ALBUM, data: { ids: [id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ALBUM, data: { ids: [id] } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
|
const album = await this.findOrFail(id);
|
||||||
|
|
||||||
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
|
const results: BulkIdResponseDto[] = [];
|
||||||
|
for (const id of dto.ids) {
|
||||||
|
const hasAsset = album.assets.find((asset) => asset.id === id);
|
||||||
|
if (hasAsset) {
|
||||||
|
results.push({ id, success: false, error: BulkIdErrorReason.DUPLICATE });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, id);
|
||||||
|
if (!hasAccess) {
|
||||||
|
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ id, success: true });
|
||||||
|
album.assets.push({ id } as AssetEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAsset = results.find(({ success }) => success);
|
||||||
|
if (newAsset) {
|
||||||
|
await this.albumRepository.update({
|
||||||
|
id,
|
||||||
|
assets: album.assets,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAsset.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
|
const album = await this.findOrFail(id);
|
||||||
|
|
||||||
|
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
|
const results: BulkIdResponseDto[] = [];
|
||||||
|
for (const id of dto.ids) {
|
||||||
|
const hasAsset = album.assets.find((asset) => asset.id === id);
|
||||||
|
if (!hasAsset) {
|
||||||
|
results.push({ id, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess = await this.access.hasAny(authUser, [
|
||||||
|
{ permission: Permission.ALBUM_REMOVE_ASSET, id },
|
||||||
|
{ permission: Permission.ASSET_SHARE, id },
|
||||||
|
]);
|
||||||
|
if (!hasAccess) {
|
||||||
|
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ id, success: true });
|
||||||
|
album.assets = album.assets.filter((asset) => asset.id !== id);
|
||||||
|
if (album.albumThumbnailAssetId === id) {
|
||||||
|
album.albumThumbnailAssetId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSuccess = results.find(({ success }) => success);
|
||||||
|
if (hasSuccess) {
|
||||||
|
await this.albumRepository.update({
|
||||||
|
id,
|
||||||
|
assets: album.assets,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
albumThumbnailAssetId: album.albumThumbnailAssetId || album.assets[0]?.id || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto) {
|
async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto) {
|
||||||
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id);
|
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id);
|
||||||
|
|
||||||
const album = await this.get(id);
|
const album = await this.findOrFail(id);
|
||||||
|
|
||||||
for (const userId of dto.sharedUserIds) {
|
for (const userId of dto.sharedUserIds) {
|
||||||
const exists = album.sharedUsers.find((user) => user.id === userId);
|
const exists = album.sharedUsers.find((user) => user.id === userId);
|
||||||
@ -172,7 +249,7 @@ export class AlbumService {
|
|||||||
userId = authUser.id;
|
userId = authUser.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const album = await this.get(id);
|
const album = await this.findOrFail(id);
|
||||||
|
|
||||||
if (album.ownerId === userId) {
|
if (album.ownerId === userId) {
|
||||||
throw new BadRequestException('Cannot remove album owner');
|
throw new BadRequestException('Cannot remove album owner');
|
||||||
@ -195,8 +272,8 @@ export class AlbumService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async get(id: string) {
|
private async findOrFail(id: string) {
|
||||||
const [album] = await this.albumRepository.getByIds([id]);
|
const album = await this.albumRepository.getById(id);
|
||||||
if (!album) {
|
if (!album) {
|
||||||
throw new BadRequestException('Album not found');
|
throw new BadRequestException('Album not found');
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
/** @deprecated Use `BulkIdResponseDto` instead */
|
/** @deprecated Use `BulkIdResponseDto` instead */
|
||||||
export enum AssetIdErrorReason {
|
export enum AssetIdErrorReason {
|
||||||
DUPLICATE = 'duplicate',
|
DUPLICATE = 'duplicate',
|
||||||
@ -19,6 +21,11 @@ export enum BulkIdErrorReason {
|
|||||||
UNKNOWN = 'unknown',
|
UNKNOWN = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BulkIdsDto {
|
||||||
|
@ValidateUUID({ each: true })
|
||||||
|
ids!: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class BulkIdResponseDto {
|
export class BulkIdResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
success!: boolean;
|
success!: boolean;
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
import { dataSource } from '@app/infra/database.config';
|
|
||||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
|
||||||
|
|
||||||
export interface IAlbumRepository {
|
|
||||||
get(albumId: string): Promise<AlbumEntity | null>;
|
|
||||||
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
|
|
||||||
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
|
|
||||||
updateThumbnails(): Promise<number | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IAlbumRepository = 'IAlbumRepository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AlbumRepository implements IAlbumRepository {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
|
|
||||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async get(albumId: string): Promise<AlbumEntity | null> {
|
|
||||||
return this.albumRepository.findOne({
|
|
||||||
where: { id: albumId },
|
|
||||||
relations: {
|
|
||||||
owner: true,
|
|
||||||
sharedUsers: true,
|
|
||||||
assets: {
|
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
sharedLinks: true,
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
assets: {
|
|
||||||
fileCreatedAt: 'DESC',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
|
|
||||||
const assetCount = album.assets.length;
|
|
||||||
|
|
||||||
album.assets = album.assets.filter((asset) => {
|
|
||||||
return !removeAssetsDto.assetIds.includes(asset.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const numRemovedAssets = assetCount - album.assets.length;
|
|
||||||
if (numRemovedAssets > 0) {
|
|
||||||
album.updatedAt = new Date();
|
|
||||||
}
|
|
||||||
await this.albumRepository.save(album, {});
|
|
||||||
|
|
||||||
return numRemovedAssets;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> {
|
|
||||||
const alreadyExisting: string[] = [];
|
|
||||||
|
|
||||||
for (const assetId of addAssetsDto.assetIds) {
|
|
||||||
// Album already contains that asset
|
|
||||||
if (album.assets?.some((a) => a.id === assetId)) {
|
|
||||||
alreadyExisting.push(assetId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
album.assets.push({ id: assetId } as AssetEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add album thumbnail if not exist.
|
|
||||||
if (!album.albumThumbnailAssetId && album.assets.length > 0) {
|
|
||||||
album.albumThumbnailAssetId = album.assets[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const successfullyAdded = addAssetsDto.assetIds.length - alreadyExisting.length;
|
|
||||||
if (successfullyAdded > 0) {
|
|
||||||
album.updatedAt = new Date();
|
|
||||||
}
|
|
||||||
await this.albumRepository.save(album);
|
|
||||||
|
|
||||||
return {
|
|
||||||
successfullyAdded,
|
|
||||||
alreadyInAlbum: alreadyExisting,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes sure all thumbnails for albums are updated by:
|
|
||||||
* - Removing thumbnails from albums without assets
|
|
||||||
* - Removing references of thumbnails to assets outside the album
|
|
||||||
* - Setting a thumbnail when none is set and the album contains assets
|
|
||||||
*
|
|
||||||
* @returns Amount of updated album thumbnails or undefined when unknown
|
|
||||||
*/
|
|
||||||
async updateThumbnails(): Promise<number | undefined> {
|
|
||||||
// Subquery for getting a new thumbnail.
|
|
||||||
const newThumbnail = this.assetRepository
|
|
||||||
.createQueryBuilder('assets')
|
|
||||||
.select('albums_assets2.assetsId')
|
|
||||||
.addFrom('albums_assets_assets', 'albums_assets2')
|
|
||||||
.where('albums_assets2.assetsId = assets.id')
|
|
||||||
.andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query
|
|
||||||
.orderBy('assets.fileCreatedAt', 'DESC')
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Using dataSource, because there is no direct access to albums_assets_assets.
|
|
||||||
const albumHasAssets = dataSource
|
|
||||||
.createQueryBuilder()
|
|
||||||
.select('1')
|
|
||||||
.from('albums_assets_assets', 'albums_assets')
|
|
||||||
.where('"albums"."id" = "albums_assets"."albumsId"');
|
|
||||||
|
|
||||||
const albumContainsThumbnail = albumHasAssets
|
|
||||||
.clone()
|
|
||||||
.andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
|
|
||||||
|
|
||||||
const updateAlbums = this.albumRepository
|
|
||||||
.createQueryBuilder('albums')
|
|
||||||
.update(AlbumEntity)
|
|
||||||
.set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` })
|
|
||||||
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
|
|
||||||
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`);
|
|
||||||
|
|
||||||
const result = await updateAlbums.execute();
|
|
||||||
|
|
||||||
return result.affected;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import { AlbumResponseDto, AuthUserDto } from '@app/domain';
|
|
||||||
import { Body, Controller, Delete, Get, Param, Put } from '@nestjs/common';
|
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
|
||||||
import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard';
|
|
||||||
import { UseValidation } from '../../app.utils';
|
|
||||||
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
|
|
||||||
import { AlbumService } from './album.service';
|
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
|
||||||
|
|
||||||
@ApiTags('Album')
|
|
||||||
@Controller('album')
|
|
||||||
@Authenticated()
|
|
||||||
@UseValidation()
|
|
||||||
export class AlbumController {
|
|
||||||
constructor(private service: AlbumService) {}
|
|
||||||
|
|
||||||
@SharedLinkRoute()
|
|
||||||
@Put(':id/assets')
|
|
||||||
addAssetsToAlbum(
|
|
||||||
@AuthUser() authUser: AuthUserDto,
|
|
||||||
@Param() { id }: UUIDParamDto,
|
|
||||||
@Body() dto: AddAssetsDto,
|
|
||||||
): Promise<AddAssetsResponseDto> {
|
|
||||||
// TODO: Handle nonexistent assetIds.
|
|
||||||
// TODO: Disallow adding assets of another user to an album.
|
|
||||||
return this.service.addAssets(authUser, id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SharedLinkRoute()
|
|
||||||
@Get(':id')
|
|
||||||
getAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
|
|
||||||
return this.service.get(authUser, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id/assets')
|
|
||||||
removeAssetFromAlbum(
|
|
||||||
@AuthUser() authUser: AuthUserDto,
|
|
||||||
@Body() dto: RemoveAssetsDto,
|
|
||||||
@Param() { id }: UUIDParamDto,
|
|
||||||
): Promise<AlbumResponseDto> {
|
|
||||||
return this.service.removeAssets(authUser, id, dto);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
|
|
||||||
import { AlbumController } from './album.controller';
|
|
||||||
import { AlbumService } from './album.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity])],
|
|
||||||
controllers: [AlbumController],
|
|
||||||
providers: [AlbumService, { provide: IAlbumRepository, useClass: AlbumRepository }],
|
|
||||||
})
|
|
||||||
export class AlbumModule {}
|
|
@ -1,258 +0,0 @@
|
|||||||
import { AlbumResponseDto, AuthUserDto, mapUser } from '@app/domain';
|
|
||||||
import { AlbumEntity, UserEntity } from '@app/infra/entities';
|
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
|
||||||
import { userStub } from '@test';
|
|
||||||
import { IAlbumRepository } from './album-repository';
|
|
||||||
import { AlbumService } from './album.service';
|
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
|
||||||
|
|
||||||
describe('Album service', () => {
|
|
||||||
let sut: AlbumService;
|
|
||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
|
||||||
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
|
||||||
id: '1111',
|
|
||||||
email: 'auth@test.com',
|
|
||||||
isAdmin: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const albumOwner: UserEntity = Object.freeze({
|
|
||||||
...authUser,
|
|
||||||
firstName: 'auth',
|
|
||||||
lastName: 'user',
|
|
||||||
createdAt: new Date('2022-06-19T23:41:36.910Z'),
|
|
||||||
deletedAt: null,
|
|
||||||
updatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
|
||||||
profileImagePath: '',
|
|
||||||
shouldChangePassword: false,
|
|
||||||
oauthId: '',
|
|
||||||
tags: [],
|
|
||||||
assets: [],
|
|
||||||
storageLabel: null,
|
|
||||||
externalPath: null,
|
|
||||||
});
|
|
||||||
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
|
||||||
const sharedAlbumOwnerId = '2222';
|
|
||||||
const sharedAlbumSharedAlsoWithId = '3333';
|
|
||||||
|
|
||||||
const _getOwnedAlbum = () => {
|
|
||||||
const albumEntity = new AlbumEntity();
|
|
||||||
albumEntity.ownerId = albumOwner.id;
|
|
||||||
albumEntity.owner = albumOwner;
|
|
||||||
albumEntity.id = albumId;
|
|
||||||
albumEntity.albumName = 'name';
|
|
||||||
albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z');
|
|
||||||
albumEntity.updatedAt = new Date('2022-06-19T23:41:36.910Z');
|
|
||||||
albumEntity.sharedUsers = [];
|
|
||||||
albumEntity.assets = [];
|
|
||||||
albumEntity.albumThumbnailAssetId = null;
|
|
||||||
albumEntity.sharedLinks = [];
|
|
||||||
return albumEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getSharedWithAuthUserAlbum = () => {
|
|
||||||
const albumEntity = new AlbumEntity();
|
|
||||||
albumEntity.ownerId = sharedAlbumOwnerId;
|
|
||||||
albumEntity.owner = albumOwner;
|
|
||||||
albumEntity.id = albumId;
|
|
||||||
albumEntity.albumName = 'name';
|
|
||||||
albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z');
|
|
||||||
albumEntity.assets = [];
|
|
||||||
albumEntity.albumThumbnailAssetId = null;
|
|
||||||
albumEntity.sharedUsers = [
|
|
||||||
{
|
|
||||||
...userStub.user1,
|
|
||||||
id: authUser.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...userStub.user1,
|
|
||||||
id: sharedAlbumSharedAlsoWithId,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
albumEntity.sharedLinks = [];
|
|
||||||
|
|
||||||
return albumEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getNotOwnedNotSharedAlbum = () => {
|
|
||||||
const albumEntity = new AlbumEntity();
|
|
||||||
albumEntity.ownerId = '5555';
|
|
||||||
albumEntity.id = albumId;
|
|
||||||
albumEntity.albumName = 'name';
|
|
||||||
albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z');
|
|
||||||
albumEntity.sharedUsers = [];
|
|
||||||
albumEntity.assets = [];
|
|
||||||
albumEntity.albumThumbnailAssetId = null;
|
|
||||||
|
|
||||||
return albumEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
albumRepositoryMock = {
|
|
||||||
addAssets: jest.fn(),
|
|
||||||
get: jest.fn(),
|
|
||||||
removeAssets: jest.fn(),
|
|
||||||
updateThumbnails: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sut = new AlbumService(albumRepositoryMock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gets an owned album', async () => {
|
|
||||||
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
|
||||||
|
|
||||||
const albumEntity = _getOwnedAlbum();
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
|
|
||||||
const expectedResult: AlbumResponseDto = {
|
|
||||||
ownerId: albumOwner.id,
|
|
||||||
owner: mapUser(albumOwner),
|
|
||||||
id: albumId,
|
|
||||||
albumName: 'name',
|
|
||||||
createdAt: new Date('2022-06-19T23:41:36.910Z'),
|
|
||||||
updatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
|
||||||
sharedUsers: [],
|
|
||||||
assets: [],
|
|
||||||
albumThumbnailAssetId: null,
|
|
||||||
shared: false,
|
|
||||||
assetCount: 0,
|
|
||||||
};
|
|
||||||
await expect(sut.get(authUser, albumId)).resolves.toEqual(expectedResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gets a shared album', async () => {
|
|
||||||
const albumEntity = _getSharedWithAuthUserAlbum();
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
|
|
||||||
const result = await sut.get(authUser, albumId);
|
|
||||||
expect(result.id).toEqual(albumId);
|
|
||||||
expect(result.ownerId).toEqual(sharedAlbumOwnerId);
|
|
||||||
expect(result.shared).toEqual(true);
|
|
||||||
expect(result.sharedUsers).toHaveLength(2);
|
|
||||||
expect(result.sharedUsers[0].id).toEqual(authUser.id);
|
|
||||||
expect(result.sharedUsers[1].id).toEqual(sharedAlbumSharedAlsoWithId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prevents retrieving an album that is not owned or shared', async () => {
|
|
||||||
const albumEntity = _getNotOwnedNotSharedAlbum();
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
await expect(sut.get(authUser, albumId)).rejects.toBeInstanceOf(ForbiddenException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws a not found exception if the album is not found', async () => {
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve(null));
|
|
||||||
await expect(sut.get(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds assets to owned album', async () => {
|
|
||||||
const albumEntity = _getOwnedAlbum();
|
|
||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
|
||||||
alreadyInAlbum: [],
|
|
||||||
successfullyAdded: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
|
||||||
|
|
||||||
const result = (await sut.addAssets(authUser, albumId, { assetIds: ['1'] })) as AddAssetsResponseDto;
|
|
||||||
|
|
||||||
// TODO: stub and expect album rendered
|
|
||||||
expect(result.album?.id).toEqual(albumId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds assets to shared album (shared with auth user)', async () => {
|
|
||||||
const albumEntity = _getSharedWithAuthUserAlbum();
|
|
||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
|
||||||
alreadyInAlbum: [],
|
|
||||||
successfullyAdded: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
|
||||||
|
|
||||||
const result = (await sut.addAssets(authUser, albumId, { assetIds: ['1'] })) as AddAssetsResponseDto;
|
|
||||||
|
|
||||||
// TODO: stub and expect album rendered
|
|
||||||
expect(result.album?.id).toEqual(albumId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prevents adding assets to a not owned / shared album', async () => {
|
|
||||||
const albumEntity = _getNotOwnedNotSharedAlbum();
|
|
||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
|
||||||
alreadyInAlbum: [],
|
|
||||||
successfullyAdded: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
|
||||||
|
|
||||||
await expect(sut.addAssets(authUser, albumId, { assetIds: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
|
|
||||||
});
|
|
||||||
|
|
||||||
// it('removes assets from owned album', async () => {
|
|
||||||
// const albumEntity = _getOwnedAlbum();
|
|
||||||
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
// albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
|
|
||||||
// await expect(
|
|
||||||
// sut.removeAssetsFromAlbum(
|
|
||||||
// authUser,
|
|
||||||
// {
|
|
||||||
// assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
|
|
||||||
// },
|
|
||||||
// albumEntity.id,
|
|
||||||
// ),
|
|
||||||
// ).resolves.toBeUndefined();
|
|
||||||
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
|
||||||
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
|
||||||
// assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('removes assets from shared album (shared with auth user)', async () => {
|
|
||||||
// const albumEntity = _getOwnedSharedAlbum();
|
|
||||||
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
// albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
|
|
||||||
// await expect(
|
|
||||||
// sut.removeAssetsFromAlbum(
|
|
||||||
// authUser,
|
|
||||||
// {
|
|
||||||
// assetIds: ['1'],
|
|
||||||
// },
|
|
||||||
// albumEntity.id,
|
|
||||||
// ),
|
|
||||||
// ).resolves.toBeUndefined();
|
|
||||||
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
|
||||||
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
|
||||||
// assetIds: ['1'],
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
it('prevents removing assets from a not owned / shared album', async () => {
|
|
||||||
const albumEntity = _getNotOwnedNotSharedAlbum();
|
|
||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
|
||||||
alreadyInAlbum: [],
|
|
||||||
successfullyAdded: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
|
||||||
|
|
||||||
await expect(sut.removeAssets(authUser, albumId, { assetIds: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,72 +0,0 @@
|
|||||||
import { AlbumResponseDto, AuthUserDto, mapAlbum } from '@app/domain';
|
|
||||||
import { AlbumEntity } from '@app/infra/entities';
|
|
||||||
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { IAlbumRepository } from './album-repository';
|
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AlbumService {
|
|
||||||
private logger = new Logger(AlbumService.name);
|
|
||||||
|
|
||||||
constructor(@Inject(IAlbumRepository) private repository: IAlbumRepository) {}
|
|
||||||
|
|
||||||
private async _getAlbum({
|
|
||||||
authUser,
|
|
||||||
albumId,
|
|
||||||
validateIsOwner = true,
|
|
||||||
}: {
|
|
||||||
authUser: AuthUserDto;
|
|
||||||
albumId: string;
|
|
||||||
validateIsOwner?: boolean;
|
|
||||||
}): Promise<AlbumEntity> {
|
|
||||||
await this.repository.updateThumbnails();
|
|
||||||
|
|
||||||
const album = await this.repository.get(albumId);
|
|
||||||
if (!album) {
|
|
||||||
throw new NotFoundException('Album Not Found');
|
|
||||||
}
|
|
||||||
const isOwner = album.ownerId == authUser.id;
|
|
||||||
|
|
||||||
if (validateIsOwner && !isOwner) {
|
|
||||||
throw new ForbiddenException('Unauthorized Album Access');
|
|
||||||
} else if (!isOwner && !album.sharedUsers?.some((user) => user.id == authUser.id)) {
|
|
||||||
throw new ForbiddenException('Unauthorized Album Access');
|
|
||||||
}
|
|
||||||
return album;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
|
|
||||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
|
||||||
return mapAlbum(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeAssets(authUser: AuthUserDto, albumId: string, dto: RemoveAssetsDto): Promise<AlbumResponseDto> {
|
|
||||||
const album = await this._getAlbum({ authUser, albumId });
|
|
||||||
const deletedCount = await this.repository.removeAssets(album, dto);
|
|
||||||
const newAlbum = await this._getAlbum({ authUser, albumId });
|
|
||||||
|
|
||||||
if (deletedCount !== dto.assetIds.length) {
|
|
||||||
throw new BadRequestException('Some assets were not found in the album');
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapAlbum(newAlbum);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addAssets(authUser: AuthUserDto, albumId: string, dto: AddAssetsDto): Promise<AddAssetsResponseDto> {
|
|
||||||
if (authUser.isPublicUser && !authUser.isAllowUpload) {
|
|
||||||
this.logger.warn('Deny public user attempt to add asset to album');
|
|
||||||
throw new ForbiddenException('Public user is not allowed to upload');
|
|
||||||
}
|
|
||||||
|
|
||||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
|
||||||
const result = await this.repository.addAssets(album, dto);
|
|
||||||
const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
album: mapAlbum(newAlbum),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { ValidateUUID } from '@app/domain';
|
|
||||||
|
|
||||||
export class AddAssetsDto {
|
|
||||||
@ValidateUUID({ each: true })
|
|
||||||
assetIds!: string[];
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { ValidateUUID } from '@app/domain';
|
|
||||||
|
|
||||||
export class AddUsersDto {
|
|
||||||
@ValidateUUID({ each: true })
|
|
||||||
sharedUserIds!: string[];
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { ValidateUUID } from '@app/domain';
|
|
||||||
|
|
||||||
export class RemoveAssetsDto {
|
|
||||||
@ValidateUUID({ each: true })
|
|
||||||
assetIds!: string[];
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { AlbumResponseDto } from '@app/domain';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export class AddAssetsResponseDto {
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
successfullyAdded!: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
alreadyInAlbum!: string[];
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
album?: AlbumResponseDto;
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ import { Module } 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';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AlbumModule } from './api-v1/album/album.module';
|
|
||||||
import { AssetRepository, IAssetRepository } from './api-v1/asset/asset-repository';
|
import { AssetRepository, IAssetRepository } from './api-v1/asset/asset-repository';
|
||||||
import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller';
|
import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller';
|
||||||
import { AssetService } from './api-v1/asset/asset.service';
|
import { AssetService } from './api-v1/asset/asset.service';
|
||||||
@ -34,7 +33,6 @@ import {
|
|||||||
imports: [
|
imports: [
|
||||||
//
|
//
|
||||||
DomainModule.register({ imports: [InfraModule] }),
|
DomainModule.register({ imports: [InfraModule] }),
|
||||||
AlbumModule,
|
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
|
TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
|
||||||
],
|
],
|
||||||
|
@ -3,6 +3,8 @@ import {
|
|||||||
AlbumCountResponseDto,
|
AlbumCountResponseDto,
|
||||||
AlbumService,
|
AlbumService,
|
||||||
AuthUserDto,
|
AuthUserDto,
|
||||||
|
BulkIdResponseDto,
|
||||||
|
BulkIdsDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
UpdateAlbumDto,
|
UpdateAlbumDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
@ -10,7 +12,7 @@ import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
|
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
|
||||||
import { Authenticated, AuthUser } from '../app.guard';
|
import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ -36,6 +38,12 @@ export class AlbumController {
|
|||||||
return this.service.create(authUser, dto);
|
return this.service.create(authUser, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SharedLinkRoute()
|
||||||
|
@Get(':id')
|
||||||
|
getAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
|
||||||
|
return this.service.get(authUser, id);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) {
|
updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) {
|
||||||
return this.service.update(authUser, id, dto);
|
return this.service.update(authUser, id, dto);
|
||||||
@ -46,6 +54,25 @@ export class AlbumController {
|
|||||||
return this.service.delete(authUser, id);
|
return this.service.delete(authUser, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SharedLinkRoute()
|
||||||
|
@Put(':id/assets')
|
||||||
|
addAssetsToAlbum(
|
||||||
|
@AuthUser() authUser: AuthUserDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Body() dto: BulkIdsDto,
|
||||||
|
): Promise<BulkIdResponseDto[]> {
|
||||||
|
return this.service.addAssets(authUser, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/assets')
|
||||||
|
removeAssetFromAlbum(
|
||||||
|
@AuthUser() authUser: AuthUserDto,
|
||||||
|
@Body() dto: BulkIdsDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
): Promise<BulkIdResponseDto[]> {
|
||||||
|
return this.service.removeAssets(authUser, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Put(':id/users')
|
@Put(':id/users')
|
||||||
addUsersToAlbum(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) {
|
addUsersToAlbum(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) {
|
||||||
return this.service.addUsers(authUser, id, dto);
|
return this.service.addUsers(authUser, id, dto);
|
||||||
|
@ -3,11 +3,35 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||||
import { dataSource } from '../database.config';
|
import { dataSource } from '../database.config';
|
||||||
import { AlbumEntity } from '../entities';
|
import { AlbumEntity, AssetEntity } from '../entities';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumRepository implements IAlbumRepository {
|
export class AlbumRepository implements IAlbumRepository {
|
||||||
constructor(@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>) {}
|
constructor(
|
||||||
|
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||||
|
@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getById(id: string): Promise<AlbumEntity | null> {
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
owner: true,
|
||||||
|
sharedUsers: true,
|
||||||
|
assets: {
|
||||||
|
exifInfo: true,
|
||||||
|
},
|
||||||
|
sharedLinks: true,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
assets: {
|
||||||
|
fileCreatedAt: 'DESC',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getByIds(ids: string[]): Promise<AlbumEntity[]> {
|
getByIds(ids: string[]): Promise<AlbumEntity[]> {
|
||||||
return this.repository.find({
|
return this.repository.find({
|
||||||
@ -161,4 +185,46 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure all thumbnails for albums are updated by:
|
||||||
|
* - Removing thumbnails from albums without assets
|
||||||
|
* - Removing references of thumbnails to assets outside the album
|
||||||
|
* - Setting a thumbnail when none is set and the album contains assets
|
||||||
|
*
|
||||||
|
* @returns Amount of updated album thumbnails or undefined when unknown
|
||||||
|
*/
|
||||||
|
async updateThumbnails(): Promise<number | undefined> {
|
||||||
|
// Subquery for getting a new thumbnail.
|
||||||
|
const newThumbnail = this.assetRepository
|
||||||
|
.createQueryBuilder('assets')
|
||||||
|
.select('albums_assets2.assetsId')
|
||||||
|
.addFrom('albums_assets_assets', 'albums_assets2')
|
||||||
|
.where('albums_assets2.assetsId = assets.id')
|
||||||
|
.andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query
|
||||||
|
.orderBy('assets.fileCreatedAt', 'DESC')
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Using dataSource, because there is no direct access to albums_assets_assets.
|
||||||
|
const albumHasAssets = dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('1')
|
||||||
|
.from('albums_assets_assets', 'albums_assets')
|
||||||
|
.where('"albums"."id" = "albums_assets"."albumsId"');
|
||||||
|
|
||||||
|
const albumContainsThumbnail = albumHasAssets
|
||||||
|
.clone()
|
||||||
|
.andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
|
||||||
|
|
||||||
|
const updateAlbums = this.repository
|
||||||
|
.createQueryBuilder('albums')
|
||||||
|
.update(AlbumEntity)
|
||||||
|
.set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` })
|
||||||
|
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
|
||||||
|
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`);
|
||||||
|
|
||||||
|
const result = await updateAlbums.execute();
|
||||||
|
|
||||||
|
return result.affected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
server/test/fixtures/album.stub.ts
vendored
13
server/test/fixtures/album.stub.ts
vendored
@ -69,6 +69,19 @@ export const albumStub = {
|
|||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
}),
|
}),
|
||||||
|
twoAssets: Object.freeze<AlbumEntity>({
|
||||||
|
id: 'album-4a',
|
||||||
|
albumName: 'Album with two assets',
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
owner: userStub.admin,
|
||||||
|
assets: [assetStub.image, assetStub.withLocation],
|
||||||
|
albumThumbnailAsset: assetStub.image,
|
||||||
|
albumThumbnailAssetId: assetStub.image.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
sharedLinks: [],
|
||||||
|
sharedUsers: [],
|
||||||
|
}),
|
||||||
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-5',
|
id: 'album-5',
|
||||||
albumName: 'Empty album with invalid thumbnail',
|
albumName: 'Empty album with invalid thumbnail',
|
||||||
|
@ -2,6 +2,7 @@ import { IAlbumRepository } from '@app/domain';
|
|||||||
|
|
||||||
export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
||||||
return {
|
return {
|
||||||
|
getById: jest.fn(),
|
||||||
getByIds: jest.fn(),
|
getByIds: jest.fn(),
|
||||||
getByAssetId: jest.fn(),
|
getByAssetId: jest.fn(),
|
||||||
getAssetCountForIds: jest.fn(),
|
getAssetCountForIds: jest.fn(),
|
||||||
@ -15,5 +16,6 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
|||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
updateThumbnails: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
120
web/src/api/open-api/api.ts
generated
120
web/src/api/open-api/api.ts
generated
@ -99,44 +99,6 @@ export interface APIKeyUpdateDto {
|
|||||||
*/
|
*/
|
||||||
'name': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface AddAssetsDto
|
|
||||||
*/
|
|
||||||
export interface AddAssetsDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof AddAssetsDto
|
|
||||||
*/
|
|
||||||
'assetIds': Array<string>;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
export interface AddAssetsResponseDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {AlbumResponseDto}
|
|
||||||
* @memberof AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
'album'?: AlbumResponseDto;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
'alreadyInAlbum': Array<string>;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
* @memberof AddAssetsResponseDto
|
|
||||||
*/
|
|
||||||
'successfullyAdded': number;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -821,6 +783,19 @@ export const BulkIdResponseDtoErrorEnum = {
|
|||||||
|
|
||||||
export type BulkIdResponseDtoErrorEnum = typeof BulkIdResponseDtoErrorEnum[keyof typeof BulkIdResponseDtoErrorEnum];
|
export type BulkIdResponseDtoErrorEnum = typeof BulkIdResponseDtoErrorEnum[keyof typeof BulkIdResponseDtoErrorEnum];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface BulkIdsDto
|
||||||
|
*/
|
||||||
|
export interface BulkIdsDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @memberof BulkIdsDto
|
||||||
|
*/
|
||||||
|
'ids': Array<string>;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -1927,19 +1902,6 @@ export interface QueueStatusDto {
|
|||||||
*/
|
*/
|
||||||
'isPaused': boolean;
|
'isPaused': boolean;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface RemoveAssetsDto
|
|
||||||
*/
|
|
||||||
export interface RemoveAssetsDto {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {Array<string>}
|
|
||||||
* @memberof RemoveAssetsDto
|
|
||||||
*/
|
|
||||||
'assetIds': Array<string>;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -3679,16 +3641,16 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {AddAssetsDto} addAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
addAssetsToAlbum: async (id: string, addAssetsDto: AddAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
addAssetsToAlbum: async (id: string, bulkIdsDto: BulkIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'id' is not null or undefined
|
// verify required parameter 'id' is not null or undefined
|
||||||
assertParamExists('addAssetsToAlbum', 'id', id)
|
assertParamExists('addAssetsToAlbum', 'id', id)
|
||||||
// verify required parameter 'addAssetsDto' is not null or undefined
|
// verify required parameter 'bulkIdsDto' is not null or undefined
|
||||||
assertParamExists('addAssetsToAlbum', 'addAssetsDto', addAssetsDto)
|
assertParamExists('addAssetsToAlbum', 'bulkIdsDto', bulkIdsDto)
|
||||||
const localVarPath = `/album/{id}/assets`
|
const localVarPath = `/album/{id}/assets`
|
||||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
@ -3722,7 +3684,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration)
|
localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
@ -3999,15 +3961,15 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {RemoveAssetsDto} removeAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
removeAssetFromAlbum: async (id: string, removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
removeAssetFromAlbum: async (id: string, bulkIdsDto: BulkIdsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'id' is not null or undefined
|
// verify required parameter 'id' is not null or undefined
|
||||||
assertParamExists('removeAssetFromAlbum', 'id', id)
|
assertParamExists('removeAssetFromAlbum', 'id', id)
|
||||||
// verify required parameter 'removeAssetsDto' is not null or undefined
|
// verify required parameter 'bulkIdsDto' is not null or undefined
|
||||||
assertParamExists('removeAssetFromAlbum', 'removeAssetsDto', removeAssetsDto)
|
assertParamExists('removeAssetFromAlbum', 'bulkIdsDto', bulkIdsDto)
|
||||||
const localVarPath = `/album/{id}/assets`
|
const localVarPath = `/album/{id}/assets`
|
||||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
@ -4037,7 +3999,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration)
|
localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
@ -4151,13 +4113,13 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {AddAssetsDto} addAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async addAssetsToAlbum(id: string, addAssetsDto: AddAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AddAssetsResponseDto>> {
|
async addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, addAssetsDto, key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, bulkIdsDto, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -4225,12 +4187,12 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {RemoveAssetsDto} removeAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async removeAssetFromAlbum(id: string, removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumResponseDto>> {
|
async removeAssetFromAlbum(id: string, bulkIdsDto: BulkIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, removeAssetsDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, bulkIdsDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -4268,13 +4230,13 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {AddAssetsDto} addAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
addAssetsToAlbum(id: string, addAssetsDto: AddAssetsDto, key?: string, options?: any): AxiosPromise<AddAssetsResponseDto> {
|
addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto, key?: string, options?: any): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||||
return localVarFp.addAssetsToAlbum(id, addAssetsDto, key, options).then((request) => request(axios, basePath));
|
return localVarFp.addAssetsToAlbum(id, bulkIdsDto, key, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -4335,12 +4297,12 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {RemoveAssetsDto} removeAssetsDto
|
* @param {BulkIdsDto} bulkIdsDto
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
removeAssetFromAlbum(id: string, removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<AlbumResponseDto> {
|
removeAssetFromAlbum(id: string, bulkIdsDto: BulkIdsDto, options?: any): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||||
return localVarFp.removeAssetFromAlbum(id, removeAssetsDto, options).then((request) => request(axios, basePath));
|
return localVarFp.removeAssetFromAlbum(id, bulkIdsDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -4380,10 +4342,10 @@ export interface AlbumApiAddAssetsToAlbumRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {AddAssetsDto}
|
* @type {BulkIdsDto}
|
||||||
* @memberof AlbumApiAddAssetsToAlbum
|
* @memberof AlbumApiAddAssetsToAlbum
|
||||||
*/
|
*/
|
||||||
readonly addAssetsDto: AddAssetsDto
|
readonly bulkIdsDto: BulkIdsDto
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -4499,10 +4461,10 @@ export interface AlbumApiRemoveAssetFromAlbumRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {RemoveAssetsDto}
|
* @type {BulkIdsDto}
|
||||||
* @memberof AlbumApiRemoveAssetFromAlbum
|
* @memberof AlbumApiRemoveAssetFromAlbum
|
||||||
*/
|
*/
|
||||||
readonly removeAssetsDto: RemoveAssetsDto
|
readonly bulkIdsDto: BulkIdsDto
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4562,7 +4524,7 @@ export class AlbumApi extends BaseAPI {
|
|||||||
* @memberof AlbumApi
|
* @memberof AlbumApi
|
||||||
*/
|
*/
|
||||||
public addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig) {
|
public addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig) {
|
||||||
return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.bulkIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4638,7 +4600,7 @@ export class AlbumApi extends BaseAPI {
|
|||||||
* @memberof AlbumApi
|
* @memberof AlbumApi
|
||||||
*/
|
*/
|
||||||
public removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig) {
|
public removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig) {
|
||||||
return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.removeAssetsDto, options).then((request) => request(this.axios, this.basePath));
|
return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
|
|
||||||
let multiSelectAsset: Set<AssetResponseDto> = new Set();
|
let multiSelectAsset: Set<AssetResponseDto> = new Set();
|
||||||
$: isMultiSelectionMode = multiSelectAsset.size > 0;
|
$: isMultiSelectionMode = multiSelectAsset.size > 0;
|
||||||
|
$: isMultiSelectionUserOwned = Array.from(multiSelectAsset).every((asset) => asset.ownerId === currentUser?.id);
|
||||||
|
|
||||||
afterNavigate(({ from }) => {
|
afterNavigate(({ from }) => {
|
||||||
backUrl = from?.url.pathname ?? '/albums';
|
backUrl = from?.url.pathname ?? '/albums';
|
||||||
@ -182,24 +183,24 @@
|
|||||||
const createAlbumHandler = async (event: CustomEvent) => {
|
const createAlbumHandler = async (event: CustomEvent) => {
|
||||||
const { assets }: { assets: AssetResponseDto[] } = event.detail;
|
const { assets }: { assets: AssetResponseDto[] } = event.detail;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.albumApi.addAssetsToAlbum({
|
const { data: results } = await api.albumApi.addAssetsToAlbum({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
addAssetsDto: {
|
bulkIdsDto: { ids: assets.map((a) => a.id) },
|
||||||
assetIds: assets.map((a) => a.id),
|
|
||||||
},
|
|
||||||
key: sharedLink?.key,
|
key: sharedLink?.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.album) {
|
const count = results.filter(({ success }) => success).length;
|
||||||
album = data.album;
|
notificationController.show({
|
||||||
}
|
type: NotificationType.Info,
|
||||||
|
message: `Added ${count} asset${count === 1 ? '' : 's'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await api.albumApi.getAlbumInfo({ id: album.id });
|
||||||
|
album = data;
|
||||||
|
|
||||||
isShowAssetSelection = false;
|
isShowAssetSelection = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error [createAlbumHandler] ', e);
|
handleError(e, 'Error creating album');
|
||||||
notificationController.show({
|
|
||||||
type: NotificationType.Error,
|
|
||||||
message: 'Error creating album, check console for more details',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -307,7 +308,7 @@
|
|||||||
{#if sharedLink?.allowDownload || !isPublicShared}
|
{#if sharedLink?.allowDownload || !isPublicShared}
|
||||||
<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} />
|
<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isOwned}
|
{#if isOwned || isMultiSelectionUserOwned}
|
||||||
<RemoveFromAlbum bind:album />
|
<RemoveFromAlbum bind:album />
|
||||||
{/if}
|
{/if}
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
|
@ -189,11 +189,8 @@
|
|||||||
isShowAlbumPicker = false;
|
isShowAlbumPicker = false;
|
||||||
const album = event.detail.album;
|
const album = event.detail.album;
|
||||||
|
|
||||||
addAssetsToAlbum(album.id, [asset.id]).then((dto) => {
|
await addAssetsToAlbum(album.id, [asset.id]);
|
||||||
if (dto.successfullyAdded === 1 && dto.album) {
|
await getAllAlbums();
|
||||||
appearsInAlbums = [...appearsInAlbums, dto.album];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableKeyDownEvent = () => {
|
const disableKeyDownEvent = () => {
|
||||||
|
@ -44,10 +44,9 @@
|
|||||||
const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
|
const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
|
||||||
showAlbumPicker = false;
|
showAlbumPicker = false;
|
||||||
const album = event.detail.album;
|
const album = event.detail.album;
|
||||||
|
|
||||||
const assetIds = Array.from(getAssets()).map((asset) => asset.id);
|
const assetIds = Array.from(getAssets()).map((asset) => asset.id);
|
||||||
|
await addAssetsToAlbum(album.id, assetIds);
|
||||||
addAssetsToAlbum(album.id, assetIds).then(clearSelect);
|
clearSelect();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,14 +17,20 @@
|
|||||||
|
|
||||||
const removeFromAlbum = async () => {
|
const removeFromAlbum = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.albumApi.removeAssetFromAlbum({
|
const { data: results } = await api.albumApi.removeAssetFromAlbum({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
removeAssetsDto: {
|
bulkIdsDto: { ids: Array.from(getAssets()).map((a) => a.id) },
|
||||||
assetIds: Array.from(getAssets()).map((a) => a.id),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data } = await api.albumApi.getAlbumInfo({ id: album.id });
|
||||||
album = data;
|
album = data;
|
||||||
|
|
||||||
|
const count = results.filter(({ success }) => success).length;
|
||||||
|
notificationController.show({
|
||||||
|
type: NotificationType.Info,
|
||||||
|
message: `Removed ${count} asset${count === 1 ? '' : 's'}`,
|
||||||
|
});
|
||||||
|
|
||||||
clearSelect();
|
clearSelect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||||
import { downloadManager } from '$lib/stores/download';
|
import { downloadManager } from '$lib/stores/download';
|
||||||
import { AddAssetsResponseDto, api, AssetApiGetDownloadInfoRequest, AssetResponseDto, DownloadResponseDto } from '@api';
|
import { api, AssetApiGetDownloadInfoRequest, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto } from '@api';
|
||||||
import { handleError } from './handle-error';
|
import { handleError } from './handle-error';
|
||||||
|
|
||||||
export const addAssetsToAlbum = async (
|
export const addAssetsToAlbum = async (
|
||||||
albumId: string,
|
albumId: string,
|
||||||
assetIds: Array<string>,
|
assetIds: Array<string>,
|
||||||
key: string | undefined = undefined,
|
key: string | undefined = undefined,
|
||||||
): Promise<AddAssetsResponseDto> =>
|
): Promise<BulkIdResponseDto[]> =>
|
||||||
api.albumApi.addAssetsToAlbum({ id: albumId, addAssetsDto: { assetIds }, key }).then(({ data: dto }) => {
|
api.albumApi.addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetIds }, key }).then(({ data: results }) => {
|
||||||
if (dto.successfullyAdded > 0) {
|
const count = results.filter(({ success }) => success).length;
|
||||||
|
if (count > 0) {
|
||||||
// This might be 0 if the user tries to add an asset that is already in the album
|
// This might be 0 if the user tries to add an asset that is already in the album
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
|
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
|
message: `Added ${count} asset${count === 1 ? '' : 's'}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto;
|
return results;
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadBlob = (data: Blob, filename: string) => {
|
const downloadBlob = (data: Blob, filename: string) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user