fix(server): emit sidecarwrites and assetupdates consequently (#31777)

* fix(server): emit sidecarwrites and assetupdates consequently

* fix medium test
This commit is contained in:
Mees Frensel
2026-09-28 14:36:05 -04:00
committed by GitHub
parent c25b6e30c2
commit 5f0c9a5996
4 changed files with 51 additions and 6 deletions
+25 -2
View File
@@ -184,6 +184,11 @@ describe(AssetService.name, () => {
await sut.update(authStub.admin, asset.id, { isFavorite: true });
expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, isFavorite: true });
expect(mocks.websocket.clientSend).toHaveBeenCalledWith(
'on_asset_update',
authStub.admin.user.id,
expect.objectContaining({ id: asset.id }),
);
});
it('should update the exif description', async () => {
@@ -220,6 +225,8 @@ describe(AssetService.name, () => {
lockedPropertiesBehavior: 'append',
}),
);
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarWrite, data: { id: asset.id } });
});
it('should fail linking a live video if the motion part could not be found', async () => {
@@ -384,6 +391,22 @@ describe(AssetService.name, () => {
});
});
it('should emit a websocket event if a sidecar write is not necessary', async () => {
const auth = AuthFactory.create();
const asset1 = AssetFactory.from().owner(auth.user).build();
const asset2 = AssetFactory.from().owner(auth.user).build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
await sut.updateAll(auth, { ids: [asset1.id, asset2.id], visibility: AssetVisibility.Archive });
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset1.id, asset2.id], {
visibility: AssetVisibility.Archive,
});
expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(2);
});
it('should not update Assets table if no relevant fields are provided', async () => {
const auth = AuthFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
@@ -736,7 +759,7 @@ describe(AssetService.name, () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
await expect(sut.upsertMetadata(authStub.admin, asset.id, { items })).rejects.toThrowError(
await expect(sut.upsertMetadata(authStub.admin, asset.id, { items })).rejects.toThrow(
'Duplicate items are not allowed:',
);
@@ -754,7 +777,7 @@ describe(AssetService.name, () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
await expect(sut.upsertBulkMetadata(authStub.admin, { items })).rejects.toThrowError(
await expect(sut.upsertBulkMetadata(authStub.admin, { items })).rejects.toThrow(
'Duplicate items are not allowed:',
);
+18 -3
View File
@@ -113,7 +113,7 @@ export class AssetService extends BaseService {
}
}
await this.updateExif({ id, description, dateTimeOriginal, latitude, longitude, rating });
const wroteMetadata = await this.updateExif({ id, description, dateTimeOriginal, latitude, longitude, rating });
const asset = await this.assetRepository.update({ id, ...rest });
@@ -129,6 +129,10 @@ export class AssetService extends BaseService {
throw new BadRequestException('Asset not found');
}
if (!wroteMetadata) {
this.websocketRepository.clientSend('on_asset_update', auth.user.id, mapAsset(asset, { auth }));
}
return this.get(auth, id) as Promise<AssetResponseDto>;
}
@@ -160,8 +164,10 @@ export class AssetService extends BaseService {
isUndefined,
);
let shouldWriteSidecar = false;
if (Object.keys(exifDto).length > 0) {
await this.assetRepository.updateAllExif(ids, exifDto);
shouldWriteSidecar = true;
}
const extractedTimeZone = extractTimeZone(dateTimeOriginal);
@@ -172,6 +178,7 @@ export class AssetService extends BaseService {
extractedTimeZone?.type === 'fixed'
) {
await this.assetRepository.updateDateTimeOriginal(ids, dateTimeRelative, timeZone ?? extractedTimeZone?.name);
shouldWriteSidecar = true;
}
if (Object.keys(assetDto).length > 0) {
@@ -182,7 +189,14 @@ export class AssetService extends BaseService {
await this.albumRepository.removeAssetsFromAll(ids);
}
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.SidecarWrite, data: { id } })));
if (shouldWriteSidecar) {
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.SidecarWrite, data: { id } })));
} else {
const assets = await this.assetRepository.getByIds(ids);
for (const asset of assets) {
this.websocketRepository.clientSend('on_asset_update', auth.user.id, mapAsset(asset, { auth }));
}
}
}
async copy(
@@ -508,7 +522,7 @@ export class AssetService extends BaseService {
);
if (Object.keys(writes).length === 0) {
return;
return false;
}
await this.assetRepository.upsertExif({
@@ -519,6 +533,7 @@ export class AssetService extends BaseService {
lockedPropertiesBehavior: 'append',
});
await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id } });
return true;
}
async getAssetEdits(auth: AuthDto, id: string): Promise<AssetEditsResponseDto> {
+5
View File
@@ -64,6 +64,7 @@ import { TagRepository } from 'src/repositories/tag.repository.js';
import { TelemetryRepository } from 'src/repositories/telemetry.repository.js';
import { UserRepository } from 'src/repositories/user.repository.js';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository.js';
import { WebsocketRepository } from 'src/repositories/websocket.repository.js';
import { WorkflowRepository } from 'src/repositories/workflow.repository.js';
import { DB } from 'src/schema/index.js';
import { AlbumTable } from 'src/schema/tables/album.table.js';
@@ -616,6 +617,10 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
return automock(StorageRepository, { args: [{ setContext: () => {} }] });
}
case WebsocketRepository: {
return automock(WebsocketRepository, { args: [undefined, { setContext: () => {} }] });
}
default: {
throw new Error(`Invalid repository key: ${key}`);
}
@@ -14,6 +14,7 @@ import { LoggingRepository } from 'src/repositories/logging.repository.js';
import { PluginRepository } from 'src/repositories/plugin.repository.js';
import { StorageRepository } from 'src/repositories/storage.repository.js';
import { UserRepository } from 'src/repositories/user.repository.js';
import { WebsocketRepository } from 'src/repositories/websocket.repository.js';
import { WorkflowRepository } from 'src/repositories/workflow.repository.js';
import { DB } from 'src/schema/index.js';
import { WorkflowExecutionService } from 'src/services/workflow-execution.service.js';
@@ -40,7 +41,7 @@ class WorkflowTestContext extends MediumTestContext<typeof WorkflowExecutionServ
UserRepository,
WorkflowRepository,
],
mock: [ConfigRepository, EventRepository],
mock: [ConfigRepository, EventRepository, WebsocketRepository],
});
}
@@ -54,6 +55,7 @@ class WorkflowTestContext extends MediumTestContext<typeof WorkflowExecutionServ
mockData.plugins.external.allow = false;
this.getMock(ConfigRepository).getEnv.mockReturnValue(mockData);
this.getMock(EventRepository).emit.mockResolvedValue();
this.getMock(WebsocketRepository).clientSend.mockReturnValue();
this.get(LoggingRepository).setLogLevel(LogLevel.Verbose);
await this.sut.onPluginSync();