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

+89 -1
View File
@@ -48307,6 +48307,32 @@ components:
description: Time asleep across those naps, in minutes.
type: number
minimum: 0
sleepStages:
description: 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.
type: object
propertyNames:
type: string
additionalProperties:
type: number
minimum: 0
inBedMinutes:
description: The main night's time in bed in minutes, or null when no in-bed span was recorded.
anyOf:
- type: number
minimum: 0
- type: "null"
awakeMinutes:
description: The main night's time awake in bed in minutes, or null when no awake span was recorded.
anyOf:
- type: number
minimum: 0
- type: "null"
awakenings:
description: Awake spans between the main night's first and last time asleep.
type: integer
minimum: 0
maximum: 9007199254740991
required:
- id
- type
@@ -49077,6 +49103,32 @@ components:
description: Time asleep across those naps, in minutes.
type: number
minimum: 0
sleepStages:
description: 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.
type: object
propertyNames:
type: string
additionalProperties:
type: number
minimum: 0
inBedMinutes:
description: The main night's time in bed in minutes, or null when no in-bed span was recorded.
anyOf:
- type: number
minimum: 0
- type: "null"
awakeMinutes:
description: The main night's time awake in bed in minutes, or null when no awake span was recorded.
anyOf:
- type: number
minimum: 0
- type: "null"
awakenings:
description: Awake spans between the main night's first and last time asleep.
type: integer
minimum: 0
maximum: 9007199254740991
required:
- id
- type
@@ -49105,6 +49157,16 @@ components:
type: integer
minimum: 0
maximum: 9007199254740991
minValue:
description: The lowest reading in the bucket; absent on a summed (cumulative) type.
type:
- number
- "null"
maxValue:
description: The highest reading in the bucket; absent on a summed (cumulative) type.
type:
- number
- "null"
unit:
type: string
required:
@@ -49282,6 +49344,32 @@ components:
description: Time asleep across those naps, in minutes.
type: number
minimum: 0
sleepStages:
description: 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.
type: object
propertyNames:
type: string
additionalProperties:
type: number
minimum: 0
inBedMinutes:
description: The main night's time in bed in minutes, or null when no in-bed span was recorded.
anyOf:
- type: number
minimum: 0
- type: "null"
awakeMinutes:
description: The main night's time awake in bed in minutes, or null when no awake span was recorded.
anyOf:
- type: number
minimum: 0
- type: "null"
awakenings:
description: Awake spans between the main night's first and last time asleep.
type: integer
minimum: 0
maximum: 9007199254740991
status:
type: string
enum:
@@ -49306,11 +49394,11 @@ components:
properties:
data:
anyOf:
- $ref: "#/components/schemas/NamedSleepStageMeasurement"
- $ref: "#/components/schemas/MeasurementResource"
- type: array
items:
$ref: "#/components/schemas/MeasurementResource"
- $ref: "#/components/schemas/NamedSleepStageMeasurement"
error:
type: "null"
meta:
+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",
),