feat(04-01): create tagRepository with tag operations

- Add TagRepository interface with findOrCreate, getAll, getById, getByEntryId, updateEntryTags
- Implement SQLiteTagRepository with case-insensitive tag lookup
- updateEntryTags atomically replaces all tags for an entry
- Export tagRepository singleton
This commit is contained in:
Thomas Richter
2026-01-31 13:03:53 +01:00
parent 7dc63e625d
commit 378d928b53

View File

@@ -1,6 +1,17 @@
import { eq, desc, asc, ne } from 'drizzle-orm';
import { eq, desc, asc, ne, sql } from 'drizzle-orm';
import { db } from './index';
import { entries, images, type Entry, type NewEntry, type Image, type NewImage } from './schema';
import {
entries,
images,
tags,
entryTags,
lower,
type Entry,
type NewEntry,
type Image,
type NewImage,
type Tag
} from './schema';
import { nanoid } from 'nanoid';
export interface EntryRepository {
@@ -139,3 +150,71 @@ class SQLiteImageRepository implements ImageRepository {
}
export const imageRepository: ImageRepository = new SQLiteImageRepository();
// Tag Repository
export interface TagRepository {
findOrCreate(name: string): Tag;
getAll(): Tag[];
getById(id: string): Tag | undefined;
getByEntryId(entryId: string): Tag[];
updateEntryTags(entryId: string, tagNames: string[]): void;
}
class SQLiteTagRepository implements TagRepository {
findOrCreate(name: string): Tag {
const normalizedName = name.trim();
// Try to find existing tag (case-insensitive)
const existing = db
.select()
.from(tags)
.where(sql`lower(${tags.name}) = lower(${normalizedName})`)
.get();
if (existing) return existing;
// Create new tag
const newTag: Tag = {
id: nanoid(),
name: normalizedName,
createdAt: new Date().toISOString()
};
db.insert(tags).values(newTag).run();
return newTag;
}
getAll(): Tag[] {
return db.select().from(tags).orderBy(asc(tags.name)).all();
}
getById(id: string): Tag | undefined {
return db.select().from(tags).where(eq(tags.id, id)).get();
}
getByEntryId(entryId: string): Tag[] {
return db
.select({
id: tags.id,
name: tags.name,
createdAt: tags.createdAt
})
.from(entryTags)
.innerJoin(tags, eq(entryTags.tagId, tags.id))
.where(eq(entryTags.entryId, entryId))
.orderBy(asc(tags.name))
.all();
}
updateEntryTags(entryId: string, tagNames: string[]): void {
// Delete existing associations
db.delete(entryTags).where(eq(entryTags.entryId, entryId)).run();
// Find or create each tag and create associations
for (const name of tagNames) {
const tag = this.findOrCreate(name);
db.insert(entryTags).values({ entryId, tagId: tag.id }).run();
}
}
}
export const tagRepository: TagRepository = new SQLiteTagRepository();