From fa62c03f8b25728bb9f82ef7ba6a25e90c596105 Mon Sep 17 00:00:00 2001 From: Alzer Del Zeyger <75466035+Dx-Alz-xD@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:57:46 +0530 Subject: [PATCH] fix(request): drop duplicate paper requests and validate the body the same subject, exam, slot and year could be requested any number of times, each one a new row. a unique index on those four fields and an upsert in createPaperRequest keep one row per paper, and a repeat request gets a 200 saying it was already requested instead of a 201 the body is now checked with zod before it reaches the database, so bad json, null, or a field that is not a string returns 400 instead of 500. exam has to be one of the six exam types, which PaperRequest now also enforces, before this an unknown exam like "midterm" was saved refs #550 --- src/app/api/request/route.ts | 30 +++++++++++++++++++++++------- src/db/paperRequest.ts | 6 +++++- src/lib/services/paper.ts | 10 +++++++++- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/app/api/request/route.ts b/src/app/api/request/route.ts index 51577162..0d3d16ce 100644 --- a/src/app/api/request/route.ts +++ b/src/app/api/request/route.ts @@ -1,19 +1,35 @@ +import { z } from "zod"; import { success, failure } from "@/lib/utils/response"; import { createPaperRequest } from "@/lib/services/paper"; -import type { CreatePaperInputType } from "@/lib/services/paper"; +import { requestExams } from "@/db/paperRequest"; import { getPostHogClient } from "@/lib/posthog-server"; import { randomUUID } from "crypto"; +const paperRequestInput = z.object({ + subject: z.string().trim().min(1).max(200), + exam: z.enum(requestExams), + slot: z.string().trim().min(1).max(20), + year: z.string().trim().min(1).max(20), +}); + export async function POST(req: Request) { try { - const { subject, exam, slot, year } = - (await req.json()) as CreatePaperInputType; + const body: unknown = await req.json().catch(() => null); + const input = paperRequestInput.safeParse(body); - if (!subject || !exam || !slot || !year) { - return failure("All fields are required.", 400); + if (!input.success) { + return failure( + `send a subject, slot and year, and one of ${requestExams.join(", ")} as exam`, + 400, + ); } - const newRequest = await createPaperRequest({ subject, exam, slot, year }); + const { subject, exam, slot, year } = input.data; + const { request, alreadyRequested } = await createPaperRequest({ subject, exam, slot, year }); + + if (alreadyRequested) { + return success({ message: "this paper has already been requested", request }); + } try { const posthog = getPostHogClient(); @@ -43,7 +59,7 @@ export async function POST(req: Request) { } return success( - { message: "Paper request submitted successfully!", request: newRequest }, + { message: "Paper request submitted successfully!", request }, "Created", 201, ); diff --git a/src/db/paperRequest.ts b/src/db/paperRequest.ts index 501f4570..9de5a3dd 100644 --- a/src/db/paperRequest.ts +++ b/src/db/paperRequest.ts @@ -1,5 +1,7 @@ import mongoose, { Schema, type Document, type Model } from "mongoose"; +export const requestExams = ["CAT-1", "CAT-2", "FAT", "Model CAT-1", "Model CAT-2", "Model FAT"] as const; + export interface IPaperRequest extends Document { subject: string; exam: string; @@ -10,12 +12,14 @@ export interface IPaperRequest extends Document { const paperRequestSchema = new Schema({ subject: { type: String, required: true }, - exam: { type: String, required: true }, + exam: { type: String, enum: [...requestExams], required: true }, slot: { type: String, required: true }, year: { type: String, required: true }, createdAt: { type: Date, default: Date.now } }); +paperRequestSchema.index({ subject: 1, exam: 1, slot: 1, year: 1 }, { unique: true }); + const PaperRequest: Model = mongoose.models.PaperRequest ?? mongoose.model("PaperRequest", paperRequestSchema); diff --git a/src/lib/services/paper.ts b/src/lib/services/paper.ts index 4eb52f43..e25ed8d6 100644 --- a/src/lib/services/paper.ts +++ b/src/lib/services/paper.ts @@ -61,7 +61,15 @@ export async function getCourseCounts(){ export async function createPaperRequest({ subject, exam, slot, year} : CreatePaperInputType){ await connectToDatabase(); - return await PaperRequest.create({ subject, exam, slot, year }); + const result = await PaperRequest.findOneAndUpdate( + { subject, exam, slot, year }, + { $setOnInsert: { subject, exam, slot, year } }, + { upsert: true, new: true, runValidators: true, includeResultMetadata: true }, + ); + return { + request: result.value, + alreadyRequested: result.lastErrorObject?.updatedExisting === true, + }; } export async function getSelectedPapers() {