diff --git a/src/server/api/routers/blog.ts b/src/server/api/routers/blog.ts index 0a2756a..5aa195a 100644 --- a/src/server/api/routers/blog.ts +++ b/src/server/api/routers/blog.ts @@ -1,10 +1,12 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { ConnectionFactory } from "~/server/utils"; -import { withCache } from "~/server/cache"; +import { withCacheAndStale } from "~/server/cache"; + +const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours export const blogRouter = createTRPCRouter({ getRecentPosts: publicProcedure.query(async () => { - return withCache("recent-posts", 10 * 60 * 1000, async () => { + return withCacheAndStale("blog-recent-posts", BLOG_CACHE_TTL, async () => { // Get database connection const conn = ConnectionFactory(); @@ -39,11 +41,14 @@ export const blogRouter = createTRPCRouter({ getPosts: publicProcedure.query(async ({ ctx }) => { const privilegeLevel = ctx.privilegeLevel; - return withCache(`posts-${privilegeLevel}`, 5 * 60 * 1000, async () => { - const conn = ConnectionFactory(); + return withCacheAndStale( + `blog-posts-${privilegeLevel}`, + BLOG_CACHE_TTL, + async () => { + const conn = ConnectionFactory(); - // Fetch all posts with aggregated data - let postsQuery = ` + // Fetch all posts with aggregated data + let postsQuery = ` SELECT p.id, p.title, @@ -63,17 +68,17 @@ export const blogRouter = createTRPCRouter({ LEFT JOIN Comment c ON p.id = c.post_id `; - if (privilegeLevel !== "admin") { - postsQuery += ` WHERE p.published = TRUE`; - } + if (privilegeLevel !== "admin") { + postsQuery += ` WHERE p.published = TRUE`; + } - postsQuery += ` GROUP BY p.id, p.title, p.subtitle, p.body, p.banner_photo, p.date, p.published, p.category, p.author_id, p.reads, p.attachments`; - postsQuery += ` ORDER BY p.date ASC;`; + postsQuery += ` GROUP BY p.id, p.title, p.subtitle, p.body, p.banner_photo, p.date, p.published, p.category, p.author_id, p.reads, p.attachments`; + postsQuery += ` ORDER BY p.date ASC;`; - const postsResult = await conn.execute(postsQuery); - const posts = postsResult.rows; + const postsResult = await conn.execute(postsQuery); + const posts = postsResult.rows; - const tagsQuery = ` + const tagsQuery = ` SELECT t.value, t.post_id FROM Tag t JOIN Post p ON t.post_id = p.id @@ -81,16 +86,17 @@ export const blogRouter = createTRPCRouter({ ORDER BY t.value ASC `; - const tagsResult = await conn.execute(tagsQuery); - const tags = tagsResult.rows; + const tagsResult = await conn.execute(tagsQuery); + const tags = tagsResult.rows; - const tagMap: Record = {}; - tags.forEach((tag: any) => { - const key = `${tag.value}`; - tagMap[key] = (tagMap[key] || 0) + 1; - }); + const tagMap: Record = {}; + tags.forEach((tag: any) => { + const key = `${tag.value}`; + tagMap[key] = (tagMap[key] || 0) + 1; + }); - return { posts, tags, tagMap, privilegeLevel }; - }); + return { posts, tags, tagMap, privilegeLevel }; + } + ); }) }); diff --git a/src/server/api/routers/database.ts b/src/server/api/routers/database.ts index daab571..b245439 100644 --- a/src/server/api/routers/database.ts +++ b/src/server/api/routers/database.ts @@ -7,6 +7,9 @@ import { z } from "zod"; import { ConnectionFactory } from "~/server/utils"; import { TRPCError } from "@trpc/server"; import { env } from "~/env/server"; +import { cache, withCacheAndStale } from "~/server/cache"; + +const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours export const databaseRouter = createTRPCRouter({ // ============================================================ @@ -290,40 +293,46 @@ export const databaseRouter = createTRPCRouter({ }) ) .query(async ({ input }) => { - try { - const conn = ConnectionFactory(); - // Single query with JOIN to get post and tags in one go - const query = ` - SELECT p.*, t.value as tag_value - FROM Post p - LEFT JOIN Tag t ON p.id = t.post_id - WHERE p.id = ? - `; - const results = await conn.execute({ - sql: query, - args: [input.id] - }); + return withCacheAndStale( + `blog-post-id-${input.id}`, + BLOG_CACHE_TTL, + async () => { + try { + const conn = ConnectionFactory(); + // Single query with JOIN to get post and tags in one go + const query = ` + SELECT p.*, t.value as tag_value + FROM Post p + LEFT JOIN Tag t ON p.id = t.post_id + WHERE p.id = ? + `; + const results = await conn.execute({ + sql: query, + args: [input.id] + }); - if (results.rows[0]) { - // Group tags by post ID - const post = results.rows[0]; - const tags = results.rows - .filter((row) => row.tag_value) - .map((row) => row.tag_value); + if (results.rows[0]) { + // Group tags by post ID + const post = results.rows[0]; + const tags = results.rows + .filter((row) => row.tag_value) + .map((row) => row.tag_value); - return { - post, - tags - }; - } else { - return { post: null, tags: [] }; + return { + post, + tags + }; + } else { + return { post: null, tags: [] }; + } + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch post by ID" + }); + } } - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch post by ID" - }); - } + ); }), getPostByTitle: publicProcedure @@ -334,47 +343,53 @@ export const databaseRouter = createTRPCRouter({ }) ) .query(async ({ input, ctx }) => { - try { - const conn = ConnectionFactory(); + return withCacheAndStale( + `blog-post-title-${input.title}`, + BLOG_CACHE_TTL, + async () => { + try { + const conn = ConnectionFactory(); - // Get post by title with JOINs to get all related data in one query - const postQuery = ` - SELECT - p.*, - COUNT(DISTINCT c.id) as comment_count, - COUNT(DISTINCT pl.user_id) as like_count, - GROUP_CONCAT(t.value) as tags - FROM Post p - LEFT JOIN Comment c ON p.id = c.post_id - LEFT JOIN PostLike pl ON p.id = pl.post_id - LEFT JOIN Tag t ON p.id = t.post_id - WHERE p.title = ? AND p.category = ? AND p.published = ? - GROUP BY p.id - `; - const postResults = await conn.execute({ - sql: postQuery, - args: [input.title, input.category, true] - }); + // Get post by title with JOINs to get all related data in one query + const postQuery = ` + SELECT + p.*, + COUNT(DISTINCT c.id) as comment_count, + COUNT(DISTINCT pl.user_id) as like_count, + GROUP_CONCAT(t.value) as tags + FROM Post p + LEFT JOIN Comment c ON p.id = c.post_id + LEFT JOIN PostLike pl ON p.id = pl.post_id + LEFT JOIN Tag t ON p.id = t.post_id + WHERE p.title = ? AND p.category = ? AND p.published = ? + GROUP BY p.id + `; + const postResults = await conn.execute({ + sql: postQuery, + args: [input.title, input.category, true] + }); - if (!postResults.rows[0]) { - return null; + if (!postResults.rows[0]) { + return null; + } + + const postRow = postResults.rows[0]; + + // Return structured data with proper formatting + return { + post: postRow, + comments: [], // Comments are not included in this optimized query - would need separate call if needed + likes: [], // Likes are not included in this optimized query - would need separate call if needed + tags: postRow.tags ? postRow.tags.split(",") : [] + }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch post by title" + }); + } } - - const postRow = postResults.rows[0]; - - // Return structured data with proper formatting - return { - post: postRow, - comments: [], // Comments are not included in this optimized query - would need separate call if needed - likes: [], // Likes are not included in this optimized query - would need separate call if needed - tags: postRow.tags ? postRow.tags.split(",") : [] - }; - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch post by title" - }); - } + ); }), createPost: publicProcedure @@ -422,6 +437,9 @@ export const databaseRouter = createTRPCRouter({ await conn.execute(tagQuery); } + // Invalidate blog cache + cache.deleteByPrefix("blog-"); + return { data: results.lastInsertRowid }; } catch (error) { console.error(error); @@ -509,6 +527,9 @@ export const databaseRouter = createTRPCRouter({ await conn.execute(tagQuery); } + // Invalidate blog cache + cache.deleteByPrefix("blog-"); + return { data: results.lastInsertRowid }; } catch (error) { console.error(error); @@ -549,6 +570,9 @@ export const databaseRouter = createTRPCRouter({ args: [input.id] }); + // Invalidate blog cache + cache.deleteByPrefix("blog-"); + return { success: true }; } catch (error) { console.error(error); diff --git a/src/server/cache.ts b/src/server/cache.ts index f72e1a2..be80ba8 100644 --- a/src/server/cache.ts +++ b/src/server/cache.ts @@ -48,6 +48,17 @@ class SimpleCache { delete(key: string): void { this.cache.delete(key); } + + /** + * Delete all keys starting with a prefix + */ + deleteByPrefix(prefix: string): void { + for (const key of this.cache.keys()) { + if (key.startsWith(prefix)) { + this.cache.delete(key); + } + } + } } export const cache = new SimpleCache();