mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
test(measurements): pin the measurement series before moving its readers
A parity walk over GET /api/measurements/series through every reader it chooses between: the plain and ladder-picked rows, pulse with its spread, the paired blood pressure (two sources and a corrupt systolic), the sleep nights, glucose raw and day-bucketed, a dense stream bucketed per hour, a refused query and a delegate in another language. The readers cut their windows against the database clock, so the record is laid out from the start of the current UTC day and the snapshot holds instants and day keys relative to it.
This commit is contained in:
1 parent
817a922df5
commit
8feeaed473
11 files changed
+2792
No files matched your search
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "weight",
|
||||
"unit": "kg",
|
||||
"points": [
|
||||
{
|
||||
"id": "<id:1>",
|
||||
"at": "anchor-20d+06:30:00",
|
||||
"value": 78,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:2>",
|
||||
"at": "anchor-19d+06:30:00",
|
||||
"value": 78.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:3>",
|
||||
"at": "anchor-18d+06:30:00",
|
||||
"value": 78.2,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"at": "anchor-17d+06:30:00",
|
||||
"value": 78.3,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"at": "anchor-16d+06:30:00",
|
||||
"value": 78.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:6>",
|
||||
"at": "anchor-15d+06:30:00",
|
||||
"value": 78.5,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:7>",
|
||||
"at": "anchor-13d+06:30:00",
|
||||
"value": 78.7,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:8>",
|
||||
"at": "anchor-12d+06:30:00",
|
||||
"value": 78.8,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:9>",
|
||||
"at": "anchor-11d+06:30:00",
|
||||
"value": 78.9,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:10>",
|
||||
"at": "anchor-10d+06:30:00",
|
||||
"value": 79,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:11>",
|
||||
"at": "anchor-9d+06:30:00",
|
||||
"value": 79.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:12>",
|
||||
"at": "anchor-8d+06:30:00",
|
||||
"value": 79.2,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:13>",
|
||||
"at": "anchor-6d+06:30:00",
|
||||
"value": 79.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:14>",
|
||||
"at": "anchor-5d+06:30:00",
|
||||
"value": 79.5,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:15>",
|
||||
"at": "anchor-4d+06:30:00",
|
||||
"value": 79.6,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:16>",
|
||||
"at": "anchor-3d+06:30:00",
|
||||
"value": 79.7,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:17>",
|
||||
"at": "anchor-2d+06:30:00",
|
||||
"value": 79.8,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:18>",
|
||||
"at": "anchor-1d+06:30:00",
|
||||
"value": 79.9,
|
||||
"secondary": null
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "up-bad",
|
||||
"targetPosition": null,
|
||||
"basis": "metric_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 78,
|
||||
"to": 79.9,
|
||||
"delta": 1.9,
|
||||
"sentiment": "negative"
|
||||
},
|
||||
"referenceLines": [],
|
||||
"stats": {
|
||||
"mean": 78.95,
|
||||
"min": 78,
|
||||
"max": 79.9,
|
||||
"stdDev": 0.6,
|
||||
"count": 18
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "weight",
|
||||
"days": 30,
|
||||
"count": 18
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "steps",
|
||||
"unit": "steps",
|
||||
"points": [
|
||||
{
|
||||
"id": "<id:1>",
|
||||
"at": "anchor-13d+12:00:00",
|
||||
"value": 7300,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:2>",
|
||||
"at": "anchor-12d+12:00:00",
|
||||
"value": 7200,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:3>",
|
||||
"at": "anchor-11d+12:00:00",
|
||||
"value": 7100,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"at": "anchor-10d+12:00:00",
|
||||
"value": 7000,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"at": "anchor-9d+12:00:00",
|
||||
"value": 6900,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:6>",
|
||||
"at": "anchor-8d+12:00:00",
|
||||
"value": 6800,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:7>",
|
||||
"at": "anchor-6d+12:00:00",
|
||||
"value": 6600,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:8>",
|
||||
"at": "anchor-5d+12:00:00",
|
||||
"value": 6500,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:9>",
|
||||
"at": "anchor-4d+12:00:00",
|
||||
"value": 6400,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:10>",
|
||||
"at": "anchor-3d+12:00:00",
|
||||
"value": 6300,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:11>",
|
||||
"at": "anchor-2d+12:00:00",
|
||||
"value": 6200,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:12>",
|
||||
"at": "anchor-1d+12:00:00",
|
||||
"value": 6100,
|
||||
"secondary": null
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "up-good",
|
||||
"targetPosition": null,
|
||||
"basis": "metric_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 7300,
|
||||
"to": 6100,
|
||||
"delta": -1200,
|
||||
"sentiment": "negative"
|
||||
},
|
||||
"referenceLines": [],
|
||||
"stats": {
|
||||
"mean": 6700,
|
||||
"min": 6100,
|
||||
"max": 7300,
|
||||
"stdDev": 389.44,
|
||||
"count": 12
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "steps",
|
||||
"days": 14,
|
||||
"count": 12
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "pulse",
|
||||
"unit": "bpm",
|
||||
"points": [
|
||||
{
|
||||
"id": "<id:1>",
|
||||
"at": "anchor-6d+09:00:00",
|
||||
"value": 69,
|
||||
"secondary": null,
|
||||
"valueMin": 64,
|
||||
"valueMax": 79
|
||||
},
|
||||
{
|
||||
"id": "<id:2>",
|
||||
"at": "anchor-6d+14:00:00",
|
||||
"value": 74,
|
||||
"secondary": null,
|
||||
"valueMin": 69,
|
||||
"valueMax": 84
|
||||
},
|
||||
{
|
||||
"id": "<id:3>",
|
||||
"at": "anchor-6d+20:00:00",
|
||||
"value": 80,
|
||||
"secondary": null,
|
||||
"valueMin": 75,
|
||||
"valueMax": 90
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"at": "anchor-5d+09:00:00",
|
||||
"value": 69,
|
||||
"secondary": null,
|
||||
"valueMin": 64,
|
||||
"valueMax": 79
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"at": "anchor-5d+14:00:00",
|
||||
"value": 74,
|
||||
"secondary": null,
|
||||
"valueMin": 69,
|
||||
"valueMax": 84
|
||||
},
|
||||
{
|
||||
"id": "<id:6>",
|
||||
"at": "anchor-5d+20:00:00",
|
||||
"value": 80,
|
||||
"secondary": null,
|
||||
"valueMin": 75,
|
||||
"valueMax": 90
|
||||
},
|
||||
{
|
||||
"id": "<id:7>",
|
||||
"at": "anchor-4d+09:00:00",
|
||||
"value": 69,
|
||||
"secondary": null,
|
||||
"valueMin": 64,
|
||||
"valueMax": 79
|
||||
},
|
||||
{
|
||||
"id": "<id:8>",
|
||||
"at": "anchor-4d+14:00:00",
|
||||
"value": 74,
|
||||
"secondary": null,
|
||||
"valueMin": 69,
|
||||
"valueMax": 84
|
||||
},
|
||||
{
|
||||
"id": "<id:9>",
|
||||
"at": "anchor-4d+20:00:00",
|
||||
"value": 80,
|
||||
"secondary": null,
|
||||
"valueMin": 75,
|
||||
"valueMax": 90
|
||||
},
|
||||
{
|
||||
"id": "<id:10>",
|
||||
"at": "anchor-3d+09:00:00",
|
||||
"value": 69,
|
||||
"secondary": null,
|
||||
"valueMin": 64,
|
||||
"valueMax": 79
|
||||
},
|
||||
{
|
||||
"id": "<id:11>",
|
||||
"at": "anchor-3d+14:00:00",
|
||||
"value": 74,
|
||||
"secondary": null,
|
||||
"valueMin": 69,
|
||||
"valueMax": 84
|
||||
},
|
||||
{
|
||||
"id": "<id:12>",
|
||||
"at": "anchor-3d+20:00:00",
|
||||
"value": 80,
|
||||
"secondary": null,
|
||||
"valueMin": 75,
|
||||
"valueMax": 90
|
||||
},
|
||||
{
|
||||
"id": "<id:13>",
|
||||
"at": "anchor-2d+09:00:00",
|
||||
"value": 69,
|
||||
"secondary": null,
|
||||
"valueMin": 64,
|
||||
"valueMax": 79
|
||||
},
|
||||
{
|
||||
"id": "<id:14>",
|
||||
"at": "anchor-2d+14:00:00",
|
||||
"value": 74,
|
||||
"secondary": null,
|
||||
"valueMin": 69,
|
||||
"valueMax": 84
|
||||
},
|
||||
{
|
||||
"id": "<id:15>",
|
||||
"at": "anchor-2d+20:00:00",
|
||||
"value": 80,
|
||||
"secondary": null,
|
||||
"valueMin": 75,
|
||||
"valueMax": 90
|
||||
},
|
||||
{
|
||||
"id": "<id:16>",
|
||||
"at": "anchor-1d+09:00:00",
|
||||
"value": 69,
|
||||
"secondary": null,
|
||||
"valueMin": 64,
|
||||
"valueMax": 79
|
||||
},
|
||||
{
|
||||
"id": "<id:17>",
|
||||
"at": "anchor-1d+14:00:00",
|
||||
"value": 74,
|
||||
"secondary": null,
|
||||
"valueMin": 69,
|
||||
"valueMax": 84
|
||||
},
|
||||
{
|
||||
"id": "<id:18>",
|
||||
"at": "anchor-1d+20:00:00",
|
||||
"value": 80,
|
||||
"secondary": null,
|
||||
"valueMin": 75,
|
||||
"valueMax": 90
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "hold",
|
||||
"targetPosition": "inside",
|
||||
"basis": "profile_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 74.33,
|
||||
"to": 74.33,
|
||||
"delta": 0,
|
||||
"sentiment": "positive"
|
||||
},
|
||||
"referenceLines": [],
|
||||
"stats": {
|
||||
"mean": 74.33,
|
||||
"min": 69,
|
||||
"max": 80,
|
||||
"stdDev": 4.5,
|
||||
"count": 18
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "pulse",
|
||||
"days": 7,
|
||||
"count": 18
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "bloodPressure",
|
||||
"unit": "mmHg",
|
||||
"points": [
|
||||
{
|
||||
"id": "<id:1>",
|
||||
"at": "anchor-20d+07:01:00",
|
||||
"value": 126,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:2>",
|
||||
"at": "anchor-19d+07:01:00",
|
||||
"value": 130,
|
||||
"secondary": 82
|
||||
},
|
||||
{
|
||||
"id": "<id:3>",
|
||||
"at": "anchor-18d+07:01:00",
|
||||
"value": 129,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"at": "anchor-17d+07:01:00",
|
||||
"value": 128,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"at": "anchor-16d+07:01:00",
|
||||
"value": 127,
|
||||
"secondary": 82
|
||||
},
|
||||
{
|
||||
"id": "<id:6>",
|
||||
"at": "anchor-15d+07:01:00",
|
||||
"value": 126,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:7>",
|
||||
"at": "anchor-13d+07:01:00",
|
||||
"value": 129,
|
||||
"secondary": 82
|
||||
},
|
||||
{
|
||||
"id": "<id:8>",
|
||||
"at": "anchor-12d+07:01:00",
|
||||
"value": 128,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:9>",
|
||||
"at": "anchor-11d+07:01:00",
|
||||
"value": 127,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:10>",
|
||||
"at": "anchor-10d+07:01:00",
|
||||
"value": 126,
|
||||
"secondary": 82
|
||||
},
|
||||
{
|
||||
"id": "<id:11>",
|
||||
"at": "anchor-9d+07:01:00",
|
||||
"value": 130,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:12>",
|
||||
"at": "anchor-8d+07:01:00",
|
||||
"value": 129,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:13>",
|
||||
"at": "anchor-6d+07:01:00",
|
||||
"value": 127,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:14>",
|
||||
"at": "anchor-5d+07:01:00",
|
||||
"value": 126,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:15>",
|
||||
"at": "anchor-4d+07:01:00",
|
||||
"value": 130,
|
||||
"secondary": 82
|
||||
},
|
||||
{
|
||||
"id": "<id:16>",
|
||||
"at": "anchor-3d+07:01:00",
|
||||
"value": 129,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:17>",
|
||||
"at": "anchor-2d+07:01:00",
|
||||
"value": 128,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:18>",
|
||||
"at": "anchor-1d+07:01:00",
|
||||
"value": 127,
|
||||
"secondary": 82
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "hold",
|
||||
"targetPosition": "inside",
|
||||
"basis": "profile_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 126,
|
||||
"to": 127,
|
||||
"delta": 1,
|
||||
"sentiment": "neutral"
|
||||
},
|
||||
"referenceLines": [
|
||||
{
|
||||
"value": 120,
|
||||
"kind": "lower",
|
||||
"source": "personal",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.targetLower",
|
||||
"label": "Your target, lower limit"
|
||||
},
|
||||
{
|
||||
"value": 129,
|
||||
"kind": "upper",
|
||||
"source": "personal",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.targetUpper",
|
||||
"label": "Your target, upper limit"
|
||||
},
|
||||
{
|
||||
"value": 70,
|
||||
"kind": "lower",
|
||||
"source": "personal",
|
||||
"appliesTo": "secondary",
|
||||
"labelKey": "series.reference.targetLower",
|
||||
"label": "Your target, lower limit"
|
||||
},
|
||||
{
|
||||
"value": 79,
|
||||
"kind": "upper",
|
||||
"source": "personal",
|
||||
"appliesTo": "secondary",
|
||||
"labelKey": "series.reference.targetUpper",
|
||||
"label": "Your target, upper limit"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"mean": 127.89,
|
||||
"min": 126,
|
||||
"max": 130,
|
||||
"stdDev": 1.41,
|
||||
"count": 18
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "bloodPressure",
|
||||
"days": 30,
|
||||
"count": 18
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "sleep",
|
||||
"unit": "h",
|
||||
"points": [
|
||||
{
|
||||
"id": "sleep:day-13",
|
||||
"at": "anchor-13d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-12",
|
||||
"at": "anchor-12d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-11",
|
||||
"at": "anchor-11d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-10",
|
||||
"at": "anchor-10d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-9",
|
||||
"at": "anchor-9d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-8",
|
||||
"at": "anchor-8d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-6",
|
||||
"at": "anchor-6d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-5",
|
||||
"at": "anchor-5d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-4",
|
||||
"at": "anchor-4d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-3",
|
||||
"at": "anchor-3d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-2",
|
||||
"at": "anchor-2d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sleep:day-1",
|
||||
"at": "anchor-1d+04:30:00",
|
||||
"value": 5.5,
|
||||
"secondary": null,
|
||||
"sleepStages": {
|
||||
"CORE": 3,
|
||||
"DEEP": 1,
|
||||
"REM": 1.5,
|
||||
"AWAKE": 0.17
|
||||
}
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "up-good",
|
||||
"targetPosition": null,
|
||||
"basis": "metric_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 5.5,
|
||||
"to": 5.5,
|
||||
"delta": 0,
|
||||
"sentiment": "neutral"
|
||||
},
|
||||
"referenceLines": [],
|
||||
"stats": {
|
||||
"mean": 5.5,
|
||||
"min": 5.5,
|
||||
"max": 5.5,
|
||||
"stdDev": 0,
|
||||
"count": 12
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "sleep",
|
||||
"days": 14,
|
||||
"count": 12
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "glucose",
|
||||
"unit": "mmol/L",
|
||||
"points": [
|
||||
{
|
||||
"id": "<id:1>",
|
||||
"at": "anchor-20d+08:00:00",
|
||||
"value": 6.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:2>",
|
||||
"at": "anchor-19d+08:00:00",
|
||||
"value": 6.3,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:3>",
|
||||
"at": "anchor-18d+08:00:00",
|
||||
"value": 6.3,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"at": "anchor-17d+08:00:00",
|
||||
"value": 6.2,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"at": "anchor-16d+08:00:00",
|
||||
"value": 6.2,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:6>",
|
||||
"at": "anchor-15d+08:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:7>",
|
||||
"at": "anchor-13d+08:00:00",
|
||||
"value": 6,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:8>",
|
||||
"at": "anchor-12d+08:00:00",
|
||||
"value": 5.9,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:9>",
|
||||
"at": "anchor-11d+08:00:00",
|
||||
"value": 5.9,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:10>",
|
||||
"at": "anchor-10d+08:00:00",
|
||||
"value": 5.8,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:11>",
|
||||
"at": "anchor-9d+08:00:00",
|
||||
"value": 5.8,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:12>",
|
||||
"at": "anchor-8d+08:00:00",
|
||||
"value": 5.7,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:13>",
|
||||
"at": "anchor-6d+08:00:00",
|
||||
"value": 5.6,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:14>",
|
||||
"at": "anchor-5d+08:00:00",
|
||||
"value": 5.5,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:15>",
|
||||
"at": "anchor-4d+08:00:00",
|
||||
"value": 5.5,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:16>",
|
||||
"at": "anchor-3d+08:00:00",
|
||||
"value": 5.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:17>",
|
||||
"at": "anchor-2d+08:00:00",
|
||||
"value": 5.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "<id:18>",
|
||||
"at": "anchor-1d+08:00:00",
|
||||
"value": 5.3,
|
||||
"secondary": null
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "up-bad",
|
||||
"targetPosition": null,
|
||||
"basis": "metric_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 6.4,
|
||||
"to": 5.3,
|
||||
"delta": -1.1,
|
||||
"sentiment": "positive"
|
||||
},
|
||||
"referenceLines": [
|
||||
{
|
||||
"value": 3.9,
|
||||
"kind": "lower",
|
||||
"source": "clinical",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.clinicalLower",
|
||||
"label": "Clinical lower limit"
|
||||
},
|
||||
{
|
||||
"value": 10,
|
||||
"kind": "upper",
|
||||
"source": "clinical",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.clinicalUpper",
|
||||
"label": "Clinical upper limit"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"mean": 5.9,
|
||||
"min": 5.3,
|
||||
"max": 6.4,
|
||||
"stdDev": 0.3,
|
||||
"count": 18
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "glucose",
|
||||
"days": 30,
|
||||
"count": 18
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "glucose",
|
||||
"unit": "mmol/L",
|
||||
"points": [
|
||||
{
|
||||
"id": "day:day-70",
|
||||
"at": "anchor-70d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-69",
|
||||
"at": "anchor-69d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-68",
|
||||
"at": "anchor-68d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-67",
|
||||
"at": "anchor-67d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-66",
|
||||
"at": "anchor-66d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-65",
|
||||
"at": "anchor-65d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-64",
|
||||
"at": "anchor-64d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-63",
|
||||
"at": "anchor-63d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-62",
|
||||
"at": "anchor-62d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-61",
|
||||
"at": "anchor-61d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-60",
|
||||
"at": "anchor-60d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-20",
|
||||
"at": "anchor-20d+00:00:00",
|
||||
"value": 6.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-19",
|
||||
"at": "anchor-19d+00:00:00",
|
||||
"value": 6.3,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-18",
|
||||
"at": "anchor-18d+00:00:00",
|
||||
"value": 6.3,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-17",
|
||||
"at": "anchor-17d+00:00:00",
|
||||
"value": 6.2,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-16",
|
||||
"at": "anchor-16d+00:00:00",
|
||||
"value": 6.2,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-15",
|
||||
"at": "anchor-15d+00:00:00",
|
||||
"value": 6.1,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-13",
|
||||
"at": "anchor-13d+00:00:00",
|
||||
"value": 6,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-12",
|
||||
"at": "anchor-12d+00:00:00",
|
||||
"value": 5.9,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-11",
|
||||
"at": "anchor-11d+00:00:00",
|
||||
"value": 5.9,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-10",
|
||||
"at": "anchor-10d+00:00:00",
|
||||
"value": 5.8,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-9",
|
||||
"at": "anchor-9d+00:00:00",
|
||||
"value": 5.8,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-8",
|
||||
"at": "anchor-8d+00:00:00",
|
||||
"value": 5.7,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-6",
|
||||
"at": "anchor-6d+00:00:00",
|
||||
"value": 5.6,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-5",
|
||||
"at": "anchor-5d+00:00:00",
|
||||
"value": 5.5,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-4",
|
||||
"at": "anchor-4d+00:00:00",
|
||||
"value": 5.5,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-3",
|
||||
"at": "anchor-3d+00:00:00",
|
||||
"value": 5.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-2",
|
||||
"at": "anchor-2d+00:00:00",
|
||||
"value": 5.4,
|
||||
"secondary": null
|
||||
},
|
||||
{
|
||||
"id": "day:day-1",
|
||||
"at": "anchor-1d+00:00:00",
|
||||
"value": 5.3,
|
||||
"secondary": null
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "up-bad",
|
||||
"targetPosition": null,
|
||||
"basis": "metric_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 6.1,
|
||||
"to": 5.3,
|
||||
"delta": -0.8,
|
||||
"sentiment": "positive"
|
||||
},
|
||||
"referenceLines": [
|
||||
{
|
||||
"value": 3.9,
|
||||
"kind": "lower",
|
||||
"source": "clinical",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.clinicalLower",
|
||||
"label": "Clinical lower limit"
|
||||
},
|
||||
{
|
||||
"value": 10,
|
||||
"kind": "upper",
|
||||
"source": "clinical",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.clinicalUpper",
|
||||
"label": "Clinical upper limit"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"mean": 6,
|
||||
"min": 5.3,
|
||||
"max": 6.4,
|
||||
"stdDev": 0.3,
|
||||
"count": 29
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "glucose",
|
||||
"days": 365,
|
||||
"count": 29
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
+1066
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"status": 422,
|
||||
"body": {
|
||||
"data": null,
|
||||
"error": "Validation failed",
|
||||
"details": {
|
||||
"issues": [
|
||||
{
|
||||
"path": "kind",
|
||||
"code": "invalid_value",
|
||||
"message": "Invalid option: expected one of \"weight\"|\"bloodPressure\"|\"pulse\"|\"bodyFat\"|\"glucose\"|\"sleep\"|\"steps\"|\"totalBodyWater\"|\"boneMass\"|\"oxygenSaturation\"|\"restingHeartRate\"|\"heartRateVariability\"|\"vo2Max\""
|
||||
},
|
||||
{
|
||||
"path": "days",
|
||||
"code": "too_small",
|
||||
"message": "Too small: expected number to be >=1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"errorCode": "http.invalid",
|
||||
"requestId": "<requestId>"
|
||||
}
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 422,
|
||||
"action": {
|
||||
"name": "measurements.series.validation-failed"
|
||||
},
|
||||
"meta": {
|
||||
"issue_count": 2,
|
||||
"received_keys": [
|
||||
"kind",
|
||||
"days"
|
||||
],
|
||||
"received_shape_excerpt": "{\"kind\":\"temperature\",\"days\":\"0\"}",
|
||||
"zod_issues": [
|
||||
{
|
||||
"path": "kind",
|
||||
"code": "invalid_value",
|
||||
"message": "Invalid option: expected one of \"weight\"|\"bloodPressure\"|\"pulse\"|\"bodyFat\"|\"glucose\"|\"sleep\"|\"steps\"|\"totalBodyWater\"|\"boneMass\"|\"oxygenSaturation\"|\"restingHeartRate\"|\"heartRateVariability\"|\"vo2Max\""
|
||||
},
|
||||
{
|
||||
"path": "days",
|
||||
"code": "too_small",
|
||||
"message": "Too small: expected number to be >=1"
|
||||
}
|
||||
],
|
||||
"rejected_attempt": "measurements.series.validation-failed"
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"kind": "bloodPressure",
|
||||
"unit": "mmHg",
|
||||
"points": [
|
||||
{
|
||||
"id": "<id:1>",
|
||||
"at": "anchor-6d+07:01:00",
|
||||
"value": 127,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:2>",
|
||||
"at": "anchor-5d+07:01:00",
|
||||
"value": 126,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:3>",
|
||||
"at": "anchor-4d+07:01:00",
|
||||
"value": 130,
|
||||
"secondary": 82
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"at": "anchor-3d+07:01:00",
|
||||
"value": 129,
|
||||
"secondary": 81
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"at": "anchor-2d+07:01:00",
|
||||
"value": 128,
|
||||
"secondary": 83
|
||||
},
|
||||
{
|
||||
"id": "<id:6>",
|
||||
"at": "anchor-1d+07:01:00",
|
||||
"value": 127,
|
||||
"secondary": 82
|
||||
}
|
||||
],
|
||||
"direction": {
|
||||
"direction": "hold",
|
||||
"targetPosition": "inside",
|
||||
"basis": "profile_default"
|
||||
},
|
||||
"change": {
|
||||
"from": 127,
|
||||
"to": 127,
|
||||
"delta": 0,
|
||||
"sentiment": "positive"
|
||||
},
|
||||
"referenceLines": [
|
||||
{
|
||||
"value": 120,
|
||||
"kind": "lower",
|
||||
"source": "personal",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.targetLower",
|
||||
"label": "Dein Ziel, Untergrenze"
|
||||
},
|
||||
{
|
||||
"value": 129,
|
||||
"kind": "upper",
|
||||
"source": "personal",
|
||||
"appliesTo": "value",
|
||||
"labelKey": "series.reference.targetUpper",
|
||||
"label": "Dein Ziel, Obergrenze"
|
||||
},
|
||||
{
|
||||
"value": 70,
|
||||
"kind": "lower",
|
||||
"source": "personal",
|
||||
"appliesTo": "secondary",
|
||||
"labelKey": "series.reference.targetLower",
|
||||
"label": "Dein Ziel, Untergrenze"
|
||||
},
|
||||
{
|
||||
"value": 79,
|
||||
"kind": "upper",
|
||||
"source": "personal",
|
||||
"appliesTo": "secondary",
|
||||
"labelKey": "series.reference.targetUpper",
|
||||
"label": "Dein Ziel, Obergrenze"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"mean": 127.83,
|
||||
"min": 126,
|
||||
"max": 130,
|
||||
"stdDev": 1.34,
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "measurements.series"
|
||||
},
|
||||
"meta": {
|
||||
"kind": "bloodPressure",
|
||||
"days": 7,
|
||||
"count": 6
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* Parity for `GET /api/measurements/series` while its readers move into
|
||||
* `src/lib/measurements/series/`.
|
||||
*
|
||||
* One account holds weight, blood pressure from two sources (with a corrupt
|
||||
* systolic row), pulse with hourly spreads, a stream of per-minute pulse
|
||||
* dense enough to be bucketed per hour, glucose in mmol/L preference, sleep
|
||||
* stages and steps. The walk asks for each kind it reads differently: the
|
||||
* plain readers, the paired blood pressure, the sleep nights, a dense kind
|
||||
* inside the raw window, the hour buckets, the day buckets of a long window,
|
||||
* a refused query, and a delegate reading the record in another language.
|
||||
*
|
||||
* The clock is NOT pinned here: the series readers cut their windows in SQL
|
||||
* against the database's own `now()`, which a pinned `Date` would disagree
|
||||
* with. The record is laid out relative to the start of the current UTC day
|
||||
* instead, no reading sits on a window's edge day, and every instant and day
|
||||
* key in the snapshot is written relative to that start, so the snapshot is
|
||||
* the same on every day it runs.
|
||||
*/
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { GET } from "@/app/api/measurements/series/route";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
import { callRoute, type RouteAuth } from "../../../kit/call-route";
|
||||
import { makeGrant, makeSession, makeUser } from "../../../kit/factories";
|
||||
import {
|
||||
eventFacts,
|
||||
expectParity,
|
||||
stable,
|
||||
wideEventRecorder,
|
||||
} from "../../../kit/parity";
|
||||
import {
|
||||
getPrismaClient,
|
||||
switchSessionTo,
|
||||
truncateAllTables,
|
||||
} from "../../setup";
|
||||
|
||||
vi.mock("@/lib/logging/transports", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/logging/transports")>();
|
||||
const { wideEventRecorder: recorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
emitIfSampled: (event: (typeof recorder.events)[number]) => {
|
||||
recorder.events.push(event);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const DAY = 86_400_000;
|
||||
/** The start of the current UTC day: the anchor the record is laid out from. */
|
||||
const ANCHOR = new Date(Math.floor(Date.now() / DAY) * DAY);
|
||||
/**
|
||||
* Days back the daily readings sit. The windows asked for are 7, 14, 30 and
|
||||
* 365 days; a reading on exactly that many days back would fall in or out of
|
||||
* the window with the time of day the test runs, so those days are skipped.
|
||||
*/
|
||||
const READING_DAYS = Array.from({ length: 20 }, (_, i) => i + 1).filter(
|
||||
(d) => d !== 7 && d !== 14,
|
||||
);
|
||||
|
||||
let userId: string;
|
||||
let owner: RouteAuth;
|
||||
let delegate: { auth: RouteAuth & { sessionId: string } };
|
||||
|
||||
function at(daysAgo: number, hour: number, minute = 0): Date {
|
||||
return new Date(
|
||||
ANCHOR.getTime() - daysAgo * DAY + (hour * 60 + minute) * 60_000,
|
||||
);
|
||||
}
|
||||
|
||||
const ISO_INSTANT = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g;
|
||||
const DAY_KEY = /\b\d{4}-\d{2}-\d{2}\b/g;
|
||||
|
||||
/** An instant as days and time from the anchor: `anchor-13d+06:30`. */
|
||||
function relativeInstant(iso: string): string {
|
||||
const ms = Date.parse(iso) - ANCHOR.getTime();
|
||||
const days = Math.floor(ms / DAY);
|
||||
const rest = new Date(ms - days * DAY).toISOString().slice(11, 19);
|
||||
return `anchor${days >= 0 ? "+" : ""}${days}d+${rest}`;
|
||||
}
|
||||
|
||||
/** A day key as days from the anchor's day: `day-13`. */
|
||||
function relativeDay(key: string): string {
|
||||
const days = Math.round(
|
||||
(Date.parse(`${key}T00:00:00Z`) - ANCHOR.getTime()) / DAY,
|
||||
);
|
||||
return `day${days >= 0 ? "+" : ""}${days}`;
|
||||
}
|
||||
|
||||
function relativeDates(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
.replace(ISO_INSTANT, relativeInstant)
|
||||
.replace(DAY_KEY, relativeDay);
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(relativeDates);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([k, v]) => [k, relativeDates(v)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function seedRows(): Prisma.MeasurementCreateManyInput[] {
|
||||
const rows: Prisma.MeasurementCreateManyInput[] = [];
|
||||
const add = (row: Omit<Prisma.MeasurementCreateManyInput, "userId">) =>
|
||||
rows.push({ userId, ...row });
|
||||
for (const d of READING_DAYS) {
|
||||
add({
|
||||
type: "WEIGHT",
|
||||
value: 80 - d * 0.1,
|
||||
unit: "kg",
|
||||
measuredAt: at(d, 6, 30),
|
||||
source: d % 2 ? "MANUAL" : "WITHINGS",
|
||||
externalId: d % 2 ? null : `w-${d}`,
|
||||
});
|
||||
for (const [source, offset] of [
|
||||
["MANUAL", 0],
|
||||
["WITHINGS", 1],
|
||||
] as const) {
|
||||
add({
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
value: 125 + (d % 5) + offset,
|
||||
unit: "mmHg",
|
||||
measuredAt: at(d, 7, offset),
|
||||
source,
|
||||
externalId: source === "WITHINGS" ? `bp-s-${d}` : null,
|
||||
});
|
||||
add({
|
||||
type: "BLOOD_PRESSURE_DIA",
|
||||
value: 80 + (d % 3) + offset,
|
||||
unit: "mmHg",
|
||||
measuredAt: at(d, 7, offset + 2),
|
||||
source,
|
||||
externalId: source === "WITHINGS" ? `bp-d-${d}` : null,
|
||||
});
|
||||
}
|
||||
add({
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 95 + d,
|
||||
unit: "mg/dL",
|
||||
measuredAt: at(d, 8),
|
||||
source: "MANUAL",
|
||||
});
|
||||
add({
|
||||
type: "ACTIVITY_STEPS",
|
||||
value: 6_000 + d * 100,
|
||||
unit: "steps",
|
||||
measuredAt: at(d, 12),
|
||||
source: "APPLE_HEALTH",
|
||||
externalId: `stats:steps:${d}`,
|
||||
aggregationProvenance: "HEALTHKIT_STATISTICS",
|
||||
});
|
||||
for (const [stage, start, minutes] of [
|
||||
["CORE", 23, 180],
|
||||
["DEEP", 2, 60],
|
||||
["REM", 3, 90],
|
||||
["AWAKE", 4, 10],
|
||||
] as const) {
|
||||
const begin = at(d, start);
|
||||
if (start === 23) begin.setTime(begin.getTime() - DAY);
|
||||
add({
|
||||
type: "SLEEP_DURATION",
|
||||
value: minutes,
|
||||
unit: "min",
|
||||
measuredAt: new Date(begin.getTime() + minutes * 60_000),
|
||||
source: "APPLE_HEALTH",
|
||||
sleepStage: stage,
|
||||
externalId: `sleep-${d}-${stage}`,
|
||||
});
|
||||
}
|
||||
for (const hour of [9, 14, 20]) {
|
||||
add({
|
||||
type: "PULSE",
|
||||
value: 60 + hour,
|
||||
valueMin: 55 + hour,
|
||||
valueMax: 70 + hour,
|
||||
unit: "bpm",
|
||||
measuredAt: at(d, hour),
|
||||
source: "APPLE_HEALTH",
|
||||
externalId: `stats:hr:${d}:${hour}`,
|
||||
aggregationProvenance: "HEALTHKIT_STATISTICS",
|
||||
});
|
||||
}
|
||||
}
|
||||
// A seed-era corrupt systolic the series must never surface.
|
||||
add({
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
value: 0,
|
||||
unit: "mmHg",
|
||||
measuredAt: at(2, 9),
|
||||
source: "MANUAL",
|
||||
});
|
||||
// Two months back, so a long window has more than the raw window holds.
|
||||
for (let d = 60; d <= 70; d++) {
|
||||
add({
|
||||
type: "BLOOD_GLUCOSE",
|
||||
value: 110,
|
||||
unit: "mg/dL",
|
||||
measuredAt: at(d, 8),
|
||||
source: "MANUAL",
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** A per-minute heart-rate stream dense enough to be bucketed per hour. */
|
||||
function denseHeartRate(): Prisma.MeasurementCreateManyInput[] {
|
||||
return Array.from({ length: 10_200 }, (_, i) => ({
|
||||
userId,
|
||||
type: "HEART_RATE_VARIABILITY",
|
||||
value: 40 + (i % 17),
|
||||
unit: "ms",
|
||||
measuredAt: new Date(ANCHOR.getTime() - (i + 1) * 60_000),
|
||||
source: "APPLE_HEALTH",
|
||||
externalId: `hrv-${i}`,
|
||||
}));
|
||||
}
|
||||
|
||||
async function step(
|
||||
name: string,
|
||||
query: Record<string, string | number>,
|
||||
auth: RouteAuth = owner,
|
||||
): Promise<void> {
|
||||
wideEventRecorder.take();
|
||||
const res = await callRoute(GET, {
|
||||
method: "GET",
|
||||
route: "/api/measurements/series",
|
||||
query,
|
||||
auth,
|
||||
});
|
||||
const events = wideEventRecorder.take();
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
await expectParity(
|
||||
`measurement-series.${name}`,
|
||||
stable(
|
||||
relativeDates({
|
||||
status: res.status,
|
||||
body: res.body,
|
||||
events: events.map(eventFacts),
|
||||
}),
|
||||
{ since: new Date() },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await truncateAllTables(prisma);
|
||||
const user = await makeUser({
|
||||
// UTC, so a daylight-saving change between two runs cannot move a local
|
||||
// day boundary relative to the anchor.
|
||||
timezone: "UTC",
|
||||
glucoseUnit: "mmol/L",
|
||||
dateOfBirth: new Date("1970-01-01T00:00:00.000Z"),
|
||||
locale: "en",
|
||||
});
|
||||
userId = user.id;
|
||||
owner = await makeSession(user);
|
||||
await prisma.measurement.createMany({ data: seedRows() });
|
||||
await prisma.measurement.createMany({ data: denseHeartRate() });
|
||||
const reader = await makeUser({ timezone: "UTC", locale: "de" });
|
||||
await makeGrant(user, reader);
|
||||
const session = await makeSession(reader);
|
||||
delegate = { auth: session };
|
||||
});
|
||||
|
||||
describe("GET /api/measurements/series parity", () => {
|
||||
it("the plain readers", async () => {
|
||||
await step("01-weight", { kind: "weight" });
|
||||
await step("02-steps", { kind: "steps", days: 14 });
|
||||
await step("03-pulse-spread", { kind: "pulse", days: 7 });
|
||||
});
|
||||
|
||||
it("the paired blood pressure and the sleep nights", async () => {
|
||||
await step("04-blood-pressure", { kind: "bloodPressure" });
|
||||
await step("05-sleep", { kind: "sleep", days: 14 });
|
||||
});
|
||||
|
||||
it("glucose raw, and day-bucketed past the raw window", async () => {
|
||||
await step("06-glucose-raw", { kind: "glucose" });
|
||||
await step("07-glucose-long", { kind: "glucose", days: 365 });
|
||||
});
|
||||
|
||||
it("a dense stream bucketed per hour", async () => {
|
||||
await step("08-hrv-hour-buckets", {
|
||||
kind: "heartRateVariability",
|
||||
days: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it("a refused query", async () => {
|
||||
await step("09-invalid", { kind: "temperature", days: 0 });
|
||||
});
|
||||
|
||||
it("a delegate reading the record in another language", async () => {
|
||||
await switchSessionTo(delegate.auth.sessionId, userId);
|
||||
await step(
|
||||
"10-delegated",
|
||||
{ kind: "bloodPressure", days: 7 },
|
||||
delegate.auth,
|
||||
);
|
||||
await switchSessionTo(delegate.auth.sessionId, null);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user