Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bruno/forms/createForm.bru
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ settings {
}

docs {
# 2. Create Form
# Create Form

Creates a new empty form container.

Expand Down
2 changes: 1 addition & 1 deletion bruno/forms/deleteForm.bru
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ settings {
}

docs {
# 5. Delete Form
# Delete Form

Permanently removes a specific form. This endpoint ensures security by only allowing the **owner** of the form to delete it. If the form does not exist or belongs to another user, no action is taken, and a 404 error is returned.

Expand Down
2 changes: 1 addition & 1 deletion bruno/forms/getAllForms.bru
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ settings {
}

docs {
# 1. Get All Forms
# Get All Forms

Retrieves a list of all forms created by the currently authenticated user. This endpoint provides a summary view, returning key metadata like the title and publication status, but not the detailed form fields.

Expand Down
78 changes: 54 additions & 24 deletions bruno/forms/getFormById.bru
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,75 @@ settings {
}

docs {
# 3. Get Form By ID
# Get Form By ID

Retrieves the details of a specific form. This endpoint is scoped to the **owner**, meaning a user can only fetch forms they created. If a form exists but belongs to another user, it will return a 404 error.
## Endpoint
`GET /forms/:formId`

* **URL:** `/forms/:formId`
* **Method:** `GET`
* **Auth Required:** Yes
## Description
Fetches a form owned by the authenticated user along with its fields in order.

### Path Parameters
## Controller
`getFormById`

| Parameter | Type | Description |
| :--- | :--- | :--- |
| `formId` | `string` (UUID) | The unique ID of the form to retrieve. |
## Auth Required
✅ Yes (User must be the form owner)

### Responses
## Path Parameters

#### ✅ 200 OK: Success
| Name | Type | Required | Description |
|--------|--------|----------|-------------|
| formId | string | Yes | ID of the form |

Returns the full form object.
## Request Body
None

## Responses

### Success Response (Form with Fields)

#### Status: 200 OK

```json
{
"success": true,
"message": "Form fetched successfully",
"data": {
"id": "form_uuid_123",
"title": "Customer Feedback Survey",
"description": "A survey to collect customer opinions.",
"isPublished": true,
"ownerId": "user_uuid_555",
"createdAt": "2023-10-27T10:00:00.000Z",
"updatedAt": "2023-10-27T12:00:00.000Z"
}
"form": {
"id": "form_456",
"title": "Job Application",
"description": "Apply for internship",
"isPublished": false,
"createdAt": "2026-02-07T10:30:00.000Z"
},
"fields": [
{
"id": "field_1",
"formId": "form_456",
"fieldName": "Full Name",
"fieldType": "text",
"prevFieldId": null
},
{
"id": "field_2",
"formId": "form_456",
"fieldName": "Email",
"fieldType": "email",
"prevFieldId": "field_1"
}
]
}

❌ 404 Not Found
Success Response (No Fields Found)
Status: 200 OK

{
"success": true,
"message": "No forms fields found",
"data": []
}

Occurs if the formId does not exist OR if the form belongs to a different user.
JSON
Error Responses
Status: 404 Not Found

{
"success": false,
Expand Down
2 changes: 1 addition & 1 deletion bruno/forms/publishForm.bru
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ settings {
}

docs {
# 6. Publish Form
# Publish Form

Changes the status of a form to **Published**. Once published, the form becomes accessible to respondents for submission.

Expand Down
2 changes: 1 addition & 1 deletion bruno/forms/unPublishForm.bru
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ settings {
}

docs {
# 7. Unpublish Form
# Unpublish Form

Reverts the status of a form to **Unpublished**. This hides the form from the public, preventing any new submissions until it is published again.

Expand Down
37 changes: 36 additions & 1 deletion src/api/forms/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) {
id: params.formId,
ownerId: user.id,
},
select: {
id: true,
title: true,
description: true,
isPublished: true,
createdAt: true,
},
});

if (!form) {
Expand All @@ -73,11 +80,39 @@ export async function getFormById({ user, params, set }: GetFormByIdContext) {
};
}

const fields = await prisma.formFields.findMany({
where: { formId: params.formId },
});

if (fields.length === 0) {
logger.info(`No fields found for formId: ${params.formId}`);
return {
success: true,
message: "No forms fields found",
data: [],
};
}

const ordered: typeof fields = [];

let current = fields.find(
(f): f is (typeof fields)[number] => f.prevFieldId === null,
);

while (current) {
ordered.push(current);

current = fields.find(
(f): f is (typeof fields)[number] => f.prevFieldId === current!.id,
);
}

logger.info("Fetched form for user", { userId: user.id, formId: form.id });
return {
success: true,
message: "Form fetched successfully",
data: form,
form: form,
fields: ordered,
};
}

Expand Down
72 changes: 60 additions & 12 deletions src/test/forms.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { beforeEach, describe, expect, it, mock } from "bun:test";

// ---------- MOCK PRISMA ----------
const findManyMock = mock();
const formFindManyMock = mock();
const formFieldsFindManyMock = mock();
const createMock = mock();
const findFirstMock = mock();
const updateMock = mock();
Expand All @@ -10,12 +11,15 @@ const deleteManyMock = mock();
mock.module("../db/prisma", () => ({
prisma: {
form: {
findMany: findManyMock,
findMany: formFindManyMock,
create: createMock,
findFirst: findFirstMock,
update: updateMock,
deleteMany: deleteManyMock,
},
formFields: {
findMany: formFieldsFindManyMock,
},
},
}));

Expand All @@ -37,7 +41,8 @@ const { getAllForms, createForm, getFormById, updateForm, deleteForm } =

describe("Forms Controller Tests", () => {
beforeEach(() => {
findManyMock.mockReset();
formFindManyMock.mockReset();
formFieldsFindManyMock.mockReset();
createMock.mockReset();
findFirstMock.mockReset();
updateMock.mockReset();
Expand All @@ -51,7 +56,7 @@ describe("Forms Controller Tests", () => {
// ===== getAllForms =====

it("getAllForms → success", async () => {
findManyMock.mockResolvedValue([
formFindManyMock.mockResolvedValue([
{ id: "1", title: "A", isPublished: true, createdAt: new Date() },
]);

Expand All @@ -62,7 +67,7 @@ describe("Forms Controller Tests", () => {
});

it("getAllForms → empty", async () => {
findManyMock.mockResolvedValue([]);
formFindManyMock.mockResolvedValue([]);

const res = await getAllForms({ user } as any);

Expand All @@ -71,7 +76,7 @@ describe("Forms Controller Tests", () => {
});

it("getAllForms → DB error", async () => {
findManyMock.mockRejectedValue(new Error("DB fail"));
formFindManyMock.mockRejectedValue(new Error("DB fail"));

expect(getAllForms({ user } as any)).rejects.toThrow();
});
Expand Down Expand Up @@ -113,26 +118,69 @@ describe("Forms Controller Tests", () => {

// ===== getFormById =====

it("getFormById → found", async () => {
findFirstMock.mockResolvedValue({ id: "1" });
it("getFormById → found with ordered fields", async () => {
findFirstMock.mockResolvedValue({
id: "1",
title: "Test Form",
description: "Desc",
isPublished: false,
createdAt: new Date(),
});

formFieldsFindManyMock.mockResolvedValue([
{ id: "f1", formId: "1", prevFieldId: null },
{ id: "f2", formId: "1", prevFieldId: "f1" },
]);

const set: any = {};

const res = await getFormById({
user,
params: { id: "1" },
params: { formId: "1" },
set,
} as any);

const result: any = res;

expect(result.success).toBe(true);
expect(result.form.id).toBe("1");
expect(result.fields.length).toBe(2);
expect(result.fields[0].id).toBe("f1");
expect(result.fields[1].id).toBe("f2");
});

it("getFormById → no fields", async () => {
findFirstMock.mockResolvedValue({
id: "1",
title: "Test Form",
description: "Desc",
isPublished: false,
createdAt: new Date(),
});

formFieldsFindManyMock.mockResolvedValue([]);

const set: any = {};

const res = await getFormById({
user,
params: { formId: "1" },
set,
} as any);

expect(res.success).toBe(true);
expect(res.data).toEqual([]);
expect(res.message).toBe("No forms fields found");
});

it("getFormById → not found", async () => {
findFirstMock.mockResolvedValue(null);

const set: any = {};

const res = await getFormById({
user,
params: { id: "2" },
params: { formId: "2" },
set,
} as any);

Expand All @@ -145,10 +193,10 @@ describe("Forms Controller Tests", () => {

const set: any = {};

expect(
await expect(
getFormById({
user,
params: { id: "1" },
params: { formId: "1" },
set,
} as any),
).rejects.toThrow();
Expand Down