chore: auto-fix Biome formatting and configure lint rules
All checks were successful
CI / ci (push) Successful in 15s

Run biome check --write --unsafe to fix tabs, import ordering, and
non-null assertions across entire codebase. Disable a11y rules not
applicable to this single-user app. Exclude auto-generated routeTree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:51:34 +01:00
parent 4d0452b7b3
commit b496462df5
63 changed files with 4752 additions and 4672 deletions

View File

@@ -1,221 +1,261 @@
import { eq, desc, sql } from "drizzle-orm";
import { threads, threadCandidates, items, categories } from "../../db/schema.ts";
import { desc, eq, sql } from "drizzle-orm";
import { db as prodDb } from "../../db/index.ts";
import type { CreateThread, UpdateThread, CreateCandidate } from "../../shared/types.ts";
import {
categories,
items,
threadCandidates,
threads,
} from "../../db/schema.ts";
import type { CreateCandidate, CreateThread } from "../../shared/types.ts";
type Db = typeof prodDb;
export function createThread(db: Db = prodDb, data: CreateThread) {
return db
.insert(threads)
.values({ name: data.name, categoryId: data.categoryId })
.returning()
.get();
return db
.insert(threads)
.values({ name: data.name, categoryId: data.categoryId })
.returning()
.get();
}
export function getAllThreads(db: Db = prodDb, includeResolved = false) {
const query = db
.select({
id: threads.id,
name: threads.name,
status: threads.status,
resolvedCandidateId: threads.resolvedCandidateId,
categoryId: threads.categoryId,
categoryName: categories.name,
categoryIcon: categories.icon,
createdAt: threads.createdAt,
updatedAt: threads.updatedAt,
candidateCount: sql<number>`(
const query = db
.select({
id: threads.id,
name: threads.name,
status: threads.status,
resolvedCandidateId: threads.resolvedCandidateId,
categoryId: threads.categoryId,
categoryName: categories.name,
categoryIcon: categories.icon,
createdAt: threads.createdAt,
updatedAt: threads.updatedAt,
candidateCount: sql<number>`(
SELECT COUNT(*) FROM thread_candidates
WHERE thread_candidates.thread_id = threads.id
)`.as("candidate_count"),
minPriceCents: sql<number | null>`(
minPriceCents: sql<number | null>`(
SELECT MIN(price_cents) FROM thread_candidates
WHERE thread_candidates.thread_id = threads.id
)`.as("min_price_cents"),
maxPriceCents: sql<number | null>`(
maxPriceCents: sql<number | null>`(
SELECT MAX(price_cents) FROM thread_candidates
WHERE thread_candidates.thread_id = threads.id
)`.as("max_price_cents"),
})
.from(threads)
.innerJoin(categories, eq(threads.categoryId, categories.id))
.orderBy(desc(threads.createdAt));
})
.from(threads)
.innerJoin(categories, eq(threads.categoryId, categories.id))
.orderBy(desc(threads.createdAt));
if (!includeResolved) {
return query.where(eq(threads.status, "active")).all();
}
return query.all();
if (!includeResolved) {
return query.where(eq(threads.status, "active")).all();
}
return query.all();
}
export function getThreadWithCandidates(db: Db = prodDb, threadId: number) {
const thread = db.select().from(threads)
.where(eq(threads.id, threadId)).get();
if (!thread) return null;
const thread = db
.select()
.from(threads)
.where(eq(threads.id, threadId))
.get();
if (!thread) return null;
const candidateList = db
.select({
id: threadCandidates.id,
threadId: threadCandidates.threadId,
name: threadCandidates.name,
weightGrams: threadCandidates.weightGrams,
priceCents: threadCandidates.priceCents,
categoryId: threadCandidates.categoryId,
notes: threadCandidates.notes,
productUrl: threadCandidates.productUrl,
imageFilename: threadCandidates.imageFilename,
createdAt: threadCandidates.createdAt,
updatedAt: threadCandidates.updatedAt,
categoryName: categories.name,
categoryIcon: categories.icon,
})
.from(threadCandidates)
.innerJoin(categories, eq(threadCandidates.categoryId, categories.id))
.where(eq(threadCandidates.threadId, threadId))
.all();
const candidateList = db
.select({
id: threadCandidates.id,
threadId: threadCandidates.threadId,
name: threadCandidates.name,
weightGrams: threadCandidates.weightGrams,
priceCents: threadCandidates.priceCents,
categoryId: threadCandidates.categoryId,
notes: threadCandidates.notes,
productUrl: threadCandidates.productUrl,
imageFilename: threadCandidates.imageFilename,
createdAt: threadCandidates.createdAt,
updatedAt: threadCandidates.updatedAt,
categoryName: categories.name,
categoryIcon: categories.icon,
})
.from(threadCandidates)
.innerJoin(categories, eq(threadCandidates.categoryId, categories.id))
.where(eq(threadCandidates.threadId, threadId))
.all();
return { ...thread, candidates: candidateList };
return { ...thread, candidates: candidateList };
}
export function updateThread(db: Db = prodDb, threadId: number, data: Partial<{ name: string; categoryId: number }>) {
const existing = db.select({ id: threads.id }).from(threads)
.where(eq(threads.id, threadId)).get();
if (!existing) return null;
export function updateThread(
db: Db = prodDb,
threadId: number,
data: Partial<{ name: string; categoryId: number }>,
) {
const existing = db
.select({ id: threads.id })
.from(threads)
.where(eq(threads.id, threadId))
.get();
if (!existing) return null;
return db
.update(threads)
.set({ ...data, updatedAt: new Date() })
.where(eq(threads.id, threadId))
.returning()
.get();
return db
.update(threads)
.set({ ...data, updatedAt: new Date() })
.where(eq(threads.id, threadId))
.returning()
.get();
}
export function deleteThread(db: Db = prodDb, threadId: number) {
const thread = db.select().from(threads)
.where(eq(threads.id, threadId)).get();
if (!thread) return null;
const thread = db
.select()
.from(threads)
.where(eq(threads.id, threadId))
.get();
if (!thread) return null;
// Collect candidate image filenames for cleanup
const candidatesWithImages = db
.select({ imageFilename: threadCandidates.imageFilename })
.from(threadCandidates)
.where(eq(threadCandidates.threadId, threadId))
.all()
.filter((c) => c.imageFilename != null);
// Collect candidate image filenames for cleanup
const candidatesWithImages = db
.select({ imageFilename: threadCandidates.imageFilename })
.from(threadCandidates)
.where(eq(threadCandidates.threadId, threadId))
.all()
.filter((c) => c.imageFilename != null);
db.delete(threads).where(eq(threads.id, threadId)).run();
db.delete(threads).where(eq(threads.id, threadId)).run();
return { ...thread, candidateImages: candidatesWithImages.map((c) => c.imageFilename!) };
return {
...thread,
candidateImages: candidatesWithImages.map((c) => c.imageFilename!),
};
}
export function createCandidate(
db: Db = prodDb,
threadId: number,
data: Partial<CreateCandidate> & { name: string; categoryId: number; imageFilename?: string },
db: Db = prodDb,
threadId: number,
data: Partial<CreateCandidate> & {
name: string;
categoryId: number;
imageFilename?: string;
},
) {
return db
.insert(threadCandidates)
.values({
threadId,
name: data.name,
weightGrams: data.weightGrams ?? null,
priceCents: data.priceCents ?? null,
categoryId: data.categoryId,
notes: data.notes ?? null,
productUrl: data.productUrl ?? null,
imageFilename: data.imageFilename ?? null,
})
.returning()
.get();
return db
.insert(threadCandidates)
.values({
threadId,
name: data.name,
weightGrams: data.weightGrams ?? null,
priceCents: data.priceCents ?? null,
categoryId: data.categoryId,
notes: data.notes ?? null,
productUrl: data.productUrl ?? null,
imageFilename: data.imageFilename ?? null,
})
.returning()
.get();
}
export function updateCandidate(
db: Db = prodDb,
candidateId: number,
data: Partial<{
name: string;
weightGrams: number;
priceCents: number;
categoryId: number;
notes: string;
productUrl: string;
imageFilename: string;
}>,
db: Db = prodDb,
candidateId: number,
data: Partial<{
name: string;
weightGrams: number;
priceCents: number;
categoryId: number;
notes: string;
productUrl: string;
imageFilename: string;
}>,
) {
const existing = db.select({ id: threadCandidates.id }).from(threadCandidates)
.where(eq(threadCandidates.id, candidateId)).get();
if (!existing) return null;
const existing = db
.select({ id: threadCandidates.id })
.from(threadCandidates)
.where(eq(threadCandidates.id, candidateId))
.get();
if (!existing) return null;
return db
.update(threadCandidates)
.set({ ...data, updatedAt: new Date() })
.where(eq(threadCandidates.id, candidateId))
.returning()
.get();
return db
.update(threadCandidates)
.set({ ...data, updatedAt: new Date() })
.where(eq(threadCandidates.id, candidateId))
.returning()
.get();
}
export function deleteCandidate(db: Db = prodDb, candidateId: number) {
const candidate = db.select().from(threadCandidates)
.where(eq(threadCandidates.id, candidateId)).get();
if (!candidate) return null;
const candidate = db
.select()
.from(threadCandidates)
.where(eq(threadCandidates.id, candidateId))
.get();
if (!candidate) return null;
db.delete(threadCandidates).where(eq(threadCandidates.id, candidateId)).run();
return candidate;
db.delete(threadCandidates).where(eq(threadCandidates.id, candidateId)).run();
return candidate;
}
export function resolveThread(
db: Db = prodDb,
threadId: number,
candidateId: number,
db: Db = prodDb,
threadId: number,
candidateId: number,
): { success: boolean; item?: any; error?: string } {
return db.transaction((tx) => {
// 1. Check thread is active
const thread = tx.select().from(threads)
.where(eq(threads.id, threadId)).get();
if (!thread || thread.status !== "active") {
return { success: false, error: "Thread not active" };
}
return db.transaction((tx) => {
// 1. Check thread is active
const thread = tx
.select()
.from(threads)
.where(eq(threads.id, threadId))
.get();
if (!thread || thread.status !== "active") {
return { success: false, error: "Thread not active" };
}
// 2. Get the candidate data
const candidate = tx.select().from(threadCandidates)
.where(eq(threadCandidates.id, candidateId)).get();
if (!candidate) {
return { success: false, error: "Candidate not found" };
}
if (candidate.threadId !== threadId) {
return { success: false, error: "Candidate not in thread" };
}
// 2. Get the candidate data
const candidate = tx
.select()
.from(threadCandidates)
.where(eq(threadCandidates.id, candidateId))
.get();
if (!candidate) {
return { success: false, error: "Candidate not found" };
}
if (candidate.threadId !== threadId) {
return { success: false, error: "Candidate not in thread" };
}
// 3. Verify categoryId still exists, fallback to Uncategorized (id=1)
const category = tx.select({ id: categories.id }).from(categories)
.where(eq(categories.id, candidate.categoryId)).get();
const safeCategoryId = category ? candidate.categoryId : 1;
// 3. Verify categoryId still exists, fallback to Uncategorized (id=1)
const category = tx
.select({ id: categories.id })
.from(categories)
.where(eq(categories.id, candidate.categoryId))
.get();
const safeCategoryId = category ? candidate.categoryId : 1;
// 4. Create collection item from candidate data
const newItem = tx
.insert(items)
.values({
name: candidate.name,
weightGrams: candidate.weightGrams,
priceCents: candidate.priceCents,
categoryId: safeCategoryId,
notes: candidate.notes,
productUrl: candidate.productUrl,
imageFilename: candidate.imageFilename,
})
.returning()
.get();
// 4. Create collection item from candidate data
const newItem = tx
.insert(items)
.values({
name: candidate.name,
weightGrams: candidate.weightGrams,
priceCents: candidate.priceCents,
categoryId: safeCategoryId,
notes: candidate.notes,
productUrl: candidate.productUrl,
imageFilename: candidate.imageFilename,
})
.returning()
.get();
// 5. Archive the thread
tx.update(threads)
.set({
status: "resolved",
resolvedCandidateId: candidateId,
updatedAt: new Date(),
})
.where(eq(threads.id, threadId))
.run();
// 5. Archive the thread
tx.update(threads)
.set({
status: "resolved",
resolvedCandidateId: candidateId,
updatedAt: new Date(),
})
.where(eq(threads.id, threadId))
.run();
return { success: true, item: newItem };
});
return { success: true, item: newItem };
});
}