Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
205 lines
6.2 KiB
TypeScript
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.' });
|
|
}
|
|
}
|
|
}
|