diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 2678f861ea..d9c44f4ba4 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -165,18 +165,14 @@ export class TagRepository { async deleteEmptyTags() { const result = await this.db .deleteFrom('tag') - .where('id', 'not in', (eb) => eb.selectFrom('tag_asset').select('tagsId')) - .where('id', 'not in', (eb) => - eb - .selectFrom('tag as child') - .select('child.parentId') - .where('child.parentId', 'is not', null) - .where((eb2) => - eb2.or([ - eb2('child.id', 'in', (eb3) => eb3.selectFrom('tag_asset').select('tagsId')), - eb2('child.id', 'not in', (eb3) => eb3.selectFrom('tag_asset').select('tagsId')), - ]), + .where(({ not, exists, selectFrom }) => + not( + exists( + selectFrom('tag_closure') + .whereRef('tag.id', '=', 'tag_closure.id_ancestor') + .innerJoin('tag_asset', 'tag_closure.id_descendant', 'tag_asset.tagsId'), ), + ), ) .executeTakeFirst(); diff --git a/server/test/medium/specs/services/tag.service.spec.ts b/server/test/medium/specs/services/tag.service.spec.ts index 508a10d7f3..2ec498e56d 100644 --- a/server/test/medium/specs/services/tag.service.spec.ts +++ b/server/test/medium/specs/services/tag.service.spec.ts @@ -5,6 +5,7 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { TagRepository } from 'src/repositories/tag.repository'; import { DB } from 'src/schema'; import { TagService } from 'src/services/tag.service'; +import { upsertTags } from 'src/utils/tag'; import { newMediumService } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; @@ -27,8 +28,8 @@ describe(TagService.name, () => { it('single tag exists, not connected to any assets, and is deleted', async () => { const { sut, ctx } = setup(); const { user } = await ctx.newUser(); - const { tag } = await ctx.newTag({ userId: user.id, value: 'tag-1' }); const tagRepo = ctx.get(TagRepository); + const [tag] = await upsertTags(tagRepo, { userId: user.id, tags: ['tag-1'] }); await expect(tagRepo.getByValue(user.id, 'tag-1')).resolves.toEqual(expect.objectContaining({ id: tag.id })); await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.Success); @@ -39,8 +40,8 @@ describe(TagService.name, () => { const { sut, ctx } = setup(); const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id }); - const { tag } = await ctx.newTag({ userId: user.id, value: 'tag-1' }); const tagRepo = ctx.get(TagRepository); + const [tag] = await upsertTags(tagRepo, { userId: user.id, tags: ['tag-1'] }); await ctx.newTagAsset({ tagIds: [tag.id], assetIds: [asset.id] }); @@ -53,48 +54,63 @@ describe(TagService.name, () => { const { sut, ctx } = setup(); const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id }); - const { tag: parentTag } = await ctx.newTag({ userId: user.id, value: 'parent' }); - const { tag: childrenTag } = await ctx.newTag({ userId: user.id, value: 'child', parentId: parentTag.id }); const tagRepo = ctx.get(TagRepository); + const [parentTag, childTag] = await upsertTags(tagRepo, { userId: user.id, tags: ['parent', 'parent/child'] }); await ctx.newTagAsset({ tagIds: [parentTag.id], assetIds: [asset.id] }); await expect(tagRepo.getByValue(user.id, 'parent')).resolves.toEqual( expect.objectContaining({ id: parentTag.id }), ); - await expect(tagRepo.getByValue(user.id, 'child')).resolves.toEqual( - expect.objectContaining({ id: childrenTag.id }), + await expect(tagRepo.getByValue(user.id, 'parent/child')).resolves.toEqual( + expect.objectContaining({ id: childTag.id }), ); await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.Success); await expect(tagRepo.getByValue(user.id, 'parent')).resolves.toEqual( expect.objectContaining({ id: parentTag.id }), ); - await expect(tagRepo.getByValue(user.id, 'child')).resolves.toBeUndefined(); + await expect(tagRepo.getByValue(user.id, 'parent/child')).resolves.toBeUndefined(); }); it('hierarchical tag exists, and only the child is connected to an asset, and nothing is deleted', async () => { const { sut, ctx } = setup(); const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id }); - const { tag: parentTag } = await ctx.newTag({ userId: user.id, value: 'parent' }); - const { tag: childrenTag } = await ctx.newTag({ userId: user.id, value: 'child', parentId: parentTag.id }); const tagRepo = ctx.get(TagRepository); + const [parentTag, childTag] = await upsertTags(tagRepo, { userId: user.id, tags: ['parent', 'parent/child'] }); - await ctx.newTagAsset({ tagIds: [childrenTag.id], assetIds: [asset.id] }); + await ctx.newTagAsset({ tagIds: [childTag.id], assetIds: [asset.id] }); await expect(tagRepo.getByValue(user.id, 'parent')).resolves.toEqual( expect.objectContaining({ id: parentTag.id }), ); - await expect(tagRepo.getByValue(user.id, 'child')).resolves.toEqual( - expect.objectContaining({ id: childrenTag.id }), + await expect(tagRepo.getByValue(user.id, 'parent/child')).resolves.toEqual( + expect.objectContaining({ id: childTag.id }), ); await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.Success); await expect(tagRepo.getByValue(user.id, 'parent')).resolves.toEqual( expect.objectContaining({ id: parentTag.id }), ); - await expect(tagRepo.getByValue(user.id, 'child')).resolves.toEqual( - expect.objectContaining({ id: childrenTag.id }), + await expect(tagRepo.getByValue(user.id, 'parent/child')).resolves.toEqual( + expect.objectContaining({ id: childTag.id }), ); }); + + it('hierarchical tag exists, and neither parent nor child is connected to an asset, and both are deleted', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const tagRepo = ctx.get(TagRepository); + const [parentTag, childTag] = await upsertTags(tagRepo, { userId: user.id, tags: ['parent', 'parent/child'] }); + + await expect(tagRepo.getByValue(user.id, 'parent')).resolves.toEqual( + expect.objectContaining({ id: parentTag.id }), + ); + await expect(tagRepo.getByValue(user.id, 'parent/child')).resolves.toEqual( + expect.objectContaining({ id: childTag.id }), + ); + await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.Success); + await expect(tagRepo.getByValue(user.id, 'parent/child')).resolves.toBeUndefined(); + await expect(tagRepo.getByValue(user.id, 'parent')).resolves.toBeUndefined(); + }); }); });