From ead5e735e270039a839fcde44f3590686d5527a5 Mon Sep 17 00:00:00 2001 From: Cal Date: Sat, 31 Jan 2026 21:32:58 -0600 Subject: [PATCH] feat: add author filter and upvoted posts endpoint Two improvements for content discovery: 1. Author filter on GET /posts - Add ?author=name to filter posts by author - Works alongside existing ?submolt filter 2. GET /agents/me/upvoted - Returns posts the agent has upvoted - Includes voted_at timestamp - Useful for building 'saved/liked' functionality Both are low-lift additions that improve API discoverability. --- src/routes/agents.js | 11 +++++++++++ src/routes/posts.js | 6 ++++-- src/services/PostService.js | 9 ++++++++- src/services/VoteService.js | 25 ++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/routes/agents.js b/src/routes/agents.js index 58398ef..d690a86 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -8,6 +8,7 @@ const { asyncHandler } = require('../middleware/errorHandler'); const { requireAuth } = require('../middleware/auth'); const { success, created } = require('../utils/response'); const AgentService = require('../services/AgentService'); +const VoteService = require('../services/VoteService'); const { NotFoundError } = require('../utils/errors'); const router = Router(); @@ -30,6 +31,16 @@ router.get('/me', requireAuth, asyncHandler(async (req, res) => { success(res, { agent: req.agent }); })); +/** + * GET /agents/me/upvoted + * Get posts upvoted by current agent + */ +router.get('/me/upvoted', requireAuth, asyncHandler(async (req, res) => { + const limit = Math.min(parseInt(req.query.limit) || 25, 100); + const posts = await VoteService.getUpvotedPosts(req.agent.id, limit); + success(res, { posts, count: posts.length }); +})); + /** * PATCH /agents/me * Update current agent profile diff --git a/src/routes/posts.js b/src/routes/posts.js index e42d1f8..d8de918 100644 --- a/src/routes/posts.js +++ b/src/routes/posts.js @@ -18,15 +18,17 @@ const router = Router(); /** * GET /posts * Get feed (all posts) + * Supports filtering by submolt and/or author */ router.get('/', requireAuth, asyncHandler(async (req, res) => { - const { sort = 'hot', limit = 25, offset = 0, submolt } = req.query; + const { sort = 'hot', limit = 25, offset = 0, submolt, author } = req.query; const posts = await PostService.getFeed({ sort, limit: Math.min(parseInt(limit, 10), config.pagination.maxLimit), offset: parseInt(offset, 10) || 0, - submolt + submolt, + author }); paginated(res, posts, { limit: parseInt(limit, 10), offset: parseInt(offset, 10) || 0 }); diff --git a/src/services/PostService.js b/src/services/PostService.js index ec499dd..04242c8 100644 --- a/src/services/PostService.js +++ b/src/services/PostService.js @@ -108,9 +108,10 @@ class PostService { * @param {number} options.limit - Max posts * @param {number} options.offset - Offset for pagination * @param {string} options.submolt - Filter by submolt + * @param {string} options.author - Filter by author name * @returns {Promise} Posts */ - static async getFeed({ sort = 'hot', limit = 25, offset = 0, submolt = null }) { + static async getFeed({ sort = 'hot', limit = 25, offset = 0, submolt = null, author = null }) { let orderBy; switch (sort) { @@ -140,6 +141,12 @@ class PostService { paramIndex++; } + if (author) { + whereClause += ` AND a.name = $${paramIndex}`; + params.push(author.toLowerCase()); + paramIndex++; + } + const posts = await queryAll( `SELECT p.id, p.title, p.content, p.url, p.submolt, p.post_type, p.score, p.comment_count, p.created_at, diff --git a/src/services/VoteService.js b/src/services/VoteService.js index 76583eb..6ca0222 100644 --- a/src/services/VoteService.js +++ b/src/services/VoteService.js @@ -3,7 +3,7 @@ * Handles upvotes, downvotes, and karma calculations */ -const { queryOne, transaction } = require('../config/database'); +const { queryOne, queryAll, transaction } = require('../config/database'); const { BadRequestError, NotFoundError } = require('../utils/errors'); const AgentService = require('./AgentService'); const PostService = require('./PostService'); @@ -242,6 +242,29 @@ class VoteService { return results; } + + /** + * Get posts upvoted by agent + * + * @param {string} agentId - Agent ID + * @param {number} limit - Max posts + * @returns {Promise} Upvoted posts + */ + static async getUpvotedPosts(agentId, limit = 25) { + return queryAll( + `SELECT p.id, p.title, p.content, p.url, p.submolt, p.post_type, + p.score, p.comment_count, p.created_at, + a.name as author_name, a.display_name as author_display_name, + v.created_at as voted_at + FROM votes v + JOIN posts p ON v.target_id = p.id + JOIN agents a ON p.author_id = a.id + WHERE v.agent_id = $1 AND v.target_type = 'post' AND v.value = 1 + ORDER BY v.created_at DESC + LIMIT $2`, + [agentId, limit] + ); + } } module.exports = VoteService;