fix(server): correct tag create operations (#30877)

This commit is contained in:
Jordan Brock
2026-08-24 17:57:32 +02:00
committed by GitHub
parent 6b7b0fe3a6
commit 1b3aa9cd51
6 changed files with 347 additions and 53 deletions
@@ -29,6 +29,86 @@ describe(TagRepository.name, () => {
await ctx.database.deleteFrom('tag').execute();
});
describe('create', () => {
it('should create a top-level tag with proper closures', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const result = await sut.create({
userId: user.id,
value: 'tagA',
color: '#000000',
});
await expect(
ctx.database
.selectFrom('tag')
.select(['userId', 'value', 'color', 'parentId'])
.where('id', '=', result.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ userId: user.id, value: 'tagA', color: '#000000', parentId: null });
await expect(
ctx.database
.selectFrom('tag_closure')
.select(['id_ancestor', 'id_descendant'])
.where('id_ancestor', '=', result.id)
.execute(),
).resolves.toEqual([{ id_ancestor: result.id, id_descendant: result.id }]);
});
it('should create a child tag with proper closures', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const resultParent = await sut.create({
userId: user.id,
value: 'tagA',
color: '#000000',
});
const resultChild = await sut.create({
userId: user.id,
value: 'tagA/tagB',
color: '#00FF00',
parentId: resultParent.id,
});
await expect(
ctx.database
.selectFrom('tag')
.select(['userId', 'value', 'color', 'parentId'])
.where('id', '=', resultParent.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ userId: user.id, value: 'tagA', color: '#000000', parentId: null });
await expect(
ctx.database
.selectFrom('tag')
.select(['userId', 'value', 'color', 'parentId'])
.where('id', '=', resultChild.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ userId: user.id, value: 'tagA/tagB', color: '#00FF00', parentId: resultParent.id });
await expect(
ctx.database
.selectFrom('tag_closure')
.select(['id_ancestor', 'id_descendant'])
.where('id_ancestor', '=', resultParent.id)
.execute(),
).resolves.toEqual([
{ id_ancestor: resultParent.id, id_descendant: resultParent.id },
{ id_ancestor: resultParent.id, id_descendant: resultChild.id },
]);
await expect(
ctx.database
.selectFrom('tag_closure')
.select(['id_ancestor', 'id_descendant'])
.where('id_ancestor', '=', resultChild.id)
.execute(),
).resolves.toEqual([{ id_ancestor: resultChild.id, id_descendant: resultChild.id }]);
});
});
describe('update', () => {
it('should update a tag color', async () => {
const { ctx, sut } = setup();
@@ -71,4 +151,138 @@ describe(TagRepository.name, () => {
).resolves.toEqual({ userId: user.id, value: 'updatedTagA', color: '#000000', parentId: null });
});
});
describe('delete', () => {
it('should delete top-level tag without descendants', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { tag } = await ctx.newTag({
userId: user.id,
value: 'tagA',
color: '#000000',
});
await expect(
ctx.database
.selectFrom('tag')
.select(['userId', 'value', 'color', 'parentId'])
.where('id', '=', tag.id)
.executeTakeFirst(),
).resolves.toEqual({ userId: user.id, value: 'tagA', color: '#000000', parentId: null });
await sut.delete(tag.id);
await expect(
ctx.database.selectFrom('tag').selectAll().where('id', '=', tag.id).executeTakeFirst(),
).resolves.toBeUndefined();
});
it('should delete a parent tag and all its descendants', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { tag: tag1 } = await ctx.newTag({
userId: user.id,
value: 'tagA',
color: '#000000',
});
const { tag: tag2 } = await ctx.newTag({
userId: user.id,
value: 'tagA/tagB',
color: '#00FF00',
parentId: tag1.id,
});
const { tag: tag3 } = await ctx.newTag({
userId: user.id,
value: 'tagA/tagB/tagC',
color: '#0000FF',
parentId: tag2.id,
});
const { tag: tag4 } = await ctx.newTag({
userId: user.id,
value: 'tagA/tagD',
color: '#FFFF00',
parentId: tag1.id,
});
const { tag: tag5 } = await ctx.newTag({
userId: user.id,
value: 'tagA/tagD/tagE',
color: '#FF00FF',
parentId: tag4.id,
});
await expect(
ctx.database
.selectFrom('tag')
.select(['userId', 'value', 'color', 'parentId'])
.where('id', 'in', [tag1.id, tag2.id, tag3.id, tag4.id, tag5.id])
.execute(),
).resolves.toEqual(
expect.arrayContaining([
{ userId: user.id, value: 'tagA', color: '#000000', parentId: null },
{ userId: user.id, value: 'tagA/tagB', color: '#00FF00', parentId: tag1.id },
{ userId: user.id, value: 'tagA/tagB/tagC', color: '#0000FF', parentId: tag2.id },
{ userId: user.id, value: 'tagA/tagD', color: '#FFFF00', parentId: tag1.id },
{ userId: user.id, value: 'tagA/tagD/tagE', color: '#FF00FF', parentId: tag4.id },
]),
);
await sut.delete(tag1.id);
await expect(
ctx.database
.selectFrom('tag')
.where('id', 'in', [tag1.id, tag2.id, tag3.id, tag4.id, tag5.id])
.executeTakeFirst(),
).resolves.toBeUndefined();
});
it('should not delete tags outside the hierarchy', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { tag: tag1 } = await ctx.newTag({
userId: user.id,
value: 'tagA',
color: '#000000',
});
const { tag: tag2 } = await ctx.newTag({
userId: user.id,
value: 'tagA/tagB',
color: '#00FF00',
parentId: tag1.id,
});
const { tag: tag3 } = await ctx.newTag({
userId: user.id,
value: 'tagC',
color: '#0000FF',
});
await expect(
ctx.database
.selectFrom('tag')
.select(['userId', 'value', 'color', 'parentId'])
.where('id', 'in', [tag1.id, tag2.id, tag3.id])
.execute(),
).resolves.toEqual(
expect.arrayContaining([
{ userId: user.id, value: 'tagA', color: '#000000', parentId: null },
{ userId: user.id, value: 'tagA/tagB', color: '#00FF00', parentId: tag1.id },
{ userId: user.id, value: 'tagC', color: '#0000FF', parentId: null },
]),
);
await sut.delete(tag1.id);
await expect(
ctx.database.selectFrom('tag').select(['userId', 'value', 'color', 'parentId']).execute(),
).resolves.toEqual([{ userId: user.id, value: 'tagC', color: '#0000FF', parentId: null }]);
});
});
});