diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 0cd0207155..eced261dbe 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -22,10 +22,24 @@ type EmitEventMap = { 'asset.untag': [{ assetId: string }]; 'asset.hide': [{ assetId: string; userId: string }]; 'asset.show': [{ assetId: string; userId: string }]; + 'asset.trash': [{ assetId: string; userId: string }]; + 'asset.delete': [{ assetId: string; userId: string }]; + + // asset bulk events + 'assets.trash': [{ assetIds: string[]; userId: string }]; + 'assets.restore': [{ assetIds: string[]; userId: string }]; // session events 'session.delete': [{ sessionId: string }]; + // stack events + 'stack.create': [{ stackId: string; userId: string }]; + 'stack.update': [{ stackId: string; userId: string }]; + 'stack.delete': [{ stackId: string; userId: string }]; + + // stack bulk events + 'stacks.delete': [{ stackIds: string[]; userId: string }]; + // user events 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 111d222c16..df3b183442 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -30,7 +30,7 @@ import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entit import { AssetType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -194,8 +194,7 @@ export class AssetMediaService { const copiedPhoto = await this.createCopy(asset); // and immediate trash it await this.assetRepository.softDeleteAll([copiedPhoto.id]); - - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, [copiedPhoto.id]); + await this.eventRepository.emit('asset.trash', { assetId: copiedPhoto.id, userId: auth.user.id }); await this.userRepository.updateUsage(auth.user.id, file.size); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 06ca3af7d5..98d3dd1459 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -23,7 +23,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IAssetDeleteJob, IJobRepository, @@ -273,7 +273,8 @@ export class AssetService { if (!asset.libraryId) { await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0)); } - this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id); + + await this.eventRepository.emit('asset.delete', { assetId: id, userId: asset.ownerId }); // delete the motion if it is not used by another asset if (asset.livePhotoVideoId) { @@ -311,7 +312,7 @@ export class AssetService { ); } else { await this.assetRepository.softDeleteAll(ids); - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, ids); + await this.eventRepository.emit('assets.trash', { assetIds: ids, userId: auth.user.id }); } } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 9d9f8f5fcf..9ef1310bfb 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -144,6 +144,23 @@ describe(NotificationService.name, () => { }); }); + describe('onAssetHide', () => { + it('should send connected clients an event', () => { + sut.onAssetHide({ assetId: 'asset-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_hidden', 'user-id', 'asset-id'); + }); + }); + + describe('onAssetShow', () => { + it('should queue the generate thumbnail job', async () => { + await sut.onAssetShow({ assetId: 'asset-id', userId: 'user-id' }); + expect(jobMock.queue).toHaveBeenCalledWith({ + name: JobName.GENERATE_THUMBNAIL, + data: { id: 'asset-id', notify: true }, + }); + }); + }); + describe('onUserSignupEvent', () => { it('skips when notify is false', async () => { await sut.onUserSignup({ id: '', notify: false }); @@ -179,6 +196,62 @@ describe(NotificationService.name, () => { }); }); + describe('onAssetTrash', () => { + it('should send connected clients an event', () => { + sut.onAssetTrash({ assetId: 'asset-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']); + }); + }); + + describe('onAssetDelete', () => { + it('should send connected clients an event', () => { + sut.onAssetDelete({ assetId: 'asset-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_delete', 'user-id', 'asset-id'); + }); + }); + + describe('onAssetsTrash', () => { + it('should send connected clients an event', () => { + sut.onAssetsTrash({ assetIds: ['asset-id'], userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']); + }); + }); + + describe('onAssetsRestore', () => { + it('should send connected clients an event', () => { + sut.onAssetsRestore({ assetIds: ['asset-id'], userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_restore', 'user-id', ['asset-id']); + }); + }); + + describe('onStackCreate', () => { + it('should send connected clients an event', () => { + sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + + describe('onStackUpdate', () => { + it('should send connected clients an event', () => { + sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + + describe('onStackDelete', () => { + it('should send connected clients an event', () => { + sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + + describe('onStacksDelete', () => { + it('should send connected clients an event', () => { + sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + describe('sendTestEmail', () => { it('should throw error if user could not be found', async () => { await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow('User not found'); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 01da235bf0..4eef49c631 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -60,7 +60,6 @@ export class NotificationService { @OnEmit({ event: 'asset.hide' }) onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) { - // Notify clients to hide the linked live photo asset this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId); } @@ -69,6 +68,46 @@ export class NotificationService { await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAIL, data: { id: assetId, notify: true } }); } + @OnEmit({ event: 'asset.trash' }) + onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, [assetId]); + } + + @OnEmit({ event: 'asset.delete' }) + onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, userId, assetId); + } + + @OnEmit({ event: 'assets.trash' }) + onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, assetIds); + } + + @OnEmit({ event: 'assets.restore' }) + onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, userId, assetIds); + } + + @OnEmit({ event: 'stack.create' }) + onStackCreate({ userId }: ArgOf<'stack.create'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + + @OnEmit({ event: 'stack.update' }) + onStackUpdate({ userId }: ArgOf<'stack.update'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + + @OnEmit({ event: 'stack.delete' }) + onStackDelete({ userId }: ArgOf<'stack.delete'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + + @OnEmit({ event: 'stacks.delete' }) + onStacksDelete({ userId }: ArgOf<'stacks.delete'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + @OnEmit({ event: 'user.signup' }) async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) { if (notify) { diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index bebc8517d6..29a598d4b4 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -4,7 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { requireAccess } from 'src/utils/access'; @@ -30,7 +30,7 @@ export class StackService { const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stack.create', { stackId: stack.id, userId: auth.user.id }); return mapStack(stack, { auth }); } @@ -50,7 +50,7 @@ export class StackService { const updatedStack = await this.stackRepository.update({ id, primaryAssetId: dto.primaryAssetId }); - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stack.update', { stackId: id, userId: auth.user.id }); return mapStack(updatedStack, { auth }); } @@ -58,15 +58,13 @@ export class StackService { async delete(auth: AuthDto, id: string): Promise { await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] }); await this.stackRepository.delete(id); - - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id }); } async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); await this.stackRepository.deleteAll(dto.ids); - - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id }); } private async findOrFail(id: string) { diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index 73a4f3d57b..5c0609956a 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException } from '@nestjs/common'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { TrashService } from 'src/services/trash.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -62,9 +62,7 @@ describe(TrashService.name, () => { assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.image.id]); - expect(eventMock.clientSend).toHaveBeenCalledWith(ClientEvent.ASSET_RESTORE, authStub.user1.user.id, [ - assetStub.image.id, - ]); + expect(eventMock.emit).toHaveBeenCalledWith('assets.restore', { assetIds: ['asset-id'], userId: 'user-id' }); }); }); diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index ac141521dd..712b9e50f2 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -5,7 +5,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; import { requireAccess } from 'src/utils/access'; import { usePagination } from 'src/utils/pagination'; @@ -64,6 +64,6 @@ export class TrashService { } await this.assetRepository.restoreAll(ids); - this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, auth.user.id, ids); + await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id }); } }