From 378d928b534fbd7b3d9bae3d871a3863b6e1bc94 Mon Sep 17 00:00:00 2001 From: Thomas Richter Date: Sat, 31 Jan 2026 13:03:53 +0100 Subject: [PATCH] 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 --- src/lib/server/db/repository.ts | 83 ++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/lib/server/db/repository.ts b/src/lib/server/db/repository.ts index 4c0111f..e4dadee 100644 --- a/src/lib/server/db/repository.ts +++ b/src/lib/server/db/repository.ts @@ -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();