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, count, desc, eq } from "drizzle-orm"; import { PUBLIC_LIST_PAGE_SIZE, PUBLIC_PREVIEW_LIMIT } from "#server/constants/public-profile-lists"; import { visibilitySchema, type Visibility } from "#server/constants/visibility"; import { normalizePublicListPage } from "#server/utils/public-pagination"; 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; } function publicTimelineListWhere(publicSlug: string) { return and( eq(users.publicSlug, publicSlug), eq(users.status, "active"), eq(timelineEvents.visibility, "public"), ); } export async function getPublicTimelinePreviewBySlug(publicSlug: string) { const whereClause = publicTimelineListWhere(publicSlug); const [countRows, rows] = await Promise.all([ dbGlobal .select({ total: count() }) .from(timelineEvents) .innerJoin(users, eq(timelineEvents.userId, users.id)) .where(whereClause), dbGlobal .select({ ev: timelineEvents }) .from(timelineEvents) .innerJoin(users, eq(timelineEvents.userId, users.id)) .where(whereClause) .orderBy(desc(timelineEvents.occurredOn), desc(timelineEvents.id)) .limit(PUBLIC_PREVIEW_LIMIT), ]); return { items: rows.map((r) => r.ev), total: countRows[0]?.total ?? 0 }; } export async function getPublicTimelinePageBySlug(publicSlug: string, pageRaw: unknown) { const page = normalizePublicListPage(pageRaw); const pageSize = PUBLIC_LIST_PAGE_SIZE; const offset = (page - 1) * pageSize; const whereClause = publicTimelineListWhere(publicSlug); const [countRows, rows] = await Promise.all([ dbGlobal .select({ total: count() }) .from(timelineEvents) .innerJoin(users, eq(timelineEvents.userId, users.id)) .where(whereClause), dbGlobal .select({ ev: timelineEvents }) .from(timelineEvents) .innerJoin(users, eq(timelineEvents.userId, users.id)) .where(whereClause) .orderBy(desc(timelineEvents.occurredOn), desc(timelineEvents.id)) .limit(pageSize) .offset(offset), ]); return { items: rows.map((r) => r.ev), total: countRows[0]?.total ?? 0, page, pageSize }; } 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; }