mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge 0be782afa6 into f9c05af45f
This commit is contained in:
commit
31fcc312b0
3 changed files with 76 additions and 3 deletions
|
|
@ -88,6 +88,28 @@ export class TagRepository {
|
||||||
await this.db.deleteFrom('tag').where('id', '=', id).execute();
|
await this.db.deleteFrom('tag').where('id', '=', id).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async getAssetIdsForTag(tagId: string): Promise<string[]> {
|
||||||
|
const results = await this.db
|
||||||
|
.selectFrom('tag_asset')
|
||||||
|
.select(['assetId'])
|
||||||
|
.where('tagId', '=', tagId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return results.map(({ assetId }) => assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async getDescendantTagIds(tagId: string): Promise<string[]> {
|
||||||
|
const results = await this.db
|
||||||
|
.selectFrom('tag_closure')
|
||||||
|
.select(['id_descendant'])
|
||||||
|
.where('id_ancestor', '=', tagId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return results.map(({ id_descendant }) => id_descendant);
|
||||||
|
}
|
||||||
|
|
||||||
@ChunkedSet({ paramIndex: 1 })
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||||
async getAssetIds(tagId: string, assetIds: string[]): Promise<Set<string>> {
|
async getAssetIds(tagId: string, assetIds: string[]): Promise<Set<string>> {
|
||||||
|
|
|
||||||
|
|
@ -169,12 +169,53 @@ describe(TagService.name, () => {
|
||||||
expect(mocks.tag.delete).not.toHaveBeenCalled();
|
expect(mocks.tag.delete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove a tag', async () => {
|
it('should remove a tag and emit AssetUntag events for affected assets', async () => {
|
||||||
mocks.tag.get.mockResolvedValue(tagStub.tag);
|
mocks.tag.getDescendantTagIds.mockResolvedValue(['tag-1']);
|
||||||
|
mocks.tag.getAssetIdsForTag.mockResolvedValue(['asset-1', 'asset-2']);
|
||||||
mocks.tag.delete.mockResolvedValue();
|
mocks.tag.delete.mockResolvedValue();
|
||||||
|
|
||||||
await sut.remove(authStub.admin, 'tag-1');
|
await sut.remove(authStub.admin, 'tag-1');
|
||||||
|
|
||||||
|
expect(mocks.tag.getDescendantTagIds).toHaveBeenCalledWith('tag-1');
|
||||||
|
expect(mocks.tag.getAssetIdsForTag).toHaveBeenCalledWith('tag-1');
|
||||||
expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1');
|
expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1');
|
||||||
|
expect(mocks.event.emit).toHaveBeenNthCalledWith(1, 'AssetUntag', { assetId: 'asset-1' });
|
||||||
|
expect(mocks.event.emit).toHaveBeenNthCalledWith(2, 'AssetUntag', { assetId: 'asset-2' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a tag with descendants and emit AssetUntag events for all affected assets', async () => {
|
||||||
|
mocks.tag.getDescendantTagIds.mockResolvedValue(['tag-1', 'tag-1-child', 'tag-1-grandchild']);
|
||||||
|
mocks.tag.getAssetIdsForTag.mockResolvedValueOnce(['asset-1', 'asset-2']);
|
||||||
|
mocks.tag.getAssetIdsForTag.mockResolvedValueOnce(['asset-2', 'asset-3']);
|
||||||
|
mocks.tag.getAssetIdsForTag.mockResolvedValueOnce(['asset-4']);
|
||||||
|
mocks.tag.delete.mockResolvedValue();
|
||||||
|
|
||||||
|
await sut.remove(authStub.admin, 'tag-1');
|
||||||
|
|
||||||
|
expect(mocks.tag.getDescendantTagIds).toHaveBeenCalledWith('tag-1');
|
||||||
|
expect(mocks.tag.getAssetIdsForTag).toHaveBeenNthCalledWith(1, 'tag-1');
|
||||||
|
expect(mocks.tag.getAssetIdsForTag).toHaveBeenNthCalledWith(2, 'tag-1-child');
|
||||||
|
expect(mocks.tag.getAssetIdsForTag).toHaveBeenNthCalledWith(3, 'tag-1-grandchild');
|
||||||
|
expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1');
|
||||||
|
// Should emit events for all affected assets (de-duplicated)
|
||||||
|
expect(mocks.event.emit).toHaveBeenCalledTimes(4);
|
||||||
|
expect(mocks.event.emit).toHaveBeenCalledWith('AssetUntag', { assetId: 'asset-1' });
|
||||||
|
expect(mocks.event.emit).toHaveBeenCalledWith('AssetUntag', { assetId: 'asset-2' });
|
||||||
|
expect(mocks.event.emit).toHaveBeenCalledWith('AssetUntag', { assetId: 'asset-3' });
|
||||||
|
expect(mocks.event.emit).toHaveBeenCalledWith('AssetUntag', { assetId: 'asset-4' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a tag with no affected assets', async () => {
|
||||||
|
mocks.tag.getDescendantTagIds.mockResolvedValue(['tag-1']);
|
||||||
|
mocks.tag.getAssetIdsForTag.mockResolvedValue([]);
|
||||||
|
mocks.tag.delete.mockResolvedValue();
|
||||||
|
|
||||||
|
await sut.remove(authStub.admin, 'tag-1');
|
||||||
|
|
||||||
|
expect(mocks.tag.getDescendantTagIds).toHaveBeenCalledWith('tag-1');
|
||||||
|
expect(mocks.tag.getAssetIdsForTag).toHaveBeenCalledWith('tag-1');
|
||||||
|
expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1');
|
||||||
|
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,19 @@ export class TagService extends BaseService {
|
||||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||||
await this.requireAccess({ auth, permission: Permission.TagDelete, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.TagDelete, ids: [id] });
|
||||||
|
|
||||||
// TODO sync tag changes for affected assets
|
const descendantTagIds = await this.tagRepository.getDescendantTagIds(id);
|
||||||
|
|
||||||
|
const affectedAssetIds = new Set<string>();
|
||||||
|
for (const tagId of descendantTagIds) {
|
||||||
|
const assetIds = await this.tagRepository.getAssetIdsForTag(tagId);
|
||||||
|
assetIds.forEach((assetId) => affectedAssetIds.add(assetId));
|
||||||
|
}
|
||||||
|
|
||||||
await this.tagRepository.delete(id);
|
await this.tagRepository.delete(id);
|
||||||
|
|
||||||
|
for (const assetId of affectedAssetIds) {
|
||||||
|
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
|
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue