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 | 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 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 });
},
);
};
|