import { dbGlobal } from "drizzle-pkg/lib/db"; import { timelineEvents } from "drizzle-pkg/lib/schema/content"; import { users } from "drizzle-pkg/lib/schema/auth"; import { and, desc, eq } from "drizzle-orm"; import { visibilitySchema, type Visibility } from "#server/constants/visibility"; import { visibilityShareToken } from "#server/utils/share-token"; import { nextIntegerId } from "#server/utils/sqlite-id"; export async function listTimelineForUser(userId: number) { return dbGlobal .select() .from(timelineEvents) .where(eq(timelineEvents.userId, userId)) .orderBy(desc(timelineEvents.occurredOn), desc(timelineEvents.id)); } export async function getTimelineForUser(userId: number, id: number) { const [row] = await dbGlobal .select() .from(timelineEvents) .where(and(eq(timelineEvents.id, id), eq(timelineEvents.userId, userId))) .limit(1); return row ?? null; } export async function createTimelineEvent( userId: number, input: { occurredOn: Date; title: string; bodyMarkdown?: string | null; linkUrl?: string | null; visibility: Visibility; }, ) { const vis = visibilitySchema.parse(input.visibility); const shareToken = visibilityShareToken(vis, null); const id = await nextIntegerId(timelineEvents, timelineEvents.id); await dbGlobal.insert(timelineEvents).values({ id, userId, occurredOn: input.occurredOn, title: input.title.trim(), bodyMarkdown: input.bodyMarkdown?.trim() || undefined, linkUrl: input.linkUrl?.trim() || undefined, visibility: vis, shareToken, }); return getTimelineForUser(userId, id); } export async function updateTimelineEvent( userId: number, id: number, patch: Partial<{ occurredOn: Date; title: string; bodyMarkdown: string | null; linkUrl: string | null; visibility: Visibility; }>, ) { const existing = await getTimelineForUser(userId, id); if (!existing) { return null; } const nextVis = patch.visibility !== undefined ? visibilitySchema.parse(patch.visibility) : (existing.visibility as Visibility); const shareToken = patch.visibility !== undefined ? visibilityShareToken(nextVis, existing.shareToken) : existing.shareToken; await dbGlobal .update(timelineEvents) .set({ ...(patch.occurredOn !== undefined ? { occurredOn: patch.occurredOn } : {}), ...(patch.title !== undefined ? { title: patch.title.trim() } : {}), ...(patch.bodyMarkdown !== undefined ? { bodyMarkdown: patch.bodyMarkdown?.trim() || null } : {}), ...(patch.linkUrl !== undefined ? { linkUrl: patch.linkUrl?.trim() || null } : {}), ...(patch.visibility !== undefined ? { visibility: nextVis, shareToken } : {}), }) .where(and(eq(timelineEvents.id, id), eq(timelineEvents.userId, userId))); return getTimelineForUser(userId, id); } export async function deleteTimelineEvent(userId: number, id: number) { const existing = await getTimelineForUser(userId, id); if (!existing) { return false; } await dbGlobal .delete(timelineEvents) .where(and(eq(timelineEvents.id, id), eq(timelineEvents.userId, userId))); return true; } export async function listPublicTimelineBySlug(publicSlug: string) { const rows = await dbGlobal .select({ ev: timelineEvents }) .from(timelineEvents) .innerJoin(users, eq(timelineEvents.userId, users.id)) .where( and( eq(users.publicSlug, publicSlug), eq(users.status, "active"), eq(timelineEvents.visibility, "public"), ), ) .orderBy(desc(timelineEvents.occurredOn), desc(timelineEvents.id)); return rows.map((r) => r.ev); } export async function getUnlistedTimeline(publicSlug: string, shareToken: string) { const [row] = await dbGlobal .select({ ev: timelineEvents }) .from(timelineEvents) .innerJoin(users, eq(timelineEvents.userId, users.id)) .where( and( eq(users.publicSlug, publicSlug), eq(users.status, "active"), eq(timelineEvents.visibility, "unlisted"), eq(timelineEvents.shareToken, shareToken), ), ) .limit(1); return row?.ev ?? null; }