server continue

This commit is contained in:
Michael Freno
2025-12-19 23:54:11 -05:00
parent 2e80fbd11e
commit 921863c602
6 changed files with 181 additions and 126 deletions

View File

@@ -1,14 +1,7 @@
import { createTRPCRouter, publicProcedure } from "../utils";
import { ConnectionFactory } from "~/server/utils";
import { withCache } from "~/server/cache";
// Simple in-memory cache for blog posts to reduce DB load
let cachedPosts: {
posts: any[];
tagMap: Record<string, number>;
privilegeLevel: string;
} | null = null;
let cacheTimestamp: number = 0;
import { postQueryInputSchema } from "~/server/api/schemas/blog";
export const blogRouter = createTRPCRouter({
getRecentPosts: publicProcedure.query(async () => {
@@ -44,61 +37,117 @@ export const blogRouter = createTRPCRouter({
});
}),
getPosts: publicProcedure.query(async ({ ctx }) => {
const privilegeLevel = ctx.privilegeLevel;
getPosts: publicProcedure
.input(postQueryInputSchema)
.query(async ({ ctx, input }) => {
const privilegeLevel = ctx.privilegeLevel;
const { filters, sortBy } = input;
// Check if we have fresh cached data (cache duration: 30 seconds)
const now = Date.now();
if (cachedPosts && now - cacheTimestamp < 30000) {
return cachedPosts;
}
// Create cache key based on filters and sort
const cacheKey = `posts-${privilegeLevel}-${filters || "all"}-${sortBy}`;
// Single optimized query using JOINs instead of subqueries and separate queries
let query = `
SELECT
p.id,
p.title,
p.subtitle,
p.body,
p.banner_photo,
p.date,
p.published,
p.category,
p.author_id,
p.reads,
p.attachments,
COUNT(DISTINCT pl.user_id) as total_likes,
COUNT(DISTINCT c.id) as total_comments,
GROUP_CONCAT(t.value) as tags
FROM Post p
LEFT JOIN PostLike pl ON p.id = pl.post_id
LEFT JOIN Comment c ON p.id = c.post_id
LEFT JOIN Tag t ON p.id = t.post_id`;
// Note: We're removing simple cache due to filtering/sorting variations
// Consider implementing a more sophisticated cache strategy if needed
if (privilegeLevel !== "admin") {
query += ` WHERE p.published = TRUE`;
}
query += ` 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 ORDER BY p.date DESC;`;
const conn = ConnectionFactory();
const conn = ConnectionFactory();
const results = await conn.execute(query);
const posts = results.rows;
// Parse filter tags (pipe-separated)
const filterTags = filters ? filters.split("|").filter(Boolean) : [];
// Process tags into a map for the UI
let tagMap: Record<string, number> = {};
posts.forEach((post: any) => {
if (post.tags) {
const postTags = post.tags.split(",");
postTags.forEach((tag: string) => {
tagMap[tag] = (tagMap[tag] || 0) + 1;
});
// Build base query
let query = `
SELECT
p.id,
p.title,
p.subtitle,
p.body,
p.banner_photo,
p.date,
p.published,
p.category,
p.author_id,
p.reads,
p.attachments,
COUNT(DISTINCT pl.user_id) as total_likes,
COUNT(DISTINCT c.id) as total_comments,
GROUP_CONCAT(t.value) as tags
FROM Post p
LEFT JOIN PostLike pl ON p.id = pl.post_id
LEFT JOIN Comment c ON p.id = c.post_id
LEFT JOIN Tag t ON p.id = t.post_id`;
// Build WHERE clause
const whereClauses: string[] = [];
const queryArgs: any[] = [];
// Published filter (if not admin)
if (privilegeLevel !== "admin") {
whereClauses.push("p.published = TRUE");
}
});
// Cache the results
cachedPosts = { posts, tagMap, privilegeLevel };
cacheTimestamp = now;
// Tag filter (if provided)
if (filterTags.length > 0) {
// Use EXISTS subquery for tag filtering
whereClauses.push(`
EXISTS (
SELECT 1 FROM Tag t2
WHERE t2.post_id = p.id
AND t2.value IN (${filterTags.map(() => "?").join(", ")})
)
`);
queryArgs.push(...filterTags);
}
return cachedPosts;
})
// Add WHERE clause if any conditions exist
if (whereClauses.length > 0) {
query += ` WHERE ${whereClauses.join(" AND ")}`;
}
// Add GROUP BY
query += ` 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`;
// Add ORDER BY based on sortBy parameter
switch (sortBy) {
case "newest":
query += ` ORDER BY p.date DESC`;
break;
case "oldest":
query += ` ORDER BY p.date ASC`;
break;
case "most_liked":
query += ` ORDER BY total_likes DESC`;
break;
case "most_read":
query += ` ORDER BY p.reads DESC`;
break;
case "most_comments":
query += ` ORDER BY total_comments DESC`;
break;
default:
query += ` ORDER BY p.date DESC`;
}
query += ";";
// Execute query
const results = await conn.execute({
sql: query,
args: queryArgs
});
const posts = results.rows;
// Process tags into a map for the UI
// Note: This includes ALL tags from filtered results
let tagMap: Record<string, number> = {};
posts.forEach((post: any) => {
if (post.tags) {
const postTags = post.tags.split(",");
postTags.forEach((tag: string) => {
tagMap[tag] = (tagMap[tag] || 0) + 1;
});
}
});
return { posts, tagMap, privilegeLevel };
})
});

View File

@@ -0,0 +1,44 @@
import { z } from "zod";
/**
* Blog Query Schemas
*
* Schemas for filtering and sorting blog posts server-side
*/
/**
* Post sort mode enum
* Defines available sorting options for blog posts
*/
export const postSortModeSchema = z.enum([
"newest",
"oldest",
"most_liked",
"most_read",
"most_comments"
]);
/**
* Post query input schema
* Accepts optional filters (pipe-separated tags) and sort mode
*/
export const postQueryInputSchema = z.object({
/**
* Pipe-separated list of tags to filter by
* e.g., "tech|design|javascript"
* Empty string or undefined means no filter
*/
filters: z.string().optional(),
/**
* Sort mode for posts
* Defaults to "newest" if not specified
*/
sortBy: postSortModeSchema.default("newest")
});
/**
* Type exports for use in components
*/
export type PostSortMode = z.infer<typeof postSortModeSchema>;
export type PostQueryInput = z.infer<typeof postQueryInputSchema>;