Browse Source

feat(server/posts): public preview and paginated list by slug

Made-with: Cursor
main
npmrun 8 hours ago
parent
commit
746e89ec63
  1. 6
      server/api/public/profile/[publicSlug].get.ts
  2. 102
      server/service/posts/index.ts

6
server/api/public/profile/[publicSlug].get.ts

@ -1,7 +1,7 @@
import { dbGlobal } from "drizzle-pkg/lib/db"; import { dbGlobal } from "drizzle-pkg/lib/db";
import { users } from "drizzle-pkg/lib/schema/auth"; import { users } from "drizzle-pkg/lib/schema/auth";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { listPublicPostsBySlug } from "#server/service/posts"; import { getPublicPostsPreviewBySlug } from "#server/service/posts";
import { listPublicTimelineBySlug } from "#server/service/timeline"; import { listPublicTimelineBySlug } from "#server/service/timeline";
import { listPublicRssItemsBySlug } from "#server/service/rss"; import { listPublicRssItemsBySlug } from "#server/service/rss";
import { parseSocialLinksJson } from "#server/service/profile"; import { parseSocialLinksJson } from "#server/service/profile";
@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => {
}; };
bio: { markdown: string | null } | null; bio: { markdown: string | null } | null;
links: typeof links; links: typeof links;
posts: Awaited<ReturnType<typeof listPublicPostsBySlug>>; posts: Awaited<ReturnType<typeof getPublicPostsPreviewBySlug>>;
timeline: Awaited<ReturnType<typeof listPublicTimelineBySlug>>; timeline: Awaited<ReturnType<typeof listPublicTimelineBySlug>>;
rssItems: Awaited<ReturnType<typeof listPublicRssItemsBySlug>>; rssItems: Awaited<ReturnType<typeof listPublicRssItemsBySlug>>;
} = { } = {
@ -43,7 +43,7 @@ export default defineEventHandler(async (event) => {
}, },
bio: null, bio: null,
links, links,
posts: await listPublicPostsBySlug(publicSlug), posts: await getPublicPostsPreviewBySlug(publicSlug),
timeline: await listPublicTimelineBySlug(publicSlug), timeline: await listPublicTimelineBySlug(publicSlug),
rssItems: await listPublicRssItemsBySlug(publicSlug), rssItems: await listPublicRssItemsBySlug(publicSlug),
}; };

102
server/service/posts/index.ts

@ -2,8 +2,10 @@ import { dbGlobal } from "drizzle-pkg/lib/db";
import { postMediaRefs, posts } from "drizzle-pkg/lib/schema/content"; import { postMediaRefs, posts } from "drizzle-pkg/lib/schema/content";
import { reconcileAssetTimestampsAfterRefChange, syncPostMediaRefs } from "#server/service/media"; import { reconcileAssetTimestampsAfterRefChange, syncPostMediaRefs } from "#server/service/media";
import { users } from "drizzle-pkg/lib/schema/auth"; 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 { visibilitySchema, type Visibility } from "#server/constants/visibility";
import { normalizePublicListPage } from "#server/utils/public-pagination";
import { visibilityShareToken } from "#server/utils/share-token"; import { visibilityShareToken } from "#server/utils/share-token";
import { nextIntegerId } from "#server/utils/sqlite-id"; import { nextIntegerId } from "#server/utils/sqlite-id";
@ -128,25 +130,85 @@ export async function deletePost(userId: number, id: number) {
return true; return true;
} }
export async function listPublicPostsBySlug(publicSlug: string) { const publicPostsListSelect = {
return dbGlobal title: posts.title,
.select({ excerpt: posts.excerpt,
title: posts.title, slug: posts.slug,
excerpt: posts.excerpt, coverUrl: posts.coverUrl,
slug: posts.slug, publishedAt: posts.publishedAt,
coverUrl: posts.coverUrl, } as const;
publishedAt: posts.publishedAt,
}) function publicPostsListWhere(publicSlug: string) {
.from(posts) return and(
.innerJoin(users, eq(posts.userId, users.id)) eq(users.publicSlug, publicSlug),
.where( eq(users.status, "active"),
and( eq(posts.visibility, "public"),
eq(users.publicSlug, publicSlug), );
eq(users.status, "active"), }
eq(posts.visibility, "public"),
), export async function getPublicPostsPreviewBySlug(publicSlug: string): Promise<{
) items: Array<{
.orderBy(desc(posts.publishedAt), desc(posts.id)); title: string;
excerpt: string;
slug: string;
coverUrl: string | null;
publishedAt: Date | null;
}>;
total: number;
}> {
const whereClause = publicPostsListWhere(publicSlug);
const [countRows, items] = await Promise.all([
dbGlobal
.select({ total: count() })
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(whereClause),
dbGlobal
.select(publicPostsListSelect)
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(whereClause)
.orderBy(desc(posts.publishedAt), desc(posts.id))
.limit(PUBLIC_PREVIEW_LIMIT),
]);
return { items, total: countRows[0]?.total ?? 0 };
}
export async function getPublicPostsPageBySlug(
publicSlug: string,
pageRaw: unknown,
): Promise<{
items: Array<{
title: string;
excerpt: string;
slug: string;
coverUrl: string | null;
publishedAt: Date | null;
}>;
total: number;
page: number;
pageSize: number;
}> {
const page = normalizePublicListPage(pageRaw);
const pageSize = PUBLIC_LIST_PAGE_SIZE;
const offset = (page - 1) * pageSize;
const whereClause = publicPostsListWhere(publicSlug);
const [countRows, items] = await Promise.all([
dbGlobal
.select({ total: count() })
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(whereClause),
dbGlobal
.select(publicPostsListSelect)
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(whereClause)
.orderBy(desc(posts.publishedAt), desc(posts.id))
.limit(pageSize)
.offset(offset),
]);
return { items, total: countRows[0]?.total ?? 0, page, pageSize };
} }
export async function getPublicPostByPublicSlugAndSlug(publicSlug: string, postSlug: string) { export async function getPublicPostByPublicSlugAndSlug(publicSlug: string, postSlug: string) {

Loading…
Cancel
Save