feat(03-01): add images table and repository

- Add images table with entryId foreign key and cascade delete
- Add Image and NewImage types
- Add ImageRepository with create, getById, getByEntryId, delete, deleteByEntryId
- Install sharp for thumbnail generation
This commit is contained in:
Thomas Richter
2026-01-29 15:22:38 +01:00
parent b09ac9013b
commit f5b5034f07
4 changed files with 598 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
import { eq, desc, asc, ne } from 'drizzle-orm';
import { db } from './index';
import { entries, type Entry, type NewEntry } from './schema';
import { entries, images, type Entry, type NewEntry, type Image, type NewImage } from './schema';
import { nanoid } from 'nanoid';
export interface EntryRepository {
@@ -86,3 +86,56 @@ class SQLiteEntryRepository implements EntryRepository {
// Singleton instance
export const entryRepository: EntryRepository = new SQLiteEntryRepository();
// Image Repository
export interface ImageRepository {
create(image: Omit<NewImage, 'id' | 'createdAt'>): Image;
getById(id: string): Image | undefined;
getByEntryId(entryId: string): Image[];
delete(id: string): boolean;
deleteByEntryId(entryId: string): number;
}
class SQLiteImageRepository implements ImageRepository {
create(image: Omit<NewImage, 'id' | 'createdAt'>): Image {
const newImage: NewImage = {
...image,
id: nanoid(),
createdAt: new Date().toISOString()
};
db.insert(images).values(newImage).run();
return this.getById(newImage.id)!;
}
getById(id: string): Image | undefined {
return db.select().from(images).where(eq(images.id, id)).get();
}
getByEntryId(entryId: string): Image[] {
return db
.select()
.from(images)
.where(eq(images.entryId, entryId))
.orderBy(asc(images.createdAt))
.all();
}
delete(id: string): boolean {
const existing = this.getById(id);
if (!existing) return false;
db.delete(images).where(eq(images.id, id)).run();
return true;
}
deleteByEntryId(entryId: string): number {
const existing = this.getByEntryId(entryId);
if (existing.length === 0) return 0;
db.delete(images).where(eq(images.entryId, entryId)).run();
return existing.length;
}
}
export const imageRepository: ImageRepository = new SQLiteImageRepository();

View File

@@ -20,3 +20,19 @@ export const entries = sqliteTable('entries', {
// Type inference for TypeScript
export type Entry = typeof entries.$inferSelect;
export type NewEntry = typeof entries.$inferInsert;
// Images table: attachments linked to entries
export const images = sqliteTable('images', {
id: text('id').primaryKey(), // nanoid
entryId: text('entry_id')
.notNull()
.references(() => entries.id, { onDelete: 'cascade' }),
filename: text('filename').notNull(), // original filename for display
ext: text('ext').notNull(), // file extension without dot: jpg, png, etc.
createdAt: text('created_at')
.notNull()
.$defaultFn(() => new Date().toISOString())
});
export type Image = typeof images.$inferSelect;
export type NewImage = typeof images.$inferInsert;