All files / src/routes likes.ts

88.6% Statements 70/79
80% Branches 8/10
100% Functions 2/2
88.6% Lines 70/79

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 991x               1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x   1x 39x   39x 4x 4x 4x 4x     39x 39x 39x 39x 1x 1x 1x 1x 1x 1x 1x 39x     39x 39x 39x 39x 3x 3x 3x   3x 3x 3x   2x 3x 1x 1x 1x 1x 1x 1x 1x                     1x   2x 2x 3x 39x 39x  
import { Prisma } from "@prisma/client";
import { AppError } from "@ontrack/backend-common";
import type { FastifyPluginAsync, FastifyRequest } from "fastify";
import type { AppConfig } from "../config.js";
import { assertServiceToken } from "../lib/service-token.js";
import { readUserUid } from "../lib/user-uid.js";
import { prisma } from "../lib/prisma.js";
 
const trackIdParamSchema = {
  type: "object",
  required: ["trackId"],
  additionalProperties: false,
  properties: {
    trackId: { type: "string", pattern: "^[1-9][0-9]*$" },
  },
} as const;
 
const toggleResponseSchema = {
  type: "object",
  required: ["liked", "likeCount"],
  additionalProperties: false,
  properties: {
    liked: { type: "boolean" },
    likeCount: { type: "integer", minimum: 0 },
  },
} as const;
 
const likedResponseSchema = {
  type: "object",
  required: ["trackIds"],
  additionalProperties: false,
  properties: {
    trackIds: { type: "array", items: { type: "integer", minimum: 1 } },
  },
} as const;
 
export const likesRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (app, opts) => {
  const { config } = opts;
 
  function authorize(request: FastifyRequest): string {
    const token = request.headers["x-service-token"];
    assertServiceToken(Array.isArray(token) ? token[0] : token, config.serviceToken);
    return readUserUid(request);
  }
 
  /** Id треков, лайкнутых текущим пользователем. */
  app.get(
    "/tracks/liked",
    { schema: { response: { 200: likedResponseSchema } } },
    async (request, reply) => {
      const uid = authorize(request);
      const rows = await prisma.trackLike.findMany({
        where: { firebaseUid: uid },
        select: { trackId: true },
      });
      return reply.send({ trackIds: rows.map((r) => r.trackId) });
    },
  );
 
  /** Тоггл лайка трека текущим пользователем; отдаёт новое состояние и счётчик. */
  app.post(
    "/tracks/:trackId/like",
    { schema: { params: trackIdParamSchema, response: { 200: toggleResponseSchema } } },
    async (request, reply) => {
      const uid = authorize(request);
      const { trackId } = request.params as { trackId: string };
      const id = Number.parseInt(trackId, 10);
 
      const existing = await prisma.trackLike.findUnique({
        where: { trackId_firebaseUid: { trackId: id, firebaseUid: uid } },
      });
 
      let liked: boolean;
      if (existing) {
        await prisma.trackLike.delete({ where: { id: existing.id } });
        liked = false;
      } else {
        try {
          await prisma.trackLike.create({ data: { trackId: id, firebaseUid: uid } });
          liked = true;
        } catch (err) {
          if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2003") {
            throw new AppError(404, "TRACK_NOT_FOUND", "Track not found");
          }
          // P2002: concurrent like already inserted — treat as liked.
          if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
            liked = true;
          } else {
            throw err;
          }
        }
      }
 
      const likeCount = await prisma.trackLike.count({ where: { trackId: id } });
      return reply.send({ liked, likeCount });
    },
  );
};