fix(openapi): declare what the measurement list and create answers carry

The sleep list rows of GET /api/measurements carry the main night's stage
minutes, time in bed, time awake and awakenings, and the aggregate buckets
carry the bucket's lowest and highest reading; none of these were in the
contract. The create answer's union tried the plain row before the named
sleep-stage result, so a contract check read a named-stage answer as a plain
row and dropped its status. The responses are unchanged; the contract now
says what they hold.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 11:47:08 +02:00
1 parent 899b86f4d4
commit f7051ee15f
2 files changed
+138 -2

No files matched your search

+49 -1
View File
@@ -356,6 +356,36 @@ export const measurementResource = z
.nonnegative()
.optional()
.describe("Time asleep across those naps, in minutes."),
sleepStages: z
.record(z.string(), z.number().nonnegative())
.optional()
.describe(
"The main night's minutes per stage (`IN_BED`, `ASLEEP`, `AWAKE`, `CORE`, `DEEP`, `REM`), each the union of that stage's spans; empty on a day of naps alone.",
),
inBedMinutes: z
.number()
.nonnegative()
.nullable()
.optional()
.describe(
"The main night's time in bed in minutes, or null when no in-bed span was recorded.",
),
awakeMinutes: z
.number()
.nonnegative()
.nullable()
.optional()
.describe(
"The main night's time awake in bed in minutes, or null when no awake span was recorded.",
),
awakenings: z
.number()
.int()
.nonnegative()
.optional()
.describe(
"Awake spans between the main night's first and last time asleep.",
),
})
.meta({
id: "MeasurementResource",
@@ -388,6 +418,20 @@ const aggregatedMeasurementBucket = z
value: z.number().describe("The bucket's aggregated value."),
measuredAt: isoDateTime().describe("Bucket start."),
count: z.number().int().nonnegative().optional(),
minValue: z
.number()
.nullable()
.optional()
.describe(
"The lowest reading in the bucket; absent on a summed (cumulative) type.",
),
maxValue: z
.number()
.nullable()
.optional()
.describe(
"The highest reading in the bucket; absent on a summed (cumulative) type.",
),
unit: z.string().optional(),
})
.meta({
@@ -895,11 +939,15 @@ export const measurementPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Created. A single resource for the object body; an array of resources, in request order, for the array body; for a named sleep-stage body, the stored row with `status: inserted`.",
content: {
"application/json": {
// The named-stage result first: it requires `status`, so a
// plain row falls through to `measurementResource`, while a
// union that tried the plain row first would read a named-stage
// answer as a plain row and drop its `status`.
schema: dataEnvelope(
z.union([
namedSleepStageResult,
measurementResource,
z.array(measurementResource),
namedSleepStageResult,
]),
"CreateMeasurementResponse",
),