diff --git a/.gitignore b/.gitignore index 9375ad66..33a61908 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ Thumbs.db vite.config.js.timestamp-* vite.config.ts.timestamp-* -minio-data +minio-data \ No newline at end of file diff --git a/src/routes/api/comment/+server.ts b/src/routes/api/comment/+server.ts index 34e95a96..08214a9f 100644 --- a/src/routes/api/comment/+server.ts +++ b/src/routes/api/comment/+server.ts @@ -75,7 +75,7 @@ export const POST: RequestHandler = async ({ return json({ error: 'Invalid reposted lynt ID' }, { status: 400 }); } - let newId = (await db.insert(lynts).values(lyntValues).returning())[0].id || null; + let newId = (await db.insert(lynts).values(lyntValues).returning())[0].id || ''; let [newLynt] = await db .select(lyntObj(userId)) .from(lynts) diff --git a/src/routes/api/comments/+server.ts b/src/routes/api/comments/+server.ts index c2a1af71..735191f8 100644 --- a/src/routes/api/comments/+server.ts +++ b/src/routes/api/comments/+server.ts @@ -6,6 +6,7 @@ import { lynts, likes, users, followers } from '@/server/schema'; import { sql, desc, and, eq, not, exists } from 'drizzle-orm'; import { lyntObj } from '../util'; +import { userRepliesQuery } from './userReplies'; export const GET: RequestHandler = async ({ url, cookies }) => { const lyntId = url.searchParams.get('id'); @@ -29,26 +30,10 @@ export const GET: RequestHandler = async ({ url, cookies }) => { const userId = jwtPayload.userId; // First, get the comments that the user has replied to - const userReplies = await db - .select(lyntObj(userId)) - .from(lynts) - .leftJoin(likes, eq(likes.lynt_id, lynts.id)) - .leftJoin(users, eq(lynts.user_id, users.id)) - .where( - and( - eq(lynts.parent, lyntId), - eq(lynts.reposted, false), - exists( - db - .select() - .from(lynts) - .where(and(eq(lynts.parent, lynts.id), eq(lynts.user_id, userId))) - ) - ) - ) - .groupBy(lynts.id, users.id) - .orderBy(desc(lynts.created_at)) - .execute(); + const userReplies = await userRepliesQuery.execute({ + user_id: userId, + lynt_id: lyntId + }); // Then, get the most liked comments let mostLikedComments: { @@ -90,7 +75,6 @@ export const GET: RequestHandler = async ({ url, cookies }) => { .where(and(eq(lynts.parent, lyntId), eq(lynts.reposted, false), notInClause)) .groupBy(lynts.id, users.id) .orderBy(desc(sql`count(distinct ${likes.user_id})`), desc(lynts.created_at)) - // .limit(50 - userReplies.length) .execute(); } const comments = [...userReplies, ...mostLikedComments]; diff --git a/src/routes/api/comments/userReplies.ts b/src/routes/api/comments/userReplies.ts new file mode 100644 index 00000000..9250d2a2 --- /dev/null +++ b/src/routes/api/comments/userReplies.ts @@ -0,0 +1,31 @@ +import { db } from '@/server/db'; +import { lynts, likes, users } from '@/server/schema'; +import { eq, and, desc, sql, exists } from 'drizzle-orm'; +import { lyntObj } from '../util'; + +// Prepared statement: user_replies +// Parameters: +const userId = sql.placeholder('user_id'); +const lyntId = sql.placeholder('lynt_id'); + +// Query: +export const userRepliesQuery = db + .select(lyntObj()) + .from(lynts) + .leftJoin(likes, eq(likes.lynt_id, lynts.id)) + .leftJoin(users, eq(lynts.user_id, users.id)) + .where( + and( + eq(lynts.parent, lyntId), + eq(lynts.reposted, false), + exists( + db + .select() + .from(lynts) + .where(and(eq(lynts.parent, lynts.id), eq(lynts.user_id, userId))) + ) + ) + ) + .groupBy(lynts.id, users.id) + .orderBy(desc(lynts.created_at)) + .prepare('user_replies'); diff --git a/src/routes/api/feed/following.ts b/src/routes/api/feed/following.ts index fa1c8903..66799d9c 100644 --- a/src/routes/api/feed/following.ts +++ b/src/routes/api/feed/following.ts @@ -1,29 +1,36 @@ -import { json } from '@sveltejs/kit'; import { db } from '@/server/db'; import { lynts, likes, users, followers, history } from '@/server/schema'; import { sql, desc, and, eq, not, exists, or, isNull } from 'drizzle-orm'; import { lyntObj } from '../util'; -export async function followingFeed(userId: string) { - const feed = await db - .select(lyntObj(userId)) - .from(lynts) - .leftJoin(likes, eq(likes.lynt_id, lynts.id)) - .leftJoin(users, eq(lynts.user_id, users.id)) - .where( - and( - or(isNull(lynts.parent), eq(lynts.reposted, true)), - exists( - db - .select() - .from(followers) - .where(and(eq(followers.user_id, lynts.user_id), eq(followers.follower_id, userId))) - ) +// Prepared statement: following_feed +// Parameters: +const userId = sql.placeholder('user_id'); + +// Query: +const followingFeedQuery = db + .select(lyntObj()) + .from(lynts) + .leftJoin(likes, eq(likes.lynt_id, lynts.id)) + .leftJoin(users, eq(lynts.user_id, users.id)) + .where( + and( + or(isNull(lynts.parent), eq(lynts.reposted, true)), + exists( + db + .select() + .from(followers) + .where(and(eq(followers.user_id, lynts.user_id), eq(followers.follower_id, userId))) ) ) - .groupBy(lynts.id, users.id) - .orderBy(desc(lynts.created_at)) - .limit(100); + ) + .groupBy(lynts.id, users.id) + .orderBy(desc(lynts.created_at)) + .limit(100) + .prepare('following_feed'); - return feed; +export async function followingFeed(userId: string) { + return await followingFeedQuery.execute({ + user_id: userId + }); } diff --git a/src/routes/api/feed/handle.ts b/src/routes/api/feed/handle.ts index bbf0eabf..333bc725 100644 --- a/src/routes/api/feed/handle.ts +++ b/src/routes/api/feed/handle.ts @@ -5,26 +5,32 @@ import { lynts, likes, users } from '@/server/schema'; import { sql, desc, and, eq, or, isNull } from 'drizzle-orm'; import { lyntObj } from '../util'; -export async function handleFeed(handleUserId: string, userId: string) { - const totalLyntsResult = await db - .select({ count: sql`count(*)` }) - .from(lynts) - .where(eq(lynts.user_id, handleUserId)); +// Prepared statement: handle_feed +// Parameters: +// - user_id from lyntObj +// - handle_user_id +const handleUserId = sql.placeholder('handle_user_id'); - const feed = await db - .select(lyntObj(userId)) - .from(lynts) - .leftJoin(likes, eq(likes.lynt_id, lynts.id)) - .leftJoin(users, eq(lynts.user_id, users.id)) - .where( - and( - eq(lynts.user_id, handleUserId), - or(and(eq(lynts.reposted, false), isNull(lynts.parent)), eq(lynts.reposted, true)) - ) +// Query: +const handleFeedQuery = db + .select(lyntObj()) + .from(lynts) + .leftJoin(likes, eq(likes.lynt_id, lynts.id)) + .leftJoin(users, eq(lynts.user_id, users.id)) + .where( + and( + eq(lynts.user_id, handleUserId), + or(and(eq(lynts.reposted, false), isNull(lynts.parent)), eq(lynts.reposted, true)) ) - .groupBy(lynts.id, users.id) - .orderBy(desc(lynts.created_at)) - .limit(50); + ) + .groupBy(lynts.id, users.id) + .orderBy(desc(lynts.created_at)) + .limit(50) + .prepare('handle_feed'); - return feed; +export async function handleFeed(handleUserId: string, userId: string) { + return await handleFeedQuery.execute({ + handle_user_id: handleUserId, + user_id: userId + }); } diff --git a/src/routes/api/feed/liked.ts b/src/routes/api/feed/liked.ts index 118c1763..9ad994bb 100644 --- a/src/routes/api/feed/liked.ts +++ b/src/routes/api/feed/liked.ts @@ -1,20 +1,30 @@ import { db } from '@/server/db'; import { lynts, likes, users } from '@/server/schema'; -import { desc, and, eq, exists, or, isNull } from 'drizzle-orm'; +import { desc, and, eq, exists, or, isNull, sql } from 'drizzle-orm'; import { lyntObj } from '../util'; +// Prepared statement: liked_feed +// Parameters: +const userId = sql.placeholder('user_id'); + +// Query: +const likedFeedQuery = db + .select({ + ...lyntObj(), + likedAt: likes.liked_at + }) + .from(likes) + .innerJoin(lynts, eq(likes.lynt_id, lynts.id)) + .innerJoin(users, eq(lynts.user_id, users.id)) + .where(eq(likes.user_id, userId)) + .orderBy(desc(likes.liked_at)) + .limit(100) + .prepare('liked_feed'); + export async function likedFeed(userId: string) { - const feed = await db - .select({ - ...lyntObj(userId), - likedAt: likes.liked_at - }) - .from(likes) - .innerJoin(lynts, eq(likes.lynt_id, lynts.id)) - .innerJoin(users, eq(lynts.user_id, users.id)) - .where(eq(likes.user_id, userId)) - .orderBy(desc(likes.liked_at)) - .limit(100); + const feed = await likedFeedQuery.execute({ + user_id: userId + }); return feed; -} \ No newline at end of file +} diff --git a/src/routes/api/feed/main.ts b/src/routes/api/feed/main.ts index feb2eaaf..c4a300d2 100644 --- a/src/routes/api/feed/main.ts +++ b/src/routes/api/feed/main.ts @@ -1,66 +1,80 @@ -import { json } from '@sveltejs/kit'; import { db } from '@/server/db'; import { lynts, likes, users, followers, history } from '@/server/schema'; import { sql, desc, and, eq, exists, or, isNull, not, inArray, asc } from 'drizzle-orm'; import { lyntObj } from '../util'; -export async function mainFeed(userId: string, limit = 20, excludePosts: string[] = []) { - const followedUsers = db - .select({ followedId: followers.follower_id }) - .from(followers) - .where(eq(followers.user_id, userId)); - - const likeCounts = db - .select({ - lyntId: likes.lynt_id, - likeCount: sql`count(*)`.as('like_count') - }) - .from(likes) - .groupBy(likes.lynt_id) - .as('like_counts'); +// Prepared query: main_feed +// Parameters: +const userId = sql.placeholder('user_id'); +const limit = sql.placeholder('limit'); +const excludePosts = sql.placeholder('exclude_posts'); - let whereConditions = and( - eq(users.banned, false), - or(isNull(lynts.parent), eq(lynts.reposted, true)), - sql`${lynts.created_at} > now() - interval '30 days'` - ); +// Subqueries: +const followedUsers = db + .select({ followedId: followers.follower_id }) + .from(followers) + .where(eq(followers.user_id, userId)); - if (excludePosts.length > 0) { - whereConditions = and(whereConditions, not(inArray(lynts.id, excludePosts))); - } +const likeCounts = db + .select({ + lyntId: likes.lynt_id, + likeCount: sql`count(*)`.as('like_count') + }) + .from(likes) + .groupBy(likes.lynt_id) + .as('like_counts'); - const feed = await db - .select({ - ...lyntObj(userId), - isFollowed: inArray(lynts.user_id, followedUsers), - likeCount: sql`coalesce(${likeCounts.likeCount}, 0)`, - isViewed: exists( - db - .select() - .from(history) - .where(and(eq(history.lynt_id, lynts.id), eq(history.user_id, userId))) - ), - viewedAt: sql`( +// Query: +const feedQuery = db + .select({ + ...lyntObj(), + isFollowed: inArray(lynts.user_id, followedUsers), + likeCount: sql`coalesce(${likeCounts.likeCount}, 0)`, + isViewed: exists( + db + .select() + .from(history) + .where(and(eq(history.lynt_id, lynts.id), eq(history.user_id, userId))) + ), + viewedAt: sql`( SELECT ${history.createdAt} FROM ${history} WHERE ${history.lynt_id} = ${lynts.id} AND ${history.user_id} = ${userId} )` - }) - .from(lynts) - .leftJoin(users, eq(lynts.user_id, users.id)) - .leftJoin(likeCounts, eq(lynts.id, likeCounts.lyntId)) - .leftJoin(history, and(eq(history.lynt_id, lynts.id), eq(history.user_id, userId))) - .where(whereConditions) - .orderBy( - desc(sql`CASE WHEN ${history.id} IS NULL THEN 1 ELSE 0 END`), - desc(lynts.created_at), - desc(sql`CASE WHEN ${users.handle} = 'facedev' THEN 1 ELSE 0 END`), - desc(sql`CASE WHEN ${lynts.user_id} IN (${followedUsers}) THEN 1 ELSE 0 END`), - desc(sql`COALESCE(${likeCounts.likeCount}, 0)`), - desc(sql`CASE WHEN ${lynts.created_at} > now() - interval '24 hours' THEN 1 ELSE 0 END`) + }) + .from(lynts) + .leftJoin(users, eq(lynts.user_id, users.id)) + .leftJoin(likeCounts, eq(lynts.id, likeCounts.lyntId)) + .leftJoin(history, and(eq(history.lynt_id, lynts.id), eq(history.user_id, userId))) + .where( + and( + eq(users.banned, false), + or(isNull(lynts.parent), eq(lynts.reposted, true)), + sql`${lynts.created_at} > now() - interval '30 days'`, + + // If you try to use a placeholder and inArray, you will get a syntax error + // so we can use this hack + not(eq(lynts.id, sql`any(${excludePosts})`)) ) - .limit(limit); + ) + .orderBy( + desc(sql`CASE WHEN ${history.id} IS NULL THEN 1 ELSE 0 END`), + desc(lynts.created_at), + desc(sql`CASE WHEN ${users.handle} = 'facedev' THEN 1 ELSE 0 END`), + desc(sql`CASE WHEN ${lynts.user_id} IN (${followedUsers}) THEN 1 ELSE 0 END`), + desc(sql`COALESCE(${likeCounts.likeCount}, 0)`), + desc(sql`CASE WHEN ${lynts.created_at} > now() - interval '24 hours' THEN 1 ELSE 0 END`) + ) + .limit(limit) + .prepare('main_feed'); + +export async function mainFeed(userId: string, limit = 20, excludePosts: string[] = []) { + const feed = await feedQuery.execute({ + user_id: userId, + exclude_posts: excludePosts, + limit: limit + }); return feed; } diff --git a/src/routes/api/feed/new.ts b/src/routes/api/feed/new.ts index d51614e2..e44b0bb9 100644 --- a/src/routes/api/feed/new.ts +++ b/src/routes/api/feed/new.ts @@ -1,19 +1,27 @@ import { json } from '@sveltejs/kit'; import { db } from '@/server/db'; import { lynts, likes, users, history } from '@/server/schema'; -import { desc, and, eq, not, exists, or, isNull } from 'drizzle-orm'; +import { desc, and, eq, not, exists, or, isNull, sql } from 'drizzle-orm'; import { lyntObj } from '../util'; +// Prepared statement: new_feed +// Parameters: +// - user_id from lyntObj +const newFeedQuery = db + .select(lyntObj()) + .from(lynts) + .leftJoin(likes, eq(likes.lynt_id, lynts.id)) + .leftJoin(users, eq(lynts.user_id, users.id)) + .where(and(or(isNull(lynts.parent), eq(lynts.reposted, true)))) + .groupBy(lynts.id, users.id) + .orderBy(desc(lynts.created_at)) + .limit(50) + .prepare('new_feed'); + export async function newFeed(userId: string) { - const feed = await db - .select(lyntObj(userId)) - .from(lynts) - .leftJoin(likes, eq(likes.lynt_id, lynts.id)) - .leftJoin(users, eq(lynts.user_id, users.id)) - .where(and(or(isNull(lynts.parent), eq(lynts.reposted, true)))) - .groupBy(lynts.id, users.id) - .orderBy(desc(lynts.created_at)) - .limit(50); + const feed = await newFeedQuery.execute({ + user_id: userId + }); return feed; } diff --git a/src/routes/api/util.ts b/src/routes/api/util.ts index 5d3994c0..1baaacec 100644 --- a/src/routes/api/util.ts +++ b/src/routes/api/util.ts @@ -1,17 +1,22 @@ import { db } from '@/server/db'; import { lynts, likes, followers, users, notifications, history } from '@/server/schema'; -import { and, eq, inArray, sql } from 'drizzle-orm'; +import { and, eq, inArray, Placeholder, sql } from 'drizzle-orm'; import sharp from 'sharp'; -export const lyntObj = (userId: string) => { +/** + * If no user_id is provided, the SQL placeholder `user_id` is used. + */ +export const lyntObj = ( + userId: string | Placeholder<'user_id', string> = sql.placeholder('user_id') +) => { return { id: lynts.id, content: lynts.content, userId: lynts.user_id, createdAt: lynts.created_at, views: sql`( - SELECT COUNT(*) - FROM ${history} + SELECT COUNT(*) + FROM ${history} WHERE ${history.lynt_id} = ${lynts.id} )`.as('views'), reposted: lynts.reposted,