// src/controllers/reactions.controller.ts // ============================================================================ // REACTIONS CONTROLLER // ============================================================================ // Provides endpoints for user reactions on content (recipes, comments, etc.). // Includes public endpoints for viewing reactions and authenticated endpoint // for toggling reactions. // // Implements ADR-028 (API Response Format) via BaseController. // ============================================================================ import { Get, Post, Route, Tags, Security, Body, Query, Request, SuccessResponse, Response, Middlewares, } from 'tsoa'; import type { Request as ExpressRequest } from 'express'; import { BaseController } from './base.controller'; import type { SuccessResponse as SuccessResponseType, ErrorResponse } from './types'; import { reactionRepo } from '../services/db/index.db'; import type { UserProfile, UserReaction } from '../types'; import { publicReadLimiter, reactionToggleLimiter } from '../config/rateLimiters'; // ============================================================================ // REQUEST/RESPONSE TYPES // ============================================================================ /** * Request body for toggling a reaction. */ interface ToggleReactionRequest { /** * Entity type (e.g., 'recipe', 'comment') * @minLength 1 */ entity_type: string; /** * Entity ID * @minLength 1 */ entity_id: string; /** * Type of reaction (e.g., 'like', 'love') * @minLength 1 */ reaction_type: string; } /** * Response for toggling a reaction - when added. */ interface ReactionAddedResponse { /** Success message */ message: string; /** The created reaction */ reaction: UserReaction; } /** * Response for toggling a reaction - when removed. */ interface ReactionRemovedResponse { /** Success message */ message: string; } /** * Reaction summary entry showing count by type. */ interface ReactionSummaryEntry { /** Reaction type */ reaction_type: string; /** Count of this reaction type */ count: number; } // ============================================================================ // REACTIONS CONTROLLER // ============================================================================ /** * Controller for user reactions on content. * * Public endpoints: * - GET /reactions - Get reactions with optional filters * - GET /reactions/summary - Get reaction summary for an entity * * Authenticated endpoints: * - POST /reactions/toggle - Toggle (add/remove) a reaction */ @Route('reactions') @Tags('Reactions') export class ReactionsController extends BaseController { // ========================================================================== // PUBLIC ENDPOINTS // ========================================================================== /** * Get reactions. * * Fetches user reactions based on query filters. Supports filtering by * userId, entityType, and entityId. All filters are optional. * * @summary Get reactions * @param request Express request for logging * @param userId Filter by user ID (UUID format) * @param entityType Filter by entity type (e.g., 'recipe', 'comment') * @param entityId Filter by entity ID * @returns List of reactions matching filters */ @Get() @Middlewares(publicReadLimiter) @SuccessResponse(200, 'List of reactions matching filters') public async getReactions( @Request() request: ExpressRequest, @Query() userId?: string, @Query() entityType?: string, @Query() entityId?: string, ): Promise> { const reactions = await reactionRepo.getReactions( { userId, entityType, entityId }, request.log, ); return this.success(reactions); } /** * Get reaction summary. * * Fetches a summary of reactions for a specific entity, showing * the count of each reaction type. * * @summary Get reaction summary * @param request Express request for logging * @param entityType Entity type (e.g., 'recipe', 'comment') - required * @param entityId Entity ID - required * @returns Reaction summary with counts by type */ @Get('summary') @Middlewares(publicReadLimiter) @SuccessResponse(200, 'Reaction summary with counts by type') @Response(400, 'Missing required query parameters') public async getReactionSummary( @Request() request: ExpressRequest, @Query() entityType: string, @Query() entityId: string, ): Promise> { const summary = await reactionRepo.getReactionSummary(entityType, entityId, request.log); return this.success(summary); } // ========================================================================== // AUTHENTICATED ENDPOINTS // ========================================================================== /** * Toggle reaction. * * Toggles a user's reaction to an entity. If the reaction exists, * it's removed; otherwise, it's added. * * @summary Toggle reaction * @param request Express request with authenticated user * @param body Reaction details * @returns Reaction added (201) or removed (200) confirmation */ @Post('toggle') @Security('bearerAuth') @Middlewares(reactionToggleLimiter) @SuccessResponse(200, 'Reaction removed') @Response(401, 'Unauthorized - invalid or missing token') public async toggleReaction( @Request() request: ExpressRequest, @Body() body: ToggleReactionRequest, ): Promise> { const userProfile = request.user as UserProfile; const reactionData = { user_id: userProfile.user.user_id, entity_type: body.entity_type, entity_id: body.entity_id, reaction_type: body.reaction_type, }; const result = await reactionRepo.toggleReaction(reactionData, request.log); if (result) { // Reaction was added this.setStatus(201); return this.success({ message: 'Reaction added.', reaction: result }); } else { // Reaction was removed return this.success({ message: 'Reaction removed.' }); } } }