From 1e96be9f516352b4541d5ec74459d2e02a911938 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Sat, 7 Feb 2026 17:07:25 +0530 Subject: [PATCH 1/5] feat: add isSubmitted field to FormResponse table --- .../migration.sql | 4 ++++ prisma/schema.prisma | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20260207112502_add_is_submitted_in_form_response/migration.sql diff --git a/prisma/migrations/20260207112502_add_is_submitted_in_form_response/migration.sql b/prisma/migrations/20260207112502_add_is_submitted_in_form_response/migration.sql new file mode 100644 index 0000000..e8ad2d8 --- /dev/null +++ b/prisma/migrations/20260207112502_add_is_submitted_in_form_response/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "form_response" ADD COLUMN "isSubmitted" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "submittedAt" DROP NOT NULL, +ALTER COLUMN "submittedAt" DROP DEFAULT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f7f7c91..806133f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -129,8 +129,9 @@ model FormResponse { respondent User? @relation(fields: [respondentId], references: [id], onDelete: SetNull) answers Json - submittedAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isSubmitted Boolean @default(false) + submittedAt DateTime? + updatedAt DateTime @updatedAt @@index([formId]) @@map("form_response") From aff2dd59fb6a5fb340096fc5312d3700e5fffa7e Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Sat, 7 Feb 2026 17:35:51 +0530 Subject: [PATCH 2/5] feat: add draftResponse endpoint --- src/api/form-response/controller.ts | 39 +++++++++++++++++++++++++++++ src/api/form-response/routes.ts | 4 ++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/api/form-response/controller.ts b/src/api/form-response/controller.ts index b7c6906..928938e 100644 --- a/src/api/form-response/controller.ts +++ b/src/api/form-response/controller.ts @@ -41,6 +41,8 @@ export async function submitResponse({ data: { formId: params.formId, respondentId: user.id, + submittedAt: new Date(), + isSubmitted: true, answers: body.answers, }, }); @@ -54,6 +56,43 @@ export async function submitResponse({ }; } +export async function submitDraftResponse({ + params, + body, + user, + set, +}: FormResponseContext) { + const form = await prisma.form.findUnique({ + where: { + id: params.formId, + }, + }); + + if (!form) { + logger.warn(`Form with ID ${params.formId} not found`); + set.status = 404; + return { + success: false, + message: "Form not found", + }; + } + + const response = await prisma.formResponse.create({ + data: { + formId: params.formId, + respondentId: user.id, + answers: body.answers, + }, + }); + + logger.info(`User ${user.id} saved draft response for form ${params.formId}`); + return { + success: true, + message: "Draft response saved successfully", + data: response, + }; +} + export async function resumeResponse({ params, body, diff --git a/src/api/form-response/routes.ts b/src/api/form-response/routes.ts index 4e29856..8bbb47b 100644 --- a/src/api/form-response/routes.ts +++ b/src/api/form-response/routes.ts @@ -10,12 +10,14 @@ import { getResponseForFormOwner, getSubmittedResponse, resumeResponse, + submitDraftResponse, submitResponse, } from "./controller"; export const formResponseRoutes = new Elysia({ prefix: "/responses" }) .use(requireAuth) - .post("/:formId", submitResponse, formResponseDTO) + .post("/submit/:formId", submitResponse, formResponseDTO) + .post("/draft/:formId", submitDraftResponse, formResponseDTO) .put("/resume/:responseId", resumeResponse, resumeResponseDTO) .get("/:formId", getResponseForFormOwner, formResponseForFormOwnerDTO) .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO); From 369f5d159f16711bca7329d59310be782899a018 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Sat, 7 Feb 2026 19:38:22 +0530 Subject: [PATCH 3/5] feat: add saveDraft and make tests accordingly --- src/api/form-response/controller.ts | 154 ++++++++++++++++++------ src/api/form-response/routes.ts | 11 +- src/test/form-response.test.ts | 129 ++++++++++++++------ src/test/forms.test.ts | 180 +++++++++++++++------------- 4 files changed, 309 insertions(+), 165 deletions(-) diff --git a/src/api/form-response/controller.ts b/src/api/form-response/controller.ts index 928938e..aad9bba 100644 --- a/src/api/form-response/controller.ts +++ b/src/api/form-response/controller.ts @@ -4,7 +4,6 @@ import type { FormResponseContext, FormResponseForFormOwnerContext, GetSubmittedResponseContext, - ResumeResponseContext, } from "../../types/form-response"; export async function submitResponse({ @@ -37,13 +36,41 @@ export async function submitResponse({ }; } - const response = await prisma.formResponse.create({ - data: { + const existing = await prisma.formResponse.findUnique({ + where: { + formId_respondentId: { + formId: params.formId, + respondentId: user.id, + }, + }, + }); + + if (existing?.isSubmitted) { + set.status = 400; + return { + success: false, + message: "You have already submitted this form", + }; + } + + const response = await prisma.formResponse.upsert({ + where: { + formId_respondentId: { + formId: params.formId, + respondentId: user.id, + }, + }, + update: { + answers: body.answers, + isSubmitted: true, + submittedAt: new Date(), + }, + create: { formId: params.formId, respondentId: user.id, - submittedAt: new Date(), - isSubmitted: true, answers: body.answers, + isSubmitted: true, + submittedAt: new Date(), }, }); logger.info( @@ -56,7 +83,7 @@ export async function submitResponse({ }; } -export async function submitDraftResponse({ +export async function saveDraftResponse({ params, body, user, @@ -77,49 +104,45 @@ export async function submitDraftResponse({ }; } - const response = await prisma.formResponse.create({ - data: { - formId: params.formId, - respondentId: user.id, - answers: body.answers, + const existing = await prisma.formResponse.findUnique({ + where: { + formId_respondentId: { + formId: params.formId, + respondentId: user.id, + }, }, }); - logger.info(`User ${user.id} saved draft response for form ${params.formId}`); - return { - success: true, - message: "Draft response saved successfully", - data: response, - }; -} + if (existing?.isSubmitted) { + set.status = 400; + return { + success: false, + message: "Response already submitted and cannot be edited", + }; + } -export async function resumeResponse({ - params, - body, - user, -}: ResumeResponseContext) { - const response = await prisma.formResponse.updateMany({ + const response = await prisma.formResponse.upsert({ where: { - id: params.responseId, - respondentId: user.id, + formId_respondentId: { + formId: params.formId, + respondentId: user.id, + }, }, - data: { + update: { + answers: body.answers, + isSubmitted: false, + }, + create: { + formId: params.formId, + respondentId: user.id, answers: body.answers, }, }); - if (response.count === 0) { - logger.warn(`No response found with ID ${params.responseId} to update`); - return { - success: false, - message: "No response found to update", - }; - } - - logger.info(`Response ${params.responseId} updated successfully`); + logger.info(`User ${user.id} saved draft response for form ${params.formId}`); return { success: true, - message: "Response updated successfully", + message: "Draft response saved successfully", data: response, }; } @@ -150,6 +173,7 @@ export async function getResponseForFormOwner({ const responses = await prisma.formResponse.findMany({ where: { formId: params.formId, + isSubmitted: true, }, select: { id: true, @@ -219,6 +243,7 @@ export async function getSubmittedResponse({ where: { respondentId: user.id, formId: params.formId, + isSubmitted: true, }, select: { id: true, @@ -284,3 +309,58 @@ export async function getSubmittedResponse({ data: formattedResponses, }; } + +export async function getDraftResponse({ + params, + user, + set, +}: GetSubmittedResponseContext) { + const draft = await prisma.formResponse.findFirst({ + where: { + respondentId: user.id, + formId: params.formId, + isSubmitted: false, + }, + select: { + id: true, + formId: true, + answers: true, + form: { + select: { title: true }, + }, + }, + }); + + if (!draft) { + set.status = 404; + return { + success: false, + message: "No draft found", + }; + } + + const fields = await prisma.formFields.findMany({ + where: { formId: params.formId }, + select: { id: true, fieldName: true }, + }); + + const map = Object.fromEntries(fields.map((f) => [f.id, f.fieldName])); + + const transformed: Record = {}; + + for (const [fieldId, value] of Object.entries( + draft.answers as Record, + )) { + transformed[map[fieldId] ?? fieldId] = value; + } + + return { + success: true, + data: { + id: draft.id, + formId: draft.formId, + formTitle: draft.form.title, + answers: transformed, + }, + }; +} diff --git a/src/api/form-response/routes.ts b/src/api/form-response/routes.ts index 8bbb47b..eced51d 100644 --- a/src/api/form-response/routes.ts +++ b/src/api/form-response/routes.ts @@ -3,21 +3,20 @@ import { formResponseDTO, formResponseForFormOwnerDTO, getSubmittedResponseDTO, - resumeResponseDTO, } from "../../types/form-response"; import { requireAuth } from "../auth/requireAuth"; import { + getDraftResponse, getResponseForFormOwner, getSubmittedResponse, - resumeResponse, - submitDraftResponse, + saveDraftResponse, submitResponse, } from "./controller"; export const formResponseRoutes = new Elysia({ prefix: "/responses" }) .use(requireAuth) .post("/submit/:formId", submitResponse, formResponseDTO) - .post("/draft/:formId", submitDraftResponse, formResponseDTO) - .put("/resume/:responseId", resumeResponse, resumeResponseDTO) + .post("/draft/:formId", saveDraftResponse, formResponseDTO) .get("/:formId", getResponseForFormOwner, formResponseForFormOwnerDTO) - .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO); + .get("/user/:formId", getSubmittedResponse, getSubmittedResponseDTO) + .get("/draft/:formId", getDraftResponse, getSubmittedResponseDTO); diff --git a/src/test/form-response.test.ts b/src/test/form-response.test.ts index 3905a94..2b4ea3e 100644 --- a/src/test/form-response.test.ts +++ b/src/test/form-response.test.ts @@ -3,9 +3,10 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; // ---------------- MOCK PRISMA ---------------- const formFindUniqueMock = mock(); -const formResponseCreateMock = mock(); -const formResponseUpdateManyMock = mock(); +const formResponseFindUniqueMock = mock(); +const formResponseUpsertMock = mock(); const formResponseFindManyMock = mock(); +const formResponseFindFirstMock = mock(); const formFieldsFindManyMock = mock(); mock.module("../db/prisma", () => ({ @@ -14,9 +15,10 @@ mock.module("../db/prisma", () => ({ findUnique: formFindUniqueMock, }, formResponse: { - create: formResponseCreateMock, - updateMany: formResponseUpdateManyMock, + findUnique: formResponseFindUniqueMock, + upsert: formResponseUpsertMock, findMany: formResponseFindManyMock, + findFirst: formResponseFindFirstMock, }, formFields: { findMany: formFieldsFindManyMock, @@ -40,17 +42,19 @@ mock.module("../logger", () => ({ // IMPORT AFTER MOCKS const { submitResponse, - resumeResponse, + saveDraftResponse, getResponseForFormOwner, getSubmittedResponse, + getDraftResponse, } = await import("../api/form-response/controller"); describe("Form Response Controller Tests", () => { beforeEach(() => { formFindUniqueMock.mockReset(); - formResponseCreateMock.mockReset(); - formResponseUpdateManyMock.mockReset(); + formResponseFindUniqueMock.mockReset(); + formResponseUpsertMock.mockReset(); formResponseFindManyMock.mockReset(); + formResponseFindFirstMock.mockReset(); formFieldsFindManyMock.mockReset(); mockInfo.mockReset(); mockWarn.mockReset(); @@ -68,7 +72,9 @@ describe("Form Response Controller Tests", () => { isPublished: true, }); - formResponseCreateMock.mockResolvedValue({ id: "r1" }); + formResponseFindUniqueMock.mockResolvedValue(null); + + formResponseUpsertMock.mockResolvedValue({ id: "r1" }); const set: any = {}; @@ -117,32 +123,68 @@ describe("Form Response Controller Tests", () => { expect(set.status).toBe(403); }); + it("submitResponse → already submitted", async () => { + formFindUniqueMock.mockResolvedValue({ + id: "f1", + isPublished: true, + }); + + formResponseFindUniqueMock.mockResolvedValue({ + isSubmitted: true, + }); + + const set: any = {}; + + const res = await submitResponse({ + params: { formId: "f1" }, + body: { answers: {} }, + user, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(400); + }); + // ===================================================== - // resumeResponse + // saveDraftResponse // ===================================================== - it("resumeResponse → success", async () => { - formResponseUpdateManyMock.mockResolvedValue({ count: 1 }); + it("saveDraftResponse → success", async () => { + formFindUniqueMock.mockResolvedValue({ id: "f1" }); + formResponseFindUniqueMock.mockResolvedValue(null); + formResponseUpsertMock.mockResolvedValue({ id: "r1" }); - const res = await resumeResponse({ - params: { responseId: "r1" }, + const set: any = {}; + + const res = await saveDraftResponse({ + params: { formId: "f1" }, body: { answers: {} }, user, + set, } as any); expect(res.success).toBe(true); }); - it("resumeResponse → not found", async () => { - formResponseUpdateManyMock.mockResolvedValue({ count: 0 }); + it("saveDraftResponse → already submitted", async () => { + formFindUniqueMock.mockResolvedValue({ id: "f1" }); - const res = await resumeResponse({ - params: { responseId: "r1" }, + formResponseFindUniqueMock.mockResolvedValue({ + isSubmitted: true, + }); + + const set: any = {}; + + const res = await saveDraftResponse({ + params: { formId: "f1" }, body: { answers: {} }, user, + set, } as any); expect(res.success).toBe(false); + expect(set.status).toBe(400); }); // ===================================================== @@ -177,22 +219,7 @@ describe("Form Response Controller Tests", () => { } as any); expect(res.success).toBe(true); - expect(res.data!.length).toBe(1); // ✅ FIX - }); - - it("getResponseForFormOwner → form not found", async () => { - formFindUniqueMock.mockResolvedValue(null); - - const set: any = {}; - - const res = await getResponseForFormOwner({ - params: { formId: "f1" }, - user, - set, - } as any); - - expect(res.success).toBe(false); - expect(set.status).toBe(404); + expect(res.data!.length).toBe(1); }); // ===================================================== @@ -222,15 +249,43 @@ describe("Form Response Controller Tests", () => { } as any); expect(res.success).toBe(true); - expect(res.data!.length).toBe(1); // ✅ SAFE ASSERTION + expect(res.data!.length).toBe(1); + }); + + // ===================================================== + // getDraftResponse + // ===================================================== + + it("getDraftResponse → success", async () => { + formResponseFindFirstMock.mockResolvedValue({ + id: "r1", + formId: "f1", + answers: { field1: "A" }, + form: { title: "Form A" }, + }); + + formFieldsFindManyMock.mockResolvedValue([ + { id: "field1", fieldName: "Name" }, + ]); + + const set: any = {}; + + const res = await getDraftResponse({ + params: { formId: "f1" }, + user, + set, + } as any); + + expect(res.success).toBe(true); + expect(res.data!.id).toBe("r1"); }); - it("getSubmittedResponse → none found", async () => { - formResponseFindManyMock.mockResolvedValue([]); + it("getDraftResponse → not found", async () => { + formResponseFindFirstMock.mockResolvedValue(null); const set: any = {}; - const res = await getSubmittedResponse({ + const res = await getDraftResponse({ params: { formId: "f1" }, user, set, diff --git a/src/test/forms.test.ts b/src/test/forms.test.ts index 9b25b34..606d4b0 100644 --- a/src/test/forms.test.ts +++ b/src/test/forms.test.ts @@ -32,8 +32,15 @@ mock.module("../logger", () => ({ })); // IMPORT AFTER MOCKS -const { getAllForms, createForm, getFormById, updateForm, deleteForm } = - await import("../api/forms/controller"); +const { + getAllForms, + createForm, + getFormById, + updateForm, + deleteForm, + publishForm, + unPublishForm, +} = await import("../api/forms/controller"); describe("Forms Controller Tests", () => { beforeEach(() => { @@ -48,7 +55,9 @@ describe("Forms Controller Tests", () => { const user = { id: "user1" }; - // ===== getAllForms ===== + // ============================== + // getAllForms + // ============================== it("getAllForms → success", async () => { findManyMock.mockResolvedValue([ @@ -58,7 +67,7 @@ describe("Forms Controller Tests", () => { const res = await getAllForms({ user } as any); expect(res.success).toBe(true); - expect(res.data.length).toBe(1); + expect(res.data!.length).toBe(1); }); it("getAllForms → empty", async () => { @@ -70,13 +79,9 @@ describe("Forms Controller Tests", () => { expect(res.data).toEqual([]); }); - it("getAllForms → DB error", async () => { - findManyMock.mockRejectedValue(new Error("DB fail")); - - expect(getAllForms({ user } as any)).rejects.toThrow(); - }); - - // ===== createForm ===== + // ============================== + // createForm + // ============================== it("createForm → success", async () => { createMock.mockResolvedValue({ id: "1", title: "New" }); @@ -89,29 +94,9 @@ describe("Forms Controller Tests", () => { expect(res.success).toBe(true); }); - it("createForm → called", async () => { - createMock.mockResolvedValue({ id: "1" }); - - await createForm({ - user, - body: { title: "T", description: "D" }, - } as any); - - expect(createMock).toHaveBeenCalled(); - }); - - it("createForm → DB error", async () => { - createMock.mockRejectedValue(new Error("DB crash")); - - expect( - createForm({ - user, - body: { title: "X", description: "Y" }, - } as any), - ).rejects.toThrow(); - }); - - // ===== getFormById ===== + // ============================== + // getFormById + // ============================== it("getFormById → found", async () => { findFirstMock.mockResolvedValue({ id: "1" }); @@ -119,7 +104,7 @@ describe("Forms Controller Tests", () => { const set: any = {}; const res = await getFormById({ user, - params: { id: "1" }, + params: { formId: "1" }, set, } as any); @@ -132,7 +117,7 @@ describe("Forms Controller Tests", () => { const set: any = {}; const res = await getFormById({ user, - params: { id: "2" }, + params: { formId: "2" }, set, } as any); @@ -140,35 +125,19 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); - it("getFormById → DB error", async () => { - findFirstMock.mockRejectedValue(new Error("DB error")); - - const set: any = {}; - - expect( - getFormById({ - user, - params: { id: "1" }, - set, - } as any), - ).rejects.toThrow(); - }); - - // ===== updateForm ===== + // ============================== + // updateForm + // ============================== it("updateForm → success", async () => { findFirstMock.mockResolvedValue({ id: "1" }); - - updateMock.mockResolvedValue({ - id: "1", - title: "Updated", - }); + updateMock.mockResolvedValue({ id: "1" }); const set: any = {}; const res = await updateForm({ user, - params: { id: "1" }, + params: { formId: "1" }, body: { title: "Updated", description: "D" }, set, } as any); @@ -183,7 +152,7 @@ describe("Forms Controller Tests", () => { const res = await updateForm({ user, - params: { id: "1" }, + params: { formId: "1" }, body: { title: "T", description: "D" }, set, } as any); @@ -192,22 +161,9 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); - it("updateForm → DB error", async () => { - findFirstMock.mockRejectedValue(new Error("DB fail")); - - const set: any = {}; - - expect( - updateForm({ - user, - params: { id: "1" }, - body: { title: "T", description: "D" }, - set, - } as any), - ).rejects.toThrow(); - }); - - // ===== deleteForm ===== + // ============================== + // deleteForm + // ============================== it("deleteForm → success", async () => { deleteManyMock.mockResolvedValue({ count: 1 }); @@ -216,7 +172,7 @@ describe("Forms Controller Tests", () => { const res = await deleteForm({ user, - params: { id: "1" }, + params: { formId: "1" }, set, } as any); @@ -230,7 +186,7 @@ describe("Forms Controller Tests", () => { const res = await deleteForm({ user, - params: { id: "1" }, + params: { formId: "1" }, set, } as any); @@ -238,17 +194,71 @@ describe("Forms Controller Tests", () => { expect(set.status).toBe(404); }); - it("deleteForm → DB error", async () => { - deleteManyMock.mockRejectedValue(new Error("DB crash")); + // ============================== + // publishForm + // ============================== + + it("publishForm → success", async () => { + findFirstMock.mockResolvedValue({ id: "1" }); + updateMock.mockResolvedValue({ id: "1", isPublished: true }); const set: any = {}; - expect( - deleteForm({ - user, - params: { id: "1" }, - set, - } as any), - ).rejects.toThrow(); + const res = await publishForm({ + user, + params: { formId: "1" }, + set, + } as any); + + expect(res.success).toBe(true); + }); + + it("publishForm → not found", async () => { + findFirstMock.mockResolvedValue(null); + + const set: any = {}; + + const res = await publishForm({ + user, + params: { formId: "1" }, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(404); + }); + + // ============================== + // unPublishForm + // ============================== + + it("unPublishForm → success", async () => { + findFirstMock.mockResolvedValue({ id: "1" }); + updateMock.mockResolvedValue({ id: "1", isPublished: false }); + + const set: any = {}; + + const res = await unPublishForm({ + user, + params: { formId: "1" }, + set, + } as any); + + expect(res.success).toBe(true); + }); + + it("unPublishForm → not found", async () => { + findFirstMock.mockResolvedValue(null); + + const set: any = {}; + + const res = await unPublishForm({ + user, + params: { formId: "1" }, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(404); }); }); From bb4cdf102c64e83258681b68a4190c74c6439f66 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Sat, 7 Feb 2026 19:38:34 +0530 Subject: [PATCH 4/5] feat: update scheme --- .../20260207124939_add_unique_form_response/migration.sql | 8 ++++++++ prisma/schema.prisma | 1 + 2 files changed, 9 insertions(+) create mode 100644 prisma/migrations/20260207124939_add_unique_form_response/migration.sql diff --git a/prisma/migrations/20260207124939_add_unique_form_response/migration.sql b/prisma/migrations/20260207124939_add_unique_form_response/migration.sql new file mode 100644 index 0000000..d752951 --- /dev/null +++ b/prisma/migrations/20260207124939_add_unique_form_response/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[formId,respondentId]` on the table `form_response` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "form_response_formId_respondentId_key" ON "form_response"("formId", "respondentId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 806133f..8c54fbf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -133,6 +133,7 @@ model FormResponse { submittedAt DateTime? updatedAt DateTime @updatedAt + @@unique([formId, respondentId]) @@index([formId]) @@map("form_response") } From 5b7c55832f5c4dd9f30580c8dc36a12509ef2e06 Mon Sep 17 00:00:00 2001 From: Nandgopal-R Date: Sat, 7 Feb 2026 19:38:46 +0530 Subject: [PATCH 5/5] docs: update docs --- bruno/form-response/draftResponse.bru | 96 +++++++++++++++++++ bruno/form-response/getDraftResponse.bru | 76 +++++++++++++++ .../getFormResponsesForFormOwner.bru | 4 +- bruno/form-response/getSubmittedResponse.bru | 2 +- bruno/form-response/resumeResponse.bru | 86 ----------------- bruno/form-response/submitResponse.bru | 78 ++++++++------- 6 files changed, 216 insertions(+), 126 deletions(-) create mode 100644 bruno/form-response/draftResponse.bru create mode 100644 bruno/form-response/getDraftResponse.bru delete mode 100644 bruno/form-response/resumeResponse.bru diff --git a/bruno/form-response/draftResponse.bru b/bruno/form-response/draftResponse.bru new file mode 100644 index 0000000..ff71b12 --- /dev/null +++ b/bruno/form-response/draftResponse.bru @@ -0,0 +1,96 @@ +meta { + name: draftResponse + type: http + seq: 2 +} + +post { + url: http://localhost:8000/responses/draft/:formId + body: json + auth: inherit +} + +params:path { + formId: 4a386821-772f-4928-b01f-034daf8dc456 +} + +body:json { + { + "answers":{ + "14ba7ca4-8db7-48d7-8d40-d6a2d43ecc75": "8282476890" + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + # Save Draft Response + + ## Endpoint + `POST /forms/:formId/draft` + + ## Description + Saves or updates a draft response for a form. If a response already exists, it is updated. If the response has already been submitted, saving a draft is not allowed. + + ## Controller + `saveDraftResponse` + + ## Auth Required + ✅ Yes (User must be logged in) + + ## Path Parameters + + | Name | Type | Required | Description | + |--------|--------|----------|-------------| + | formId | string | Yes | ID of the form | + + ## Request Body + + ```json + { + "answers": { + "fieldId1": "Partial Answer 1", + "fieldId2": "Partial Answer 2" + } + } + + Responses + Success Response + Status: 200 OK + + { + "success": true, + "message": "Draft response saved successfully", + "data": { + "id": "response_123", + "formId": "form_456", + "respondentId": "user_789", + "answers": { + "fieldId1": "Partial Answer 1", + "fieldId2": "Partial Answer 2" + }, + "isSubmitted": false, + "submittedAt": null, + "updatedAt": "2026-02-07T10:30:00.000Z" + } + } + + Error Responses + Status: 404 Not Found + + { + "success": false, + "message": "Form not found" + } + + Status: 400 Bad Request + + { + "success": false, + "message": "Response already submitted and cannot be edited" + } +} diff --git a/bruno/form-response/getDraftResponse.bru b/bruno/form-response/getDraftResponse.bru new file mode 100644 index 0000000..13f1735 --- /dev/null +++ b/bruno/form-response/getDraftResponse.bru @@ -0,0 +1,76 @@ +meta { + name: getDraftResponse + type: http + seq: 5 +} + +get { + url: http://localhost:8000/responses/draft/:formId + body: none + auth: inherit +} + +params:path { + formId: 4a386821-772f-4928-b01f-034daf8dc456 +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + # Get Draft Response + + ## Endpoint + `GET /forms/:formId/draft` + + ## Description + Fetches the current user's saved draft response for a specific form (i.e., a response that is not yet submitted). + + Field IDs stored in the database are converted into human-readable field names before returning. + + ## Controller + `getDraftResponse` + + ## Auth Required + ✅ Yes (User must be logged in) + + ## Path Parameters + + | Name | Type | Required | Description | + |--------|--------|----------|-------------| + | formId | string | Yes | ID of the form | + + ## Request Body + None + + ## Responses + + ### Success Response + + #### Status: 200 OK + + ```json + { + "success": true, + "data": { + "id": "response_123", + "formId": "form_456", + "formTitle": "Job Application Form", + "answers": { + "Full Name": "John Doe", + "Email": "john@example.com", + "Experience": "2 years" + } + } + } + + Error Responses + Status: 404 Not Found + + { + "success": false, + "message": "No draft found" + } +} diff --git a/bruno/form-response/getFormResponsesForFormOwner.bru b/bruno/form-response/getFormResponsesForFormOwner.bru index 14739c4..c447527 100644 --- a/bruno/form-response/getFormResponsesForFormOwner.bru +++ b/bruno/form-response/getFormResponsesForFormOwner.bru @@ -1,7 +1,7 @@ meta { name: getFormResponsesForFormOwner type: http - seq: 2 + seq: 3 } get { @@ -11,7 +11,7 @@ get { } params:path { - formId: ef50e45d-d095-418d-ab49-7196900fa5c2 + formId: 4a386821-772f-4928-b01f-034daf8dc456 } settings { diff --git a/bruno/form-response/getSubmittedResponse.bru b/bruno/form-response/getSubmittedResponse.bru index 282f933..c8c7ff8 100644 --- a/bruno/form-response/getSubmittedResponse.bru +++ b/bruno/form-response/getSubmittedResponse.bru @@ -1,7 +1,7 @@ meta { name: getSubmittedResponse type: http - seq: 3 + seq: 4 } get { diff --git a/bruno/form-response/resumeResponse.bru b/bruno/form-response/resumeResponse.bru deleted file mode 100644 index 65e2948..0000000 --- a/bruno/form-response/resumeResponse.bru +++ /dev/null @@ -1,86 +0,0 @@ -meta { - name: resumeResponse - type: http - seq: 4 -} - -put { - url: http://localhost:8000/responses/resume/:formId - body: json - auth: inherit -} - -params:path { - formId: ef50e45d-d095-418d-ab49-7196900fa5c2 -} - -body:json { - { - "answers":{ - "575f5192-d46c-4974-bb0c-f909d9fb80ce": "Jack", - "894b4cf0-1320-4403-b116-8504f23ef6e3": "jack@gmail.com", - "15026215-127b-44bc-8a06-a12e33f79802":"3" - } - } -} - -settings { - encodeUrl: true - timeout: 0 -} - -docs { - ## Resume (Update) Response - - Updates an existing response. This is typically used for "Save as Draft" functionality or editing a submission if allowed. It strictly ensures the user updating the response is the one who created it. - - URL: /responses/:responseId - - Method: PATCH - - Auth Required: Yes - - Path Parameters - Parameter Type Description - responseId string (UUID) The unique ID of the response to update. - Request Body - Field Type Description - answers object The updated JSON object of answers. - - Sample Input: - ```JSON - - { - "answers": { - "field_123_uuid": "Jane Doe Updated", - "field_456_uuid": 26 - } - } - ``` - - Responses - ✅ 200 OK: Success - - Returns confirmation of the update. - ```JSON - - { - "success": true, - "message": "Response updated successfully", - "data": { - "count": 1 - } - } - ``` - - ⚠️ 200 OK (Logical Failure): Not Found or Unauthorized - - Occurs if the response ID does not exist, or if the response belongs to a different user. - ```JSON - - { - "success": false, - "message": "No response found to update" - } - ``` -} diff --git a/bruno/form-response/submitResponse.bru b/bruno/form-response/submitResponse.bru index c358d42..4d97f93 100644 --- a/bruno/form-response/submitResponse.bru +++ b/bruno/form-response/submitResponse.bru @@ -5,7 +5,7 @@ meta { } post { - url: http://localhost:8000/responses/:formId + url: http://localhost:8000/responses/submit/:formId body: json auth: inherit } @@ -30,72 +30,76 @@ settings { } docs { - ## Submit Response - Creates a new response entry for a specific form. This endpoint checks if the form exists and is published before accepting the submission. + # Submit Response - * **URL:** `/forms/:formId/responses` - * **Method:** `POST` - * **Auth Required:** Yes + ## Endpoint + `POST /forms/:formId/responses` - ### Path Parameters - | Parameter | Type | Description | - | :--- | :--- | :--- | - | `formId` | `string` (UUID) | The unique ID of the form being submitted. | + ## Description + Submits a user's final response for a form. If a draft exists, it is updated and marked as submitted. If already submitted, the request is rejected. - ### Request Body - | Field | Type | Description | - | :--- | :--- | :--- | - | `answers` | `object` | A JSON object where keys are **Field IDs** and values are the user's answers. | + ## Controller + `submitResponse` + + ## Auth Required + ✅ Yes (User must be logged in) + + ## Path Parameters + + | Name | Type | Required | Description | + |--------|--------|----------|-------------| + | formId | string | Yes | ID of the form to submit | + + ## Request Body - **Sample Input:** ```json { "answers": { - "field_123_uuid": "John Doe", - "field_456_uuid": 25, - "field_789_uuid": ["Option A", "Option B"] + "fieldId1": "Answer 1", + "fieldId2": "Answer 2" } } Responses - ✅ 200 OK: Success - - Returns the created response object. - JSON + Success Response + Status: 200 OK { "success": true, "message": "Response submitted successfully", "data": { - "id": "response_abc_123", - "formId": "form_xyz_789", - "respondentId": "user_555", + "id": "response_123", + "formId": "form_456", + "respondentId": "user_789", "answers": { - "field_123_uuid": "John Doe", - "field_456_uuid": 25, - "field_789_uuid": ["Option A", "Option B"] + "fieldId1": "Answer 1", + "fieldId2": "Answer 2" }, - "submittedAt": "2023-10-27T10:00:00.000Z" + "isSubmitted": true, + "submittedAt": "2026-02-07T10:30:00.000Z", + "updatedAt": "2026-02-07T10:30:00.000Z" } } - ❌ 403 Forbidden: Form Not Published + Error Responses + Status: 404 Not Found + + { + "success": false, + "message": "Form not found" + } - Occurs if the form exists but isPublished is false. - JSON + Status: 403 Forbidden { "success": false, "message": "Form is not published" } - ❌ 404 Not Found: Form Doesn't Exist - - Occurs if the formId provided is invalid. - JSON + Status: 400 Bad Request { "success": false, - "message": "Form not found" + "message": "You have already submitted this form" } }