All files / src/routes favorites.ts

50.64% Statements 39/77
100% Branches 1/1
50% Functions 1/2
50.64% Lines 39/77

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 971x               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             39x 39x 39x 39x               39x     39x 39x 39x 39x                                                               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: ["favorited"],
  additionalProperties: false,
  properties: {
    favorited: { type: "boolean" },
  },
} as const;
 
const favoritedResponseSchema = {
  type: "object",
  required: ["trackIds"],
  additionalProperties: false,
  properties: {
    trackIds: { type: "array", items: { type: "integer", minimum: 1 } },
  },
} as const;
 
export const favoritesRoutes: 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/favorited",
    { schema: { response: { 200: favoritedResponseSchema } } },
    async (request, reply) => {
      const uid = authorize(request);
      const rows = await prisma.trackFavorite.findMany({
        where: { firebaseUid: uid },
        select: { trackId: true },
      });
      return reply.send({ trackIds: rows.map((r) => r.trackId) });
    },
  );
 
  /** Тоггл избранного для трека текущим пользователем; отдаёт новое состояние. */
  app.post(
    "/tracks/:trackId/favorite",
    { 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.trackFavorite.findUnique({
        where: { trackId_firebaseUid: { trackId: id, firebaseUid: uid } },
      });
 
      let favorited: boolean;
      if (existing) {
        await prisma.trackFavorite.delete({ where: { id: existing.id } });
        favorited = false;
      } else {
        try {
          await prisma.trackFavorite.create({ data: { trackId: id, firebaseUid: uid } });
          favorited = true;
        } catch (err) {
          if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2003") {
            throw new AppError(404, "TRACK_NOT_FOUND", "Track not found");
          }
          // P2002: гонка — запись уже создана; считаем «в избранном».
          if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
            favorited = true;
          } else {
            throw err;
          }
        }
      }
 
      return reply.send({ favorited });
    },
  );
};