All files / src/services/db reaction.db.ts

43.08% Statements 81/188
100% Branches 15/15
70% Functions 7/10
48.68% Lines 74/152

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 1532x 2x 2x 2x 2x 2x     2x   2x 40x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 8x 8x 8x 8x 8x   8x 2x 2x     8x 3x 3x     8x 2x 2x     8x   8x 7x   1x                             2x 2x 2x 2x 2x 2x 10x   10x 10x 9x         7x 2x 2x     5x         5x 5x     3x                               2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 4x 4x                 4x       3x   1x                         27x  
// src/services/db/reaction.db.ts
import type { Pool, PoolClient } from 'pg';
import type { Logger } from 'pino';
import { getPool, withTransaction } from './connection.db';
import { handleDbError } from './errors.db';
import type { UserReaction } from '../../types';

export class ReactionRepository {
  private db: Pick<Pool | PoolClient, 'query'>;

  constructor(db: Pick<Pool | PoolClient, 'query'> = getPool()) {
    this.db = db;
  }
 
  /**
   * Fetches user reactions based on query filters.
   * Supports filtering by user_id, entity_type, and entity_id.
   */
  async getReactions(
    filters: {
      userId?: string;
      entityType?: string;
      entityId?: string;
    },
    logger: Logger,
  ): Promise<UserReaction[]> {
    const { userId, entityType, entityId } = filters;
    try {
      let query = 'SELECT * FROM public.user_reactions WHERE 1=1';
      const params: (string | number)[] = [];
      let paramCount = 1;

      if (userId) {
        query += ` AND user_id = $${paramCount++}`;
        params.push(userId);
      }

      if (entityType) {
        query += ` AND entity_type = $${paramCount++}`;
        params.push(entityType);
      }

      if (entityId) {
        query += ` AND entity_id = $${paramCount++}`;
        params.push(entityId);
      }

      query += ' ORDER BY created_at DESC';

      const result = await this.db.query<UserReaction>(query, params);
      return result.rows;
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in getReactions',
        { filters },
        {
          defaultMessage: 'Failed to retrieve user reactions.',
        },
      );
    }
  }

  /**
   * Toggles a user's reaction to an entity.
   * If the reaction exists, it's deleted. If it doesn't, it's created.
   * @returns The created UserReaction if a reaction was added, or null if it was removed.
   */
  async toggleReaction(
    reactionData: Omit<UserReaction, 'reaction_id' | 'created_at' | 'updated_at'>,
    logger: Logger,
  ): Promise<UserReaction | null> {
    const { user_id, entity_type, entity_id, reaction_type } = reactionData;

    try {
      return await withTransaction(async (client) => {
        const deleteRes = await client.query(
          'DELETE FROM public.user_reactions WHERE user_id = $1 AND entity_type = $2 AND entity_id = $3 AND reaction_type = $4',
          [user_id, entity_type, entity_id, reaction_type],
        );

        if ((deleteRes.rowCount ?? 0) > 0) {
          logger.debug({ reactionData }, 'Reaction removed.');
          return null;
        }

        const insertRes = await client.query<UserReaction>(
          'INSERT INTO public.user_reactions (user_id, entity_type, entity_id, reaction_type) VALUES ($1, $2, $3, $4) RETURNING *',
          [user_id, entity_type, entity_id, reaction_type],
        );

        logger.debug({ reaction: insertRes.rows[0] }, 'Reaction added.');
        return insertRes.rows[0];
      });
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in toggleReaction',
        { reactionData },
        {
          fkMessage: 'The specified user or entity does not exist.',
          defaultMessage: 'Failed to toggle user reaction.',
        },
      );
    }
  }

  /**
   * Gets a summary of reactions for a specific entity.
   * Counts the number of each reaction_type.
   * @param entityType The type of the entity (e.g., 'recipe').
   * @param entityId The ID of the entity.
   * @param logger The pino logger instance.
   * @returns A promise that resolves to an array of reaction summaries.
   */
  async getReactionSummary(
    entityType: string,
    entityId: string,
    logger: Logger,
  ): Promise<{ reaction_type: string; count: number }[]> {
    try {
      const query = `
        SELECT
          reaction_type,
          COUNT(*)::int as count
        FROM public.user_reactions
        WHERE entity_type = $1 AND entity_id = $2
        GROUP BY reaction_type
        ORDER BY count DESC;
      `;
      const result = await getPool().query<{ reaction_type: string; count: number }>(query, [
        entityType,
        entityId,
      ]);
      return result.rows;
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in getReactionSummary',
        { entityType, entityId },
        {
          defaultMessage: 'Failed to retrieve reaction summary.',
        },
      );
    }
  }
}

export const reactionRepo = new ReactionRepository();