mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
restore: bulk actions (#3730)
* feat: improve bulk isArchive and isFavorite updates * chore: open api
This commit is contained in:
@@ -808,6 +808,39 @@
|
||||
"tags": [
|
||||
"Asset"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateAssets",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetBulkUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Asset"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/assetById/{id}": {
|
||||
@@ -4841,6 +4874,27 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetBulkUpdateDto": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"isArchived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ids"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetBulkUploadCheckDto": {
|
||||
"properties": {
|
||||
"assets": {
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface IAssetRepository {
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>;
|
||||
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
|
||||
@@ -514,4 +514,22 @@ describe(AssetService.name, () => {
|
||||
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAll', () => {
|
||||
it('should require asset write access for all ids', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||
await expect(
|
||||
sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
isArchived: false,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should update all assets', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { HumanReadableSize, usePagination } from '../domain.util';
|
||||
import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||
import { IAssetRepository } from './asset.repository';
|
||||
import {
|
||||
AssetBulkUpdateDto,
|
||||
AssetIdsDto,
|
||||
DownloadArchiveInfo,
|
||||
DownloadInfoDto,
|
||||
@@ -268,4 +269,10 @@ export class AssetService {
|
||||
const stats = await this.assetRepository.getStatistics(authUser.id, dto);
|
||||
return mapStats(stats);
|
||||
}
|
||||
|
||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) {
|
||||
const { ids, ...options } = dto;
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
}
|
||||
}
|
||||
|
||||
12
server/src/domain/asset/dto/asset.dto.ts
Normal file
12
server/src/domain/asset/dto/asset.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
import { BulkIdsDto } from '../response-dto';
|
||||
|
||||
export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isFavorite?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isArchived?: boolean;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './asset-ids.dto';
|
||||
export * from './asset-statistics.dto';
|
||||
export * from './asset.dto';
|
||||
export * from './download.dto';
|
||||
export * from './map-marker.dto';
|
||||
export * from './memory-lane.dto';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AssetBulkUpdateDto,
|
||||
AssetIdsDto,
|
||||
AssetResponseDto,
|
||||
AssetService,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from '@app/domain';
|
||||
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
|
||||
import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, StreamableFile } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard';
|
||||
import { asStreamableFile, UseValidation } from '../app.utils';
|
||||
@@ -76,4 +77,10 @@ export class AssetController {
|
||||
getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getByTimeBucket(authUser, dto);
|
||||
}
|
||||
|
||||
@Put()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
|
||||
return this.service.updateAll(authUser, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,10 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void> {
|
||||
await this.repository.update({ id: In(ids) }, options);
|
||||
}
|
||||
|
||||
async save(asset: Partial<AssetEntity>): Promise<AssetEntity> {
|
||||
const { id } = await this.repository.save(asset);
|
||||
return this.repository.findOneOrFail({
|
||||
|
||||
@@ -11,6 +11,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
||||
getFirstAssetForAlbumId: jest.fn(),
|
||||
getLastUpdatedAssetForAlbumId: jest.fn(),
|
||||
getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
|
||||
updateAll: jest.fn(),
|
||||
deleteAll: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findLivePhotoMatch: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user