feat: add v2 TypeScript water intake routes#1073
feat: add v2 TypeScript water intake routes#1073Soulplayer wants to merge 22 commits intoCodeWithCJ:mainfrom
Conversation
Port water intake endpoints to /api/v2/measurements/water-intake using TypeScript and existing Zod schemas from measurementSchemas.ts. Follows the established v2 handler pattern. Also fixes Express route ordering so /entry/:id is registered before /:date. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Add .default when require()-ing the ES module default export so app.use() receives the actual middleware function - Replace req.authenticatedUserId with req.originalUserId in permission checks for consistency with the rest of the codebase - Guard userId query param with Array.isArray to handle duplicate params Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Standardise param validation error format to use flatten().fieldErrors across all handlers (was previously using issues.map().join()) - Catch 'Water intake entry not found.' in update and delete handlers so missing entries return 404 instead of falling through to 500 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Feat/v2 water intake routes clean
nullableLegacyNumber accepts null but rejects undefined, forcing clients to explicitly send container_id: null. Using nullableOptionalLegacyNumber allows omitting the field entirely, which is the expected behaviour for callers that have no container preference. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
fix: make container_id optional in UpsertWaterIntakeBodySchema
getWaterIntakeEntryById, updateWaterIntake, and deleteWaterIntake all called repository functions without passing userId, causing the RLS client to throw "userId is required". Also fixes updateWaterIntake passing updateData as the actingUserId arg to the repository. These are pre-existing bugs in measurementService.js exposed by the new v2 routes (the v1 routes hit the same service but the missing arg was silently swallowed in some code paths). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
fix: pass userId to repository calls in water intake service functions
There was a problem hiding this comment.
Code Review
This pull request introduces a new set of v2 routes for managing water intake, including CRUD operations, Swagger documentation, and Zod validation. It also updates the measurementService to pass user context to repository methods and adjusts the water intake schema to make the container ID optional. Feedback focuses on potential issues with family access and impersonation; specifically, the service layer's ownership checks may incorrectly filter out entries belonging to other family members, and the lack of standard impersonation middleware leads to redundant manual permission checks and auditing inaccuracies.
| const entryOwnerId = await measurementRepository.getWaterIntakeEntryOwnerId( | ||
| id, | ||
| authenticatedUserId | ||
| ); |
There was a problem hiding this comment.
The call to getWaterIntakeEntryOwnerId passing authenticatedUserId will cause a 404 error when a family member (e.g., a parent) tries to access an entry belonging to another user (e.g., a child). This is because the repository function getWaterIntakeEntryOwnerId explicitly filters the query by the provided userId. To support family access, the service should be able to retrieve the owner ID regardless of the requester's ID and then verify permissions.
|
|
||
| const router = express.Router(); | ||
|
|
||
| router.use(checkPermissionMiddleware('checkin')); |
There was a problem hiding this comment.
Consider adding onBehalfOfMiddleware to this router. This middleware is typically used in v2 routes to handle impersonation by setting req.userId to the target user while keeping the original user in req.originalUserId. Without it, the service layer's ownership checks will fail for family members or admins trying to manage water intake for other users.
| const updatedEntry = await measurementRepository.updateWaterIntake( | ||
| id, | ||
| authenticatedUserId, | ||
| authenticatedUserId, | ||
| updateData | ||
| ); |
There was a problem hiding this comment.
While this change correctly fixes the parameter order for the updateWaterIntake repository call, passing authenticatedUserId as both the owner and the actor prevents accurate auditing when impersonation is used. The actingUserId (the actual logged-in user) should be passed as the third argument. Consider updating the service function signature to accept actingUserId as well.
| const userIdQuery = req.query.userId; | ||
| const userId = Array.isArray(userIdQuery) ? userIdQuery[0] : userIdQuery; | ||
| const targetUserId = typeof userId === 'string' ? userId : req.userId; |
There was a problem hiding this comment.
The manual extraction of userId from query parameters and the subsequent permission check are redundant if onBehalfOfMiddleware is used. If you decide not to use the middleware, ensure this pattern is consistently applied to the GET /entry/:id, PUT, and DELETE routes as well, as they currently lack family access support.
Replace manual ?userId= query param extraction and canAccessUserData permission checks with onBehalfOfMiddleware. The middleware reads the X-On-Behalf-Of-User-Id header, verifies family access permission, and rewrites req.userId to the target user so all downstream service calls work correctly without extra plumbing. Removes canAccessUserData import — no longer needed in the route layer. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Going to test the middleware now on my test server. |
After onBehalfOfMiddleware runs, req.userId is the target user and req.originalUserId is the real requester. The first arg to getWaterIntake (authenticatedUserId) is used in error logging to identify the actor, so it should be req.originalUserId || req.userId — not req.userId twice. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
fix: use onBehalfOfMiddleware for family access in water intake routes
Testing & Review ResponseAll review feedback has been addressed. Here's a summary of what was changed and how it was tested. Changes made after review
Console testing (browser DevTools)Tested against live environment after each redeploy. GET by date POST upsert GET entry by ID PUT update DELETE 404 on non-existent entry Family access — unauthorized user ( The 403 confirms
|
Adds GET, POST, PUT, DELETE endpoints at /api/v2/goal-presets following the established v2 route pattern with Zod validation, named RequestHandler typed functions, and consistent error shapes. - schemas/goalPresetSchemas.ts: CreateGoalPresetBodySchema and UpdateGoalPresetBodySchema covering all 30+ DB columns; Update is .partial() of Create to avoid duplication - routes/v2/goalPresetRoutes.ts: 5 handlers with safeParse validation and null-check 404 (service returns null, not throw, for missing records) - SparkyFitnessServer.js: mount at /api/v2/goal-presets alongside other v2 routes Legacy /api/goal-presets route is untouched and continues to work. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The goalPresetRepository.updateGoalPreset performs a full overwrite of all 31 columns unconditionally. Using .partial() on the schema allowed clients to omit fields, which would write NULL to the DB (violating the preset_name NOT NULL constraint) and produce NaN when macro-gram calculations ran without a calories value. PUT semantics match the repository's full-replacement SQL: the client loads the existing preset, edits what it needs, and sends everything back. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
feat: add v2 TypeScript routes for goal presets
Using !== null (strict) caused the condition to fire when percentage fields were absent (undefined), because undefined !== null is true. This overwrote explicitly-set protein/carbs/fat with NaN, which PostgreSQL stored as null. Changing to != null (loose) correctly treats both null and undefined as "not provided", so the calculation only runs when all three percentages are actually present. Affects both createGoalPreset and updateGoalPreset. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
fix: use loose null check in percentage-to-grams calculation
If calories is null or undefined while percentage fields are present, the multiplication produces NaN. Adding calories != null to the condition ensures the calculation only runs when all four inputs are available. Applies to both createGoalPreset and updateGoalPreset. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The goal_presets_unique_name_per_user constraint prevents duplicate names per user. Previously this surfaced as a generic 500. Now the service detects pg error code 23505 and throws a descriptive error, and the route handler returns 409 Conflict with a clear message. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Feat/v2 goal preset routes
|
Looks very good, but it's still missing tests. It's best to add them now so we can prevent future regressions. |
|
@Soulplayer just checking if you will be adding the validation scripts that @Sim-sat suggested |
ba79792 to
ead1ba8
Compare
Test ResultsI ran the full server test suite ( Summary: 276 passed, 8 failed — the 8 failures are pre-existing and unrelated to this PR (hardcoded dev path in I also added a dedicated route-level test file for the new endpoints: Water intake routes: 18/18 passed ✅ Run with: |
|
I'm only getting one failed test for the server right now. Have you pulled the latest changes from GitHub into your branch? If not, look into Thanks for adding the tests. This is a great start. I think we'll want a bit more code coverage though. Rather than testing the route handler itself, we want to test the core logic. A couple tests I'd suggest:
If you need a hand with any of these, just shout. Really appreciate you taking this on. Having well tested v2 routes in typescript will go a long way towards catching bugs before they get to users. |
f4414d3 to
204797e
Compare
|
I used your comment to go through the tests. I think it is okay for now. 1. Test the schema change: verify that container_id can be omitted
2. Measurement service test: verify that the userId is actually passed to the repository layer
3. Test the core logic rather than just route handlers
📊 Test CoverageNew Test File:
Overall: 33/33 tests passing (15 new + 18 existing route tests) 🔧 Bug Fixes
🎯 Key Benefits
|
Tip
Help us review and merge your PR faster!
Please ensure you have completed the Checklist below.
For Frontend changes, please run
pnpm run validateto check for any errors.PRs that include tests and clear screenshots are highly preferred!
Description
Adds v2 TypeScript endpoints for water intake under
/api/v2/measurements/water-intake, following the pattern established byroutes/v2/exerciseEntryRoutes.tsandroutes/v2/foodRoutes.ts. The new routes reuse existing Zod schemas fromschemas/measurementSchemas.tsand run alongside the v1 routes without breaking anything.Live testing also uncovered and fixed three pre-existing bugs in
measurementService.js(missinguserIdarguments on repository calls causing RLS errors) and one inmeasurementSchemas.ts(container_idrejecting absent values instead of treating them as optional).New endpoints
GET/api/v2/measurements/water-intake/:date?userId=for family access)GET/api/v2/measurements/water-intake/entry/:idPOST/api/v2/measurements/water-intakePUT/api/v2/measurements/water-intake/:idDELETE/api/v2/measurements/water-intake/:idBugs fixed
getWaterIntakeEntryById,updateWaterIntake, anddeleteWaterIntakeinmeasurementService.jscalled repository functions without passinguserId, causing"userId is required for getClient to ensure RLS is applied."500 errorsupdateWaterIntakepassedupdateDataas theactingUserIdargument to the repository (wrong parameter position), meaning updates silently wrote nothingcontainer_idinUpsertWaterIntakeBodySchemachanged fromnullableLegacyNumbertonullableOptionalLegacyNumberso clients can omit it rather than being forced to sendnullAlso improved over v1
GET /water-intake/entry/:idregistered beforeGET /water-intake/:dateto prevent Express matching the literal"entry"segment as a date paramArray.isArrayguard on?userId=query param to handle duplicate query string values{ error, details }validation error format across all handlersRelated Issue
PR type: [ ] New Feature
Checklist
pnpm run typecheckpasses; 18/18 new route tests + 276 existing server tests pass.RequestHandlertyped handlers,safeParsevalidation,checkPermissionMiddlewareat router level)water_intakeapplies.rls_policies.sqlnot changed.Screenshots (if applicable)
All 5 endpoints verified via browser DevTools console against the live test environment:
GET by date
POST upsert
GET entry by ID
PUT update
DELETE
404 on non-existent ID (regression test for the service bug fix)