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.
170 lines
5.5 KiB
170 lines
5.5 KiB
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;
|
|
}
|
|
|