Replace all z.any() output schemas with typed Zod schemas#402
Replace all z.any() output schemas with typed Zod schemas#402
Conversation
📝 WalkthroughWalkthroughThis PR systematically replaces unconstrained Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
47 tRPC procedures across 11 routers now have proper typed output schemas instead of z.any(). This enables runtime response validation, typed OpenAPI specs, and TypeScript client inference. Closes #329 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- health.ts: JobType is string enum, not number - settings.ts: use sideSchema instead of z.literal for db select results - BedTempMatrix: timestamp is Date, remove string cast - CurveEditor: type createPromises as Promise<unknown>[] - WaterLevelCard/WaterModal: fix field name mismatches (direction→trend, levelPercent→level, changePercent→lowPercent, alertType→type) - Tests: add null guards for nullable getByDay/find results Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- device.ts: add optional isAlarmVibrating to SideStatus output (present in WS stream, absent in HTTP — union type needs it) - useSchedules.ts: align local interfaces with typed API output (createdAt/updatedAt as Date, add missing fields) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6c4aee8 to
825ba64
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/server/routers/settings.ts (1)
53-124: Consider extracting shared settings payload schemas.The device and side response shapes are now duplicated across several procedures, so the next settings-field change will be easy to miss in one endpoint. Pulling them into named shared payload schemas would keep these contracts in sync.
Also applies to: 189-204, 300-311, 424-435
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/routers/settings.ts` around lines 53 - 124, The inline Zod objects for device, sides (left/right), and gesture items are duplicated; extract them into shared named schemas (e.g., deviceSettingsSchema, sideSettingsSchema, gestureItemSchema or gesturesSchema) and reuse them instead of repeating the same object literal in the .output call and the other repeated blocks referenced (the other occurrences around 189-204, 300-311, 424-435). Ensure the extracted schemas reference existing primitives like temperatureUnitSchema, sideSchema, and tapTypeSchema, then replace the inline z.object(...) definitions for device, sides.left/right, and gestures.left/right with the new named schemas so all endpoints share a single source of truth.src/server/routers/environment.ts (1)
31-43: Extract shared bed/freezer output schemas to avoid drift.The same object shapes are repeated across list/latest procedures. Centralizing them will reduce maintenance risk.
♻️ Refactor sketch
+const bedTempOutputSchema = z.object({ + id: z.number(), + timestamp: z.date(), + ambientTemp: z.number().nullable(), + mcuTemp: z.number().nullable(), + humidity: z.number().nullable(), + leftOuterTemp: z.number().nullable(), + leftCenterTemp: z.number().nullable(), + leftInnerTemp: z.number().nullable(), + rightOuterTemp: z.number().nullable(), + rightCenterTemp: z.number().nullable(), + rightInnerTemp: z.number().nullable(), +}) + +const freezerTempOutputSchema = z.object({ + id: z.number(), + timestamp: z.date(), + ambientTemp: z.number().nullable(), + heatsinkTemp: z.number().nullable(), + leftWaterTemp: z.number().nullable(), + rightWaterTemp: z.number().nullable(), +}) ... -.output(z.array(z.object({ ... }))) +.output(z.array(bedTempOutputSchema)) ... -.output(z.object({ ... }).nullable()) +.output(bedTempOutputSchema.nullable()) ... -.output(z.array(z.object({ ... }))) +.output(z.array(freezerTempOutputSchema)) ... -.output(z.object({ ... }).nullable()) +.output(freezerTempOutputSchema.nullable())Also applies to: 87-94, 133-145, 182-189
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/routers/environment.ts` around lines 31 - 43, Extract the repeated object shape into a shared Zod schema (e.g., const EnvironmentReadingSchema = z.object({...})) and replace the inline object literal in all .output(...) calls with EnvironmentReadingSchema (use z.array(EnvironmentReadingSchema) where arrays are expected and EnvironmentReadingSchema for single-object outputs). Update the usages in the environment router procedures (the list/latest outputs shown and the other occurrences around the file) to reference this constant so the schema is centralized and reused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/server/routers/biometrics.ts`:
- Around line 58-68: The sleep-record output schemas in
src/server/routers/biometrics.ts declare leftBedAt as z.date() but active
sessions allow null; update the schema(s) that define leftBedAt (the
array/object outputs around the .output(...) that include id, side,
enteredBedAt, leftBedAt, sleepDurationSeconds, etc.) to make leftBedAt nullable
(use z.date().nullable()) so runtime validation accepts active records; apply
the same change to the other occurrences of the sleep-record output schema
patterns (the similar blocks around the other two occurrences referenced).
In `@src/server/routers/health.ts`:
- Around line 36-54: The health "healthy" field is computed incorrectly because
jobCounts.total is derived from jobs.length, making the existing condition
always true; update the logic that sets the healthy field (the "healthy"
property generation around jobs and jobCounts.total) to treat the
scheduler-enabled-but-empty case as unhealthy by computing healthy as: when
enabled is true, healthy is true only if jobCounts.total > 0 (or jobs.length >
0); when enabled is false, healthy should remain true — i.e., replace the
current jobs.length > 0 || jobCounts.total === 0 expression with an expression
equivalent to enabled ? jobCounts.total > 0 : true so the
scheduler-enabled-but-empty scenario yields healthy = false.
In `@src/server/routers/runOnce.ts`:
- Around line 128-136: The output schema currently exposes setPoints as
z.unknown(), allowing arbitrary JSON; change the output definition to validate
setPoints against the existing setPointSchema array (use z.array(setPointSchema)
instead of z.unknown()) and, in the retrieval logic that returns this route's
payload (the code that constructs the object matching this output), run a safe
parse (e.g., setPointsSchemaArray.safeParse or parse with try/catch) and return
a typed fallback (an empty array or a well-defined default) when parsing fails
so malformed persisted payloads cannot leak through; update references to
setPointSchema and the output z.object(...) to enforce this contract.
---
Nitpick comments:
In `@src/server/routers/environment.ts`:
- Around line 31-43: Extract the repeated object shape into a shared Zod schema
(e.g., const EnvironmentReadingSchema = z.object({...})) and replace the inline
object literal in all .output(...) calls with EnvironmentReadingSchema (use
z.array(EnvironmentReadingSchema) where arrays are expected and
EnvironmentReadingSchema for single-object outputs). Update the usages in the
environment router procedures (the list/latest outputs shown and the other
occurrences around the file) to reference this constant so the schema is
centralized and reused.
In `@src/server/routers/settings.ts`:
- Around line 53-124: The inline Zod objects for device, sides (left/right), and
gesture items are duplicated; extract them into shared named schemas (e.g.,
deviceSettingsSchema, sideSettingsSchema, gestureItemSchema or gesturesSchema)
and reuse them instead of repeating the same object literal in the .output call
and the other repeated blocks referenced (the other occurrences around 189-204,
300-311, 424-435). Ensure the extracted schemas reference existing primitives
like temperatureUnitSchema, sideSchema, and tapTypeSchema, then replace the
inline z.object(...) definitions for device, sides.left/right, and
gestures.left/right with the new named schemas so all endpoints share a single
source of truth.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 269f9ec4-7983-43ff-ae3e-607617211024
📒 Files selected for processing (18)
src/components/Schedule/CurveEditor.tsxsrc/components/Sensors/BedTempMatrix.tsxsrc/components/status/WaterLevelCard.tsxsrc/components/status/WaterModal.tsxsrc/hooks/useSchedules.tssrc/server/routers/biometrics.tssrc/server/routers/calibration.tssrc/server/routers/device.tssrc/server/routers/environment.tssrc/server/routers/health.tssrc/server/routers/raw.tssrc/server/routers/runOnce.tssrc/server/routers/scheduleGroups.tssrc/server/routers/schedules.tssrc/server/routers/settings.tssrc/server/routers/tests/scheduleGroups.test.tssrc/server/routers/tests/schedules.test.tssrc/server/routers/waterLevel.ts
| .output(z.array(z.object({ | ||
| id: z.number(), | ||
| side: sideSchema, | ||
| enteredBedAt: z.date(), | ||
| leftBedAt: z.date(), | ||
| sleepDurationSeconds: z.number(), | ||
| timesExitedBed: z.number(), | ||
| presentIntervals: z.unknown(), | ||
| notPresentIntervals: z.unknown(), | ||
| createdAt: z.date(), | ||
| }))) |
There was a problem hiding this comment.
Allow leftBedAt to be nullable in sleep-record output schemas.
leftBedAt is treated as nullable for active sessions in this router (see Line 745 and Line 755), but these outputs require a non-null z.date(). That can fail runtime output validation for active records.
💡 Suggested fix
.output(z.array(z.object({
id: z.number(),
side: sideSchema,
enteredBedAt: z.date(),
- leftBedAt: z.date(),
+ leftBedAt: z.date().nullable(),
sleepDurationSeconds: z.number(),
timesExitedBed: z.number(),
presentIntervals: z.unknown(),
notPresentIntervals: z.unknown(),
createdAt: z.date(),
}))) .output(z.object({
id: z.number(),
side: sideSchema,
enteredBedAt: z.date(),
- leftBedAt: z.date(),
+ leftBedAt: z.date().nullable(),
sleepDurationSeconds: z.number(),
timesExitedBed: z.number(),
presentIntervals: z.unknown(),
notPresentIntervals: z.unknown(),
createdAt: z.date(),
}).nullable()) .output(z.object({
id: z.number(),
side: sideSchema,
enteredBedAt: z.date(),
- leftBedAt: z.date(),
+ leftBedAt: z.date().nullable(),
sleepDurationSeconds: z.number(),
timesExitedBed: z.number(),
presentIntervals: z.unknown(),
notPresentIntervals: z.unknown(),
createdAt: z.date(),
}))Also applies to: 310-320, 572-582
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/routers/biometrics.ts` around lines 58 - 68, The sleep-record
output schemas in src/server/routers/biometrics.ts declare leftBedAt as z.date()
but active sessions allow null; update the schema(s) that define leftBedAt (the
array/object outputs around the .output(...) that include id, side,
enteredBedAt, leftBedAt, sleepDurationSeconds, etc.) to make leftBedAt nullable
(use z.date().nullable()) so runtime validation accepts active records; apply
the same change to the other occurrences of the sleep-record output schema
patterns (the similar blocks around the other two occurrences referenced).
| .output(z.object({ | ||
| enabled: z.boolean(), | ||
| jobCounts: z.object({ | ||
| temperature: z.number(), | ||
| powerOn: z.number(), | ||
| powerOff: z.number(), | ||
| alarm: z.number(), | ||
| prime: z.number(), | ||
| reboot: z.number(), | ||
| total: z.number(), | ||
| }), | ||
| upcomingJobs: z.array(z.object({ | ||
| id: z.string(), | ||
| type: z.string(), | ||
| side: z.string().optional(), | ||
| nextRun: z.string().nullable(), | ||
| })), | ||
| healthy: z.boolean(), | ||
| })) |
There was a problem hiding this comment.
healthy is currently always true.
The implementation for this field uses jobs.length > 0 || jobCounts.total === 0, but jobCounts.total is initialized from jobs.length, so one side of the || is always true. This endpoint will miss the exact “scheduler enabled but empty” failure case the comment describes.
Suggested fix
return {
enabled: scheduler.isEnabled(),
jobCounts,
upcomingJobs,
- healthy: jobs.length > 0 || jobCounts.total === 0, // Healthy if has jobs or expected to have none
+ healthy: !scheduler.isEnabled() || jobs.length > 0,
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/routers/health.ts` around lines 36 - 54, The health "healthy"
field is computed incorrectly because jobCounts.total is derived from
jobs.length, making the existing condition always true; update the logic that
sets the healthy field (the "healthy" property generation around jobs and
jobCounts.total) to treat the scheduler-enabled-but-empty case as unhealthy by
computing healthy as: when enabled is true, healthy is true only if
jobCounts.total > 0 (or jobs.length > 0); when enabled is false, healthy should
remain true — i.e., replace the current jobs.length > 0 || jobCounts.total === 0
expression with an expression equivalent to enabled ? jobCounts.total > 0 : true
so the scheduler-enabled-but-empty scenario yields healthy = false.
| .output(z.object({ | ||
| id: z.number(), | ||
| side: z.enum(['left', 'right']), | ||
| setPoints: z.unknown(), | ||
| wakeTime: z.string(), | ||
| startedAt: z.number(), | ||
| expiresAt: z.number(), | ||
| status: z.enum(['active', 'completed', 'cancelled']), | ||
| }).nullable()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "getActive|setPoints" src/server/routers/runOnce.ts -C3
rg -n "runOnce\\.getActive|setPoints\\." --type=ts --type=tsx src -C2Repository: sleepypod/core
Length of output: 2135
🏁 Script executed:
#!/bin/bash
# Check for setPointSchema definition/import in runOnce.ts
head -50 src/server/routers/runOnce.ts | cat -n
# Search for setPointSchema definition anywhere in the file
rg -n "setPointSchema|export.*setPointSchema" src/server/routers/runOnce.ts
# Check for imports at the top of the file
rg -n "^import" src/server/routers/runOnce.ts | head -20Repository: sleepypod/core
Length of output: 3047
🏁 Script executed:
#!/bin/bash
# Search for usages of getActive in the codebase
rg -n "\.getActive\(" --type=ts src -C2
# Also check for getActive in tests or other reference patterns
rg -n "getActive" --type=ts src -C1Repository: sleepypod/core
Length of output: 297
🏁 Script executed:
#!/bin/bash
# Check the complete context around the getActive procedure and its return handling
sed -n '125,170p' src/server/routers/runOnce.ts | cat -nRepository: sleepypod/core
Length of output: 1744
🏁 Script executed:
#!/bin/bash
# Look for any client or type imports from runOnce router
rg -n "runOnce|from.*runOnce" --type=ts src -C1 | head -40
# Check if there are any test files
fd "runOnce" src --type=ts | grep -i testRepository: sleepypod/core
Length of output: 2494
🏁 Script executed:
#!/bin/bash
# Look for any trpc client generation or type inference that might reference getActive
rg -n "getActive|inferProcedureOutput" --type=ts src -C2Repository: sleepypod/core
Length of output: 475
🏁 Script executed:
#!/bin/bash
# Final check: search for any TRPC client calls to runOnce.getActive
rg -n "runOnce\.getActive|\.getActive\(" src --type-list | head -20Repository: sleepypod/core
Length of output: 447
🏁 Script executed:
#!/bin/bash
# Check the exact return statement one more time to ensure fix applies correctly
sed -n '158,162p' src/server/routers/runOnce.ts | cat -nRepository: sleepypod/core
Length of output: 214
Validate setPoints shape instead of exposing arbitrary JSON.
Line 131 still allows any structure with z.unknown(), so malformed persisted payloads can bypass the runtime contract. The input validates setPoints as z.array(setPointSchema), but the output doesn't. Parse and validate against setPointSchema[] on retrieval, returning a typed fallback on parse failure.
Proposed fix
.output(z.object({
id: z.number(),
side: z.enum(['left', 'right']),
- setPoints: z.unknown(),
+ setPoints: z.array(setPointSchema),
wakeTime: z.string(),
startedAt: z.number(),
expiresAt: z.number(),
status: z.enum(['active', 'completed', 'cancelled']),
}).nullable())
.query(async ({ input }) => {
// ...
- let setPoints: unknown = []
+ let setPoints: z.infer<typeof setPointSchema>[] = []
try {
- setPoints = JSON.parse(session.setPoints)
+ const parsed = JSON.parse(session.setPoints)
+ const validated = z.array(setPointSchema).safeParse(parsed)
+ if (validated.success) setPoints = validated.data
}
catch {
// malformed — return empty
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/routers/runOnce.ts` around lines 128 - 136, The output schema
currently exposes setPoints as z.unknown(), allowing arbitrary JSON; change the
output definition to validate setPoints against the existing setPointSchema
array (use z.array(setPointSchema) instead of z.unknown()) and, in the retrieval
logic that returns this route's payload (the code that constructs the object
matching this output), run a safe parse (e.g., setPointsSchemaArray.safeParse or
parse with try/catch) and return a typed fallback (an empty array or a
well-defined default) when parsing fails so malformed persisted payloads cannot
leak through; update references to setPointSchema and the output z.object(...)
to enforce this contract.
Summary
z.any()output schemas across 11 tRPC routers with proper typed Zod schemasdirection→trend,levelPercent→level,alertType→type) — these were bugs hidden byz.any()stringcast onDatetimestampisAlarmVibratingto SideStatus for WS/HTTP union compatibilityCloses #329
Test plan
{}🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Chores