You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

234 lines
6.5 KiB

import { dbGlobal } from "drizzle-pkg/lib/db";
import { posts } 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";
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,98}[a-z0-9]$|^[a-z0-9]$/;
function assertSlug(slug: string) {
if (!SLUG_RE.test(slug)) {
throw createError({ statusCode: 400, statusMessage: "slug 格式无效(小写字母、数字、短横线,1–100 字符)" });
}
}
export async function listPostsForUser(userId: number) {
return dbGlobal
.select()
.from(posts)
.where(eq(posts.userId, userId))
.orderBy(desc(posts.publishedAt), desc(posts.id));
}
export async function getPostForUser(userId: number, id: number) {
const [row] = await dbGlobal
.select()
.from(posts)
.where(and(eq(posts.id, id), eq(posts.userId, userId)))
.limit(1);
return row ?? null;
}
export async function createPost(
userId: number,
input: {
title: string;
slug: string;
bodyMarkdown: string;
excerpt: string;
coverUrl?: string | null;
tagsJson?: string;
publishedAt?: Date | null;
visibility: Visibility;
},
) {
assertSlug(input.slug);
const vis = visibilitySchema.parse(input.visibility);
const shareToken = visibilityShareToken(vis, null);
const id = await nextIntegerId(posts, posts.id);
await dbGlobal.insert(posts).values({
id,
userId,
title: input.title.trim(),
slug: input.slug.trim(),
bodyMarkdown: input.bodyMarkdown,
excerpt: input.excerpt,
coverUrl: input.coverUrl?.trim() || undefined,
tagsJson: input.tagsJson ?? "[]",
publishedAt: input.publishedAt ?? new Date(),
visibility: vis,
shareToken,
});
return getPostForUser(userId, id);
}
export async function updatePost(
userId: number,
id: number,
patch: Partial<{
title: string;
slug: string;
bodyMarkdown: string;
excerpt: string;
coverUrl: string | null;
tagsJson: string;
publishedAt: Date | null;
visibility: Visibility;
}>,
) {
const existing = await getPostForUser(userId, id);
if (!existing) {
return null;
}
if (patch.slug !== undefined) {
assertSlug(patch.slug);
}
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(posts)
.set({
...(patch.title !== undefined ? { title: patch.title.trim() } : {}),
...(patch.slug !== undefined ? { slug: patch.slug.trim() } : {}),
...(patch.bodyMarkdown !== undefined ? { bodyMarkdown: patch.bodyMarkdown } : {}),
...(patch.excerpt !== undefined ? { excerpt: patch.excerpt } : {}),
...(patch.coverUrl !== undefined ? { coverUrl: patch.coverUrl?.trim() || null } : {}),
...(patch.tagsJson !== undefined ? { tagsJson: patch.tagsJson } : {}),
...(patch.publishedAt !== undefined ? { publishedAt: patch.publishedAt } : {}),
...(patch.visibility !== undefined ? { visibility: nextVis, shareToken } : {}),
})
.where(and(eq(posts.id, id), eq(posts.userId, userId)));
return getPostForUser(userId, id);
}
export async function deletePost(userId: number, id: number) {
const existing = await getPostForUser(userId, id);
if (!existing) {
return false;
}
await dbGlobal.delete(posts).where(and(eq(posts.id, id), eq(posts.userId, userId)));
return true;
}
export async function listPublicPostsBySlug(publicSlug: string) {
return dbGlobal
.select({
title: posts.title,
excerpt: posts.excerpt,
slug: posts.slug,
coverUrl: posts.coverUrl,
publishedAt: posts.publishedAt,
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(
and(
eq(users.publicSlug, publicSlug),
eq(users.status, "active"),
eq(posts.visibility, "public"),
),
)
.orderBy(desc(posts.publishedAt), desc(posts.id));
}
export async function getPublicPostByPublicSlugAndSlug(publicSlug: string, postSlug: string) {
const slug = postSlug.trim();
if (!slug) {
return null;
}
const [row] = await dbGlobal
.select({
title: posts.title,
slug: posts.slug,
excerpt: posts.excerpt,
bodyMarkdown: posts.bodyMarkdown,
coverUrl: posts.coverUrl,
publishedAt: posts.publishedAt,
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(
and(
eq(users.publicSlug, publicSlug),
eq(users.status, "active"),
eq(posts.visibility, "public"),
eq(posts.slug, slug),
),
)
.limit(1);
return row ?? null;
}
export async function getPublicPostCommentContext(
publicSlug: string,
postSlug: string,
): Promise<{ id: number; userId: number } | null> {
const slug = postSlug.trim();
if (!slug) {
return null;
}
const [row] = await dbGlobal
.select({
id: posts.id,
userId: posts.userId,
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(
and(
eq(users.publicSlug, publicSlug),
eq(users.status, "active"),
eq(posts.visibility, "public"),
eq(posts.slug, slug),
),
)
.limit(1);
return row ?? null;
}
export async function getUnlistedPostCommentContext(
publicSlug: string,
shareToken: string,
): Promise<{ id: number; userId: number } | null> {
const [row] = await dbGlobal
.select({
id: posts.id,
userId: posts.userId,
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(
and(
eq(users.publicSlug, publicSlug),
eq(users.status, "active"),
eq(posts.visibility, "unlisted"),
eq(posts.shareToken, shareToken),
),
)
.limit(1);
return row ?? null;
}
export async function getUnlistedPost(publicSlug: string, shareToken: string) {
const [row] = await dbGlobal
.select({ post: posts })
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(
and(
eq(users.publicSlug, publicSlug),
eq(users.status, "active"),
eq(posts.visibility, "unlisted"),
eq(posts.shareToken, shareToken),
),
)
.limit(1);
return row?.post ?? null;
}