You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
What this accomplishes: The table needs a compact row per applicant. Including just these fields keeps the response small even when listing 1000+ applicants.
The endpoint returns { rows: ApplicantSummary[], total: number, page: number, pageSize: number }.
What this accomplishes:total enables pagination controls. page and pageSize echo back so the client knows what it actually received.
What this accomplishes: The frontend filter UI already sets these as URL params. The endpoint just needs to honor them.
Validate each: page is positive int, pageSize within bounds, sortBy is one of an allow-list of fields, sortDir is asc or desc, status/decision/rsvp are valid enum values or undefined.
What this accomplishes: Prevents bad input from generating malformed Mongo queries or DoS (a pageSize=10000000 request).
On invalid input, return 400 with a specific error.
Implement buildApplicantQuery
In src/lib/applicants/queries.ts, write buildApplicantQuery(filters: ApplicantFilters): Filter<ApplicantDoc>.
What this accomplishes: Translates filter inputs into a Mongo filter object. One function, easy to test.
If filters.status is set, add { applicationStatus: filters.status }. Same for decisionStatus and rsvpStatus.
If filters.search is set, build a case-insensitive regex matching the email field OR the name field. Use { $or: [{ email: regex }, { 'applicationResponses.name': regex }] }.
What this accomplishes: Admins can find an applicant by typing part of their name or email. Case-insensitive so capitalization doesn't matter.
Combine all conditions with $and (or just merge them — Mongo treats top-level fields as implicit AND).
What this accomplishes: Filters compose correctly when multiple are set.
Call buildApplicantQuery(filters) to get the Mongo filter.
Run two parallel queries with Promise.all: applicant_data.countDocuments(filter) for total, and applicant_data.find(filter).sort({ [sort.by]: sort.dir === 'asc' ? 1 : -1 }).skip((page - 1) * pageSize).limit(pageSize).toArray() for rows.
What this accomplishes: Parallel queries mean the endpoint is fast even on large collections. Total count is needed for pagination; rows are the actual page.
Project the row results down to the ApplicantSummary fields. Extract name from applicationResponses.name (or whichever question ID holds it).
What this accomplishes: Avoids sending the entire application payload over the wire when only a summary is needed. Significant bandwidth savings at scale.
Add a comment // TODO: gate with requireAdmin() once Ticket 1 ships its helpers.
Index the collection
Add Mongo indexes on applicationStatus, decisionStatus, rsvpStatus, appSubmissionTime, and email.
What this accomplishes: Without indexes, the filter and sort operations do collection scans. With them, response time stays in single-digit milliseconds even on tens of thousands of records.
Document the indexes in a comment in src/lib/applicants/service.ts or in scripts/setup-indexes.ts if such a file exists.
What this accomplishes: Future devs setting up a new environment know which indexes are required.
Test via curl
Seed the database with ~20 applicants in various statuses (use or extend scripts/seed.ts).
curl localhost:3000/api/v1/applicants returns the first 25 (or 20, since you only have 20) sorted by submission time descending.
curl localhost:3000/api/v1/applicants?status=submitted returns only submitted applicants.
Goal
GET /api/v1/applicantsreturns a real paginated, filtered list of applicants from Mongo. The admin applicants table page now shows real data.Tasks
Define the response shape
ApplicantSummaryincludes:_id,email,name(from responses),applicationStatus,decisionStatus,rsvpStatus,appSubmissionTime,lastSavedAt.{ rows: ApplicantSummary[], total: number, page: number, pageSize: number }.totalenables pagination controls.pageandpageSizeecho back so the client knows what it actually received.Parse query parameters
page(default 1),pageSize(default 25, max 100),sortBy(defaultappSubmissionTime),sortDir(defaultdesc),status,decision,rsvp,search.ascordesc, status/decision/rsvp are valid enum values or undefined.pageSize=10000000request).Implement
buildApplicantQuerysrc/lib/applicants/queries.ts, writebuildApplicantQuery(filters: ApplicantFilters): Filter<ApplicantDoc>.filters.statusis set, add{ applicationStatus: filters.status }. Same fordecisionStatusandrsvpStatus.filters.searchis set, build a case-insensitive regex matching the email field OR the name field. Use{ $or: [{ email: regex }, { 'applicationResponses.name': regex }] }.$and(or just merge them — Mongo treats top-level fields as implicit AND).Implement
listApplicantssrc/lib/applicants/service.ts, writelistApplicants({ filters, sort, page, pageSize }): Promise<{ rows, total, page, pageSize }>.buildApplicantQuery(filters)to get the Mongo filter.Promise.all:applicant_data.countDocuments(filter)for total, andapplicant_data.find(filter).sort({ [sort.by]: sort.dir === 'asc' ? 1 : -1 }).skip((page - 1) * pageSize).limit(pageSize).toArray()for rows.ApplicantSummaryfields. ExtractnamefromapplicationResponses.name(or whichever question ID holds it).Wire the API endpoint
src/app/api/v1/applicants/route.ts. ImplementGET: parse and validate query params, calllistApplicants, return JSON.// TODO: gate with requireAdmin() once Ticket 1 ships its helpers.Index the collection
applicationStatus,decisionStatus,rsvpStatus,appSubmissionTime, andemail.src/lib/applicants/service.tsor inscripts/setup-indexes.tsif such a file exists.Test via curl
scripts/seed.ts).curl localhost:3000/api/v1/applicantsreturns the first 25 (or 20, since you only have 20) sorted by submission time descending.curl localhost:3000/api/v1/applicants?status=submittedreturns only submitted applicants.curl localhost:3000/api/v1/applicants?status=submitted&decision=admittedcombines filters correctly.curl localhost:3000/api/v1/applicants?search=alicereturns applicants whose name or email contains "alice."curl localhost:3000/api/v1/applicants?page=2&pageSize=5returns rows 6-10.curl localhost:3000/api/v1/applicants?sortBy=email&sortDir=ascreturns rows sorted by email A-Z.Test from the admin page
/admin/applicants, change filters in the UI, change page, change sort. Confirm each interaction returns the right data.Unit tests
buildApplicantQueryproduces the expected Mongo filter for each combination: status only, decision only, search only, all three, none.page=0is rejected at the validation step.pageSize=999999is clamped or rejected.Definition of done
/admin/applicantsloads real data from Mongo, paginated and filterable.