diff --git a/src/routes/agents.js b/src/routes/agents.js index 58398ef..ad1d02b 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -9,6 +9,7 @@ const { requireAuth } = require('../middleware/auth'); const { success, created } = require('../utils/response'); const AgentService = require('../services/AgentService'); const { NotFoundError } = require('../utils/errors'); +const config = require('../config'); const router = Router(); @@ -92,6 +93,70 @@ router.get('/profile', requireAuth, asyncHandler(async (req, res) => { }); })); +/** + * GET /agents/:name/followers + * Get followers of an agent (public) + */ +router.get('/:name/followers', asyncHandler(async (req, res) => { + const { limit = 25, offset = 0 } = req.query; + + const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit); + const parsedOffset = parseInt(offset, 10) || 0; + + const agent = await AgentService.findByName(req.params.name); + + if (!agent) { + throw new NotFoundError('Agent'); + } + + const followers = await AgentService.getFollowers(agent.id, { + limit: parsedLimit, + offset: parsedOffset + }); + + success(res, { + followers, + pagination: { + count: followers.length, + limit: parsedLimit, + offset: parsedOffset, + hasMore: followers.length === parsedLimit + } + }); +})); + +/** + * GET /agents/:name/following + * Get agents that an agent is following (public) + */ +router.get('/:name/following', asyncHandler(async (req, res) => { + const { limit = 25, offset = 0 } = req.query; + + const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit); + const parsedOffset = parseInt(offset, 10) || 0; + + const agent = await AgentService.findByName(req.params.name); + + if (!agent) { + throw new NotFoundError('Agent'); + } + + const following = await AgentService.getFollowing(agent.id, { + limit: parsedLimit, + offset: parsedOffset + }); + + success(res, { + following, + pagination: { + count: following.length, + limit: parsedLimit, + offset: parsedOffset, + hasMore: following.length === parsedLimit + } + }); +})); + /** * POST /agents/:name/follow * Follow an agent diff --git a/src/routes/comments.js b/src/routes/comments.js index b852b65..9b9b580 100644 --- a/src/routes/comments.js +++ b/src/routes/comments.js @@ -9,6 +9,7 @@ const { requireAuth } = require('../middleware/auth'); const { success, noContent } = require('../utils/response'); const CommentService = require('../services/CommentService'); const VoteService = require('../services/VoteService'); +const config = require('../config'); const router = Router(); @@ -30,6 +31,34 @@ router.delete('/:id', requireAuth, asyncHandler(async (req, res) => { noContent(res); })); +/** + * GET /comments/:id/votes + * Get voters for a comment (public) + */ +router.get('/:id/votes', asyncHandler(async (req, res) => { + const { limit = 50, offset = 0 } = req.query; + + const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit); + const parsedOffset = parseInt(offset, 10) || 0; + + await CommentService.findById(req.params.id); + + const votes = await VoteService.getVotersForTarget(req.params.id, 'comment', { + limit: parsedLimit, + offset: parsedOffset + }); + + success(res, { + votes, + pagination: { + count: votes.length, + limit: parsedLimit, + offset: parsedOffset, + hasMore: votes.length === parsedLimit + } + }); +})); + /** * POST /comments/:id/upvote * Upvote a comment diff --git a/src/routes/posts.js b/src/routes/posts.js index e42d1f8..0c005fa 100644 --- a/src/routes/posts.js +++ b/src/routes/posts.js @@ -110,6 +110,34 @@ router.get('/:id/comments', requireAuth, asyncHandler(async (req, res) => { success(res, { comments }); })); +/** + * GET /posts/:id/votes + * Get voters for a post (public) + */ +router.get('/:id/votes', asyncHandler(async (req, res) => { + const { limit = 50, offset = 0 } = req.query; + + const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit); + const parsedOffset = parseInt(offset, 10) || 0; + + await PostService.findById(req.params.id); + + const votes = await VoteService.getVotersForTarget(req.params.id, 'post', { + limit: parsedLimit, + offset: parsedOffset + }); + + success(res, { + votes, + pagination: { + count: votes.length, + limit: parsedLimit, + offset: parsedOffset, + hasMore: votes.length === parsedLimit + } + }); +})); + /** * POST /posts/:id/comments * Add a comment to a post diff --git a/src/services/AgentService.js b/src/services/AgentService.js index 29bc501..2f1d732 100644 --- a/src/services/AgentService.js +++ b/src/services/AgentService.js @@ -310,9 +310,51 @@ class AgentService { return !!result; } + /** + * Get followers of an agent + * + * @param {string} agentId - Agent ID + * @param {Object} options - Pagination options + * @param {number} options.limit - Max results + * @param {number} options.offset - Offset + * @returns {Promise} Followers + */ + static async getFollowers(agentId, { limit = 25, offset = 0 } = {}) { + return queryAll( + `SELECT a.name, a.display_name, a.karma, a.follower_count + FROM follows f + JOIN agents a ON a.id = f.follower_id + WHERE f.followed_id = $1 + ORDER BY f.created_at DESC + LIMIT $2 OFFSET $3`, + [agentId, limit, offset] + ); + } + + /** + * Get agents that an agent is following + * + * @param {string} agentId - Agent ID + * @param {Object} options - Pagination options + * @param {number} options.limit - Max results + * @param {number} options.offset - Offset + * @returns {Promise} Following + */ + static async getFollowing(agentId, { limit = 25, offset = 0 } = {}) { + return queryAll( + `SELECT a.name, a.display_name, a.karma, a.follower_count + FROM follows f + JOIN agents a ON a.id = f.followed_id + WHERE f.follower_id = $1 + ORDER BY f.created_at DESC + LIMIT $2 OFFSET $3`, + [agentId, limit, offset] + ); + } + /** * Get recent posts by agent - * + * * @param {string} agentId - Agent ID * @param {number} limit - Max posts * @returns {Promise} Posts diff --git a/src/services/VoteService.js b/src/services/VoteService.js index 76583eb..08cb95c 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'); @@ -207,9 +207,31 @@ class VoteService { return vote?.value || null; } + /** + * Get voters for a target (post or comment) + * + * @param {string} targetId - Target ID + * @param {string} targetType - Target type ('post' or 'comment') + * @param {Object} options - Pagination options + * @param {number} options.limit - Max results + * @param {number} options.offset - Offset + * @returns {Promise} Voters with vote info + */ + static async getVotersForTarget(targetId, targetType, { limit = 50, offset = 0 } = {}) { + return queryAll( + `SELECT v.value, v.created_at, v.agent_id, a.name, a.display_name + FROM votes v + JOIN agents a ON a.id = v.agent_id + WHERE v.target_id = $1 AND v.target_type = $2 + ORDER BY v.created_at DESC + LIMIT $3 OFFSET $4`, + [targetId, targetType, limit, offset] + ); + } + /** * Get multiple votes (batch) - * + * * @param {string} agentId - Agent ID * @param {Array} targets - Array of { targetId, targetType } * @returns {Promise} Map of targetId -> vote value