pull/24552/merge
Pacien B 2025-12-17 20:47:19 +01:00 committed by GitHub
commit 31cf7d0e94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 76 additions and 3 deletions

View File

@ -88,6 +88,28 @@ export class TagRepository {
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 })
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
async getAssetIds(tagId: string, assetIds: string[]): Promise<Set<string>> {

View File

@ -169,12 +169,53 @@ describe(TagService.name, () => {
expect(mocks.tag.delete).not.toHaveBeenCalled();
});
it('should remove a tag', async () => {
mocks.tag.get.mockResolvedValue(tagStub.tag);
it('should remove a tag and emit AssetUntag events for affected assets', async () => {
mocks.tag.getDescendantTagIds.mockResolvedValue(['tag-1']);
mocks.tag.getAssetIdsForTag.mockResolvedValue(['asset-1', 'asset-2']);
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).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();
});
});

View File

@ -70,9 +70,19 @@ export class TagService extends BaseService {
async remove(auth: AuthDto, id: string): Promise<void> {
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);
for (const assetId of affectedAssetIds) {
await this.eventRepository.emit('AssetUntag', { assetId });
}
}
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {