mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat: add album asset event handling (#29008)
* feat: add album asset event handling * fix medium tests * patch medium test * collapse in AlbumUpdate
This commit is contained in:
parent
7a7303aceb
commit
ee4bd3f833
11 changed files with 64 additions and 35 deletions
|
|
@ -38,7 +38,7 @@ type EventMap = {
|
|||
ConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||
|
||||
// album events
|
||||
AlbumUpdate: [{ id: string; recipientId: string }];
|
||||
AlbumUpdate: [{ id: string; userIds: string[]; recipientIds: string[] }];
|
||||
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
||||
|
||||
// asset events
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export interface ClientEventMap {
|
|||
on_asset_hidden: [string];
|
||||
on_asset_restore: [string[]];
|
||||
on_asset_stack_update: string[];
|
||||
on_album_update: [string];
|
||||
on_person_thumbnail: [string];
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
|
|
|
|||
|
|
@ -841,7 +841,8 @@ describe(AlbumService.name, () => {
|
|||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: album.id,
|
||||
recipientId: owner.id,
|
||||
userIds: album.albumUsers.map(({ user }) => user.id),
|
||||
recipientIds: [owner.id],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1091,11 +1092,13 @@ describe(AlbumService.name, () => {
|
|||
]);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: album1.id,
|
||||
recipientId: owner1.id,
|
||||
userIds: album1.albumUsers.map(({ user }) => user.id),
|
||||
recipientIds: [owner1.id],
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: album2.id,
|
||||
recipientId: owner2.id,
|
||||
userIds: album2.albumUsers.map(({ user }) => user.id),
|
||||
recipientIds: [owner2.id],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -190,11 +190,9 @@ export class AlbumService extends BaseService {
|
|||
auth.user.id,
|
||||
);
|
||||
|
||||
const allUsersExceptUs = album.albumUsers.map(({ user }) => user.id).filter((userId) => userId !== auth.user.id);
|
||||
|
||||
for (const recipientId of allUsersExceptUs) {
|
||||
await this.eventRepository.emit('AlbumUpdate', { id, recipientId });
|
||||
}
|
||||
const userIds = album.albumUsers.map(({ user }) => user.id);
|
||||
const recipientIds = userIds.filter((userId) => userId !== auth.user.id);
|
||||
await this.eventRepository.emit('AlbumUpdate', { id, userIds, recipientIds });
|
||||
}
|
||||
|
||||
return results;
|
||||
|
|
@ -223,7 +221,7 @@ export class AlbumService extends BaseService {
|
|||
}
|
||||
|
||||
const albumAssetValues: { albumId: string; assetId: string }[] = [];
|
||||
const events: { id: string; recipients: string[] }[] = [];
|
||||
const events: { id: string; userIds: string[]; recipientIds: string[] }[] = [];
|
||||
for (const albumId of allowedAlbumIds) {
|
||||
const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]);
|
||||
const notPresentAssetIds = [...allowedAssetIds.difference(existingAssetIds)];
|
||||
|
|
@ -246,15 +244,14 @@ export class AlbumService extends BaseService {
|
|||
},
|
||||
auth.user.id,
|
||||
);
|
||||
const allUsersExceptUs = album.albumUsers.map(({ user }) => user.id).filter((userId) => userId !== auth.user.id);
|
||||
events.push({ id: albumId, recipients: allUsersExceptUs });
|
||||
const userIds = album.albumUsers.map(({ user }) => user.id);
|
||||
const recipientIds = userIds.filter((userId) => userId !== auth.user.id);
|
||||
events.push({ id: albumId, userIds, recipientIds });
|
||||
}
|
||||
|
||||
await this.albumRepository.addAssetIdsToAlbums(albumAssetValues);
|
||||
for (const event of events) {
|
||||
for (const recipientId of event.recipients) {
|
||||
await this.eventRepository.emit('AlbumUpdate', { id: event.id, recipientId });
|
||||
}
|
||||
await this.eventRepository.emit('AlbumUpdate', event);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
|
@ -271,8 +268,16 @@ export class AlbumService extends BaseService {
|
|||
);
|
||||
|
||||
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (removedIds.length > 0 && album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
if (removedIds.length > 0) {
|
||||
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
}
|
||||
|
||||
await this.eventRepository.emit('AlbumUpdate', {
|
||||
id,
|
||||
userIds: album.albumUsers.map(({ user }) => user.id),
|
||||
recipientIds: [],
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { defaults, SystemConfig } from 'src/config';
|
|||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { INotifyAlbumUpdateJob } from 'src/types';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
|
|
@ -157,13 +156,21 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
describe('onAlbumUpdateEvent', () => {
|
||||
it('should queue notify album update event', async () => {
|
||||
await sut.onAlbumUpdate({ id: 'album', recipientId: '42' });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
it('should send a websocket event to every user and queue notify jobs for recipients', async () => {
|
||||
await sut.onAlbumUpdate({ id: 'album', userIds: ['1', '42'], recipientIds: ['42'] });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_album_update', '1', 'album');
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_album_update', '42', 'album');
|
||||
expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({
|
||||
name: JobName.NotifyAlbumUpdate,
|
||||
data: { id: 'album', recipientId: '42', delay: 300_000 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not queue email jobs when there are no recipients', async () => {
|
||||
await sut.onAlbumUpdate({ id: 'album', userIds: ['1'], recipientIds: [] });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_album_update', '1', 'album');
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAlbumInviteEvent', () => {
|
||||
|
|
@ -522,7 +529,7 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should add new recipients for new images if job is already queued', async () => {
|
||||
await sut.onAlbumUpdate({ id: '1', recipientId: '2' } as INotifyAlbumUpdateJob);
|
||||
await sut.onAlbumUpdate({ id: '1', userIds: ['2'], recipientIds: ['2'] });
|
||||
expect(mocks.job.removeJob).toHaveBeenCalledWith(JobName.NotifyAlbumUpdate, '1/2');
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.NotifyAlbumUpdate,
|
||||
|
|
|
|||
|
|
@ -217,12 +217,18 @@ export class NotificationService extends BaseService {
|
|||
}
|
||||
|
||||
@OnEvent({ name: 'AlbumUpdate' })
|
||||
async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) {
|
||||
await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipientId}`);
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NotifyAlbumUpdate,
|
||||
data: { id, recipientId, delay: NotificationService.albumUpdateEmailDelayMs },
|
||||
});
|
||||
async onAlbumUpdate({ id, userIds, recipientIds }: ArgOf<'AlbumUpdate'>) {
|
||||
for (const userId of userIds) {
|
||||
this.websocketRepository.clientSend('on_album_update', userId, id);
|
||||
}
|
||||
|
||||
for (const recipientId of recipientIds) {
|
||||
await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipientId}`);
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NotifyAlbumUpdate,
|
||||
data: { id, recipientId, delay: NotificationService.albumUpdateEmailDelayMs },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AlbumInvite' })
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { AssetRepository } from 'src/repositories/asset.repository';
|
|||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
|
|
@ -39,7 +40,7 @@ class WorkflowTestContext extends MediumTestContext<WorkflowExecutionService> {
|
|||
UserRepository,
|
||||
WorkflowRepository,
|
||||
],
|
||||
mock: [ConfigRepository],
|
||||
mock: [ConfigRepository, EventRepository],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +53,7 @@ class WorkflowTestContext extends MediumTestContext<WorkflowExecutionService> {
|
|||
mockData.resourcePaths.corePlugin = '../packages/plugin-core';
|
||||
mockData.plugins.external.allow = false;
|
||||
this.getMock(ConfigRepository).getEnv.mockReturnValue(mockData);
|
||||
this.getMock(EventRepository).emit.mockResolvedValue();
|
||||
this.get(LoggingRepository).setLogLevel(LogLevel.Verbose);
|
||||
|
||||
await this.sut.onPluginSync();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue