From b9b443f30ce8ee4cdd23785908b0948cb0d8ec6b Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 18 Apr 2026 21:36:58 +0800 Subject: [PATCH] feat(server/timeline): public preview and paginated list by slug Made-with: Cursor --- server/api/public/profile/[publicSlug].get.ts | 6 +-- server/service/timeline/index.ts | 66 +++++++++++++++++++++------ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/server/api/public/profile/[publicSlug].get.ts b/server/api/public/profile/[publicSlug].get.ts index 2a48564..36a6e08 100644 --- a/server/api/public/profile/[publicSlug].get.ts +++ b/server/api/public/profile/[publicSlug].get.ts @@ -2,7 +2,7 @@ import { dbGlobal } from "drizzle-pkg/lib/db"; import { users } from "drizzle-pkg/lib/schema/auth"; import { and, eq } from "drizzle-orm"; import { getPublicPostsPreviewBySlug } from "#server/service/posts"; -import { listPublicTimelineBySlug } from "#server/service/timeline"; +import { getPublicTimelinePreviewBySlug } from "#server/service/timeline"; import { listPublicRssItemsBySlug } from "#server/service/rss"; import { parseSocialLinksJson } from "#server/service/profile"; @@ -33,7 +33,7 @@ export default defineEventHandler(async (event) => { bio: { markdown: string | null } | null; links: typeof links; posts: Awaited>; - timeline: Awaited>; + timeline: Awaited>; rssItems: Awaited>; } = { user: { @@ -44,7 +44,7 @@ export default defineEventHandler(async (event) => { bio: null, links, posts: await getPublicPostsPreviewBySlug(publicSlug), - timeline: await listPublicTimelineBySlug(publicSlug), + timeline: await getPublicTimelinePreviewBySlug(publicSlug), rssItems: await listPublicRssItemsBySlug(publicSlug), }; diff --git a/server/service/timeline/index.ts b/server/service/timeline/index.ts index 555211b..c8b320d 100644 --- a/server/service/timeline/index.ts +++ b/server/service/timeline/index.ts @@ -1,8 +1,10 @@ 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 { 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"; @@ -100,20 +102,54 @@ export async function deleteTimelineEvent(userId: number, id: number) { 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); +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) {