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
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;
|
|
}
|
|
|