mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
restore: bulk actions (#3730)
* feat: improve bulk isArchive and isFavorite updates * chore: open api
This commit is contained in:
parent
8568ec838a
commit
bab739efbd
30 changed files with 734 additions and 57 deletions
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue