Files
flyer-crawler.projectium.com/src/controllers/reactions.controller.ts
Torben Sorensen 2d2cd52011
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
Massive Dependency Modernization Project
2026-02-13 00:34:22 -08:00

205 lines
6.2 KiB
TypeScript

// 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<SuccessResponseType<UserReaction[]>> {
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<ErrorResponse>(400, 'Missing required query parameters')
public async getReactionSummary(
@Request() request: ExpressRequest,
@Query() entityType: string,
@Query() entityId: string,
): Promise<SuccessResponseType<ReactionSummaryEntry[]>> {
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<ErrorResponse>(401, 'Unauthorized - invalid or missing token')
public async toggleReaction(
@Request() request: ExpressRequest,
@Body() body: ToggleReactionRequest,
): Promise<SuccessResponseType<ReactionAddedResponse | ReactionRemovedResponse>> {
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.' });
}
}
}