fix(web/server) uploaded asset in shared link not loaded (#1766)

* fix(web/server): Uploaded asset to shared link does not get added to the shared link/album

* remove unused code

* Add endpoints for each remove and add assets to shared link

* Update api

* Added deletion logic

* Convert callback to async/await

* Fix linter

* Fix test

* Fix server test

* added test

* Test coverage

* modify DTO

* Add notification

* fix test
This commit is contained in:
Alex
2023-02-15 15:21:22 -06:00
committed by GitHub
parent 125ec1e85f
commit b660240059
25 changed files with 583 additions and 526 deletions

View File

@@ -1,3 +1,4 @@
import { AddAssetsDto } from './../album/dto/add-assets.dto';
import {
Controller,
Post,
@@ -52,10 +53,10 @@ import {
import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { SharedLinkResponseDto } from '@app/domain';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length });
@@ -330,11 +331,20 @@ export class AssetController {
}
@Authenticated({ isShared: true })
@Patch('/shared-link')
async updateAssetsInSharedLink(
@Patch('/shared-link/add')
async addAssetsToSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
@Body(ValidationPipe) dto: AddAssetsDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.updateAssetsInSharedLink(authUser, dto);
return await this.assetService.addAssetsToSharedLink(authUser, dto);
}
@Authenticated({ isShared: true })
@Patch('/shared-link/remove')
async removeAssetsFromSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: RemoveAssetsDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.removeAssetsFromSharedLink(authUser, dto);
}
}

View File

@@ -198,14 +198,31 @@ describe('AssetService', () => {
sharedLinkRepositoryMock.get.mockResolvedValue(null);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
});
it('should add assets to a shared link', async () => {
const asset1 = _getAsset_1();
const authDto = authStub.adminSharedLink;
const dto = { assetIds: [asset1.id] };
assetRepositoryMock.getById.mockResolvedValue(asset1);
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
});
it('should remove assets from a shared link', async () => {
const asset1 = _getAsset_1();
@@ -217,11 +234,11 @@ describe('AssetService', () => {
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
});
});

View File

@@ -58,8 +58,9 @@ import { ISharedLinkRepository } from '@app/domain';
import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
import { AddAssetsDto } from '../album/dto/add-assets.dto';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
const fileInfo = promisify(stat);
@@ -606,23 +607,35 @@ export class AssetService {
return mapSharedLink(sharedLink);
}
async updateAssetsInSharedLink(
authUser: AuthUserDto,
dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) {
throw new ForbiddenException();
}
const assets = [];
await this.checkAssetsAccess(authUser, dto.assetIds);
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets);
const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets);
return mapSharedLink(updatedLink);
}
async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) {
throw new ForbiddenException();
}
const assets = [];
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets);
return mapSharedLink(updatedLink);
}

View File

@@ -1,6 +0,0 @@
import { IsNotEmpty } from 'class-validator';
export class UpdateAssetsToSharedLinkDto {
@IsNotEmpty()
assetIds!: string[];
}

View File

@@ -1869,9 +1869,11 @@
"bearer": []
}
]
},
}
},
"/asset/shared-link/add": {
"patch": {
"operationId": "updateAssetsInSharedLink",
"operationId": "addAssetsToSharedLink",
"description": "",
"parameters": [],
"requestBody": {
@@ -1879,7 +1881,44 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto"
"$ref": "#/components/schemas/AddAssetsDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedLinkResponseDto"
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
}
]
}
},
"/asset/shared-link/remove": {
"patch": {
"operationId": "removeAssetsFromSharedLink",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RemoveAssetsDto"
}
}
}
@@ -4171,7 +4210,21 @@
"assetIds"
]
},
"UpdateAssetsToSharedLinkDto": {
"AddAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"RemoveAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
@@ -4267,20 +4320,6 @@
"sharedUserIds"
]
},
"AddAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"AddAssetsResponseDto": {
"type": "object",
"properties": {
@@ -4302,20 +4341,6 @@
"alreadyInAlbum"
]
},
"RemoveAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"UpdateAlbumDto": {
"type": "object",
"properties": {

View File

@@ -63,13 +63,24 @@ export class ShareCore {
return this.repository.remove(link);
}
async updateAssets(userId: string, id: string, assets: AssetEntity[]) {
async addAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
return this.repository.save({ ...link, assets });
return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
}
async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
return this.repository.save({ ...link, assets: newAssets });
}
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {

View File

@@ -140,9 +140,9 @@
},
"./libs/domain/": {
"branches": 80,
"functions": 89,
"functions": 88,
"lines": 95,
"statements": 95
"statements": 94
}
},
"testEnvironment": "node",