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:
@@ -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 { 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';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export interface EntryRepository {
|
export interface EntryRepository {
|
||||||
@@ -139,3 +150,71 @@ class SQLiteImageRepository implements ImageRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const imageRepository: ImageRepository = new SQLiteImageRepository();
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user