Skip to content

Conversation

@shoexue
Copy link
Member

@shoexue shoexue commented Dec 24, 2025

Forms API – Summary (Create / Clone / Update / Delete)

This PR completes the admin-only Forms management APIs and corresponding service logic, with full unit test coverage for the service layer.


✅ Implemented Endpoints

CREATE Form (Admin only)

POST /api/seasons/{seasonCode}/forms

Creates a new form for a season, optionally with questions.

  • Verifies admin access via sessionToken
  • Inserts into form
  • Optionally inserts related formQuestion rows
  • Returns 201 Created

CLONE Form (Admin only)

POST /api/seasons/{seasonCode}/forms/{formId}/clone

Creates a new form by cloning an existing one.

  • Verifies admin access
  • Validates source form exists (404 if not)
  • Creates a new form row
  • Clones all questions with:
    • New formId
    • New formQuestionId via crypto.randomUUID()
  • Returns 201 Created

UPDATE Form (Admin only)

POST /api/seasons/{seasonCode}/forms/{formId}

Updates form metadata and optionally replaces questions.

  • Verifies admin access
  • Validates form exists (404 if not)
  • Updates openTime, closeTime, tags
  • Questions behavior:
    • If omitted → leave unchanged
    • If provided → delete existing questions, then insert new ones
  • Returns 200 OK

DELETE Form (Admin only)

DELETE /api/seasons/{seasonCode}/forms/{formId}

Deletes a form and all associated questions.

  • Verifies admin access
  • Validates form exists (404 if not)
  • Deletes in correct order:
    1. formQuestion
    2. form
  • Returns 204 No Content (intentional, no response body)

🧠 Service Layer (forms.service.ts)

  • createForm
  • cloneForm
  • updateForm
  • deleteForm

All database interactions:

  • Use Drizzle ORM
  • Properly handle foreign key constraints
  • Normalize optional fields (tags, questions)
  • Wrap DB errors via handleDbError

The logic is intentionally self-contained and mirrors API behavior closely, so no additional explanation is required beyond the routes.


🧪 Tests

  • 100% coverage for forms.service.ts
  • Covers:
    • Happy paths
    • 404 cases
    • Question replacement semantics
    • Deterministic UUIDs for cloning
  • Global coverage thresholds satisfied

ℹ️ Notes

  • Admin validation is centralized via isAdmin(userId)
  • HTTP semantics respected (e.g. DELETE → 204)
  • API routes documented via hono-openapi
  • Ready for frontend and further integration

This PR completes all admin-side Forms mutation endpoints.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of mocking your ORM. These should just be integration tests

});

it("returns 403 when user is not admin", async () => {
(isAdmin as Mock).mockResolvedValue(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are mocking isAdmin, which is creating an implementation requirement that the routes use isAdmin to check permissions.

However, this may not necessarily be true. Instead, you should inject the request context with an admin user to make this more resilient and require the test to know less about the implementation

@BlazingAsher
Copy link
Member

Also less about your code but your PR, I would shy away from using AI to generate the description. It's just a whole bunch of unnecessary detail that makes it annoying to read. Just include a summary of your changes, any big decisions you made (and why, if it's not obvious), and potential things you want a reviewer to focus on

Copy link
Contributor

@danielp1218 danielp1218 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall just clean up your code, the logic looks OK to me.

const params = c.req.valid("param");
const body = c.req.valid("json");

const admin = await isAdmin(body.sessionToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a note that this implementation is subject to change #4

if (input.questions.length > 0) {
await db.insert(formQuestion).values(
input.questions.map((q) => ({
formQuestionId: q.formQuestionId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the database will auto-generate IDs, we definitely don't want to supply them with data the client sends. you can just remove this line.

});
}

// Delete children first (avoid FK issues)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have cascading deletes set in our schema so this isn't needed

await db.insert(formQuestion).values(
srcQuestions.map((q) => ({
// generate a new id so we never collide if formQuestionId is globally unique
formQuestionId: crypto.randomUUID(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB will auto-generate, can get rid of this line

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It auto-generates formQuestionId or do you mean formId.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mean formQuestionId in the formQuestion table. We have it set up so every table’s surrogate key a.k.a the {table}Id will default to a random uuid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current schema it looks like only formId has a uuid default

questions?: CreateFormQuestionInput[];
}

export const createForm = async (input: CreateFormInput) => {
Copy link
Contributor

@danielp1218 danielp1218 Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you should wrap all the DB operations in this function within a transaction. (same with updateForm and cloneForm)
Conceptually, we want either all the DB operations to happen or all fail. It will result in wierd data integrity issues if a function fails halfway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants