fix(measurements): accept deviceType on single-entry POST (mirrors batch route)

`Measurement.deviceType` exists on the row, was added by the W8c
batch ingest path, but the single-entry `createMeasurementSchema`
omitted it — every single-row POST silently dropped the device tag
even when the client supplied one. Manual rows tagged with watch /
scale / ring metadata lost their attribution at the schema boundary
and showed up as unsourced.

Declare `deviceType` on the schema (optional, nullable, 1–32 chars),
thread it through both single-entry and array-body POST handlers,
and add three RTL cases (present, null, omitted) so the parsed
shape stays observable.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 16:56:06 +02:00
1 parent 72fcdc656f
commit c49fe731e4
3 files changed
+60 -1

No files matched your search

+10 -1
View File
@@ -96,6 +96,10 @@ async function postMeasurement(request: NextRequest) {
notes: m.notes ?? null,
glucoseContext:
(m.glucoseContext as GlucoseContext | undefined) ?? null,
// v1.4.25 W10 reconcile (code-review M4): mirror the
// single-entry path so the multi-entry batch persists
// `deviceType` instead of silently dropping it.
deviceType: m.deviceType ?? null,
},
}),
),
@@ -127,7 +131,7 @@ async function postMeasurement(request: NextRequest) {
return apiError(parsed.error.issues[0].message, 422);
}
const { type, value, measuredAt, notes, source, glucoseContext } =
const { type, value, measuredAt, notes, source, glucoseContext, deviceType } =
parsed.data;
// Handle unique constraint violation
@@ -143,6 +147,11 @@ async function postMeasurement(request: NextRequest) {
measuredAt,
notes: notes ?? null,
glucoseContext: (glucoseContext as GlucoseContext | undefined) ?? null,
// v1.4.25 W10 reconcile (code-review M4): the deviceType column
// already accepts client metadata from the batch route. Mirror
// the behaviour here so single-entry POST persists the tag
// instead of silently dropping it.
deviceType: deviceType ?? null,
},
});
} catch (err) {
@@ -130,5 +130,47 @@ describe("measurement validation", () => {
});
expect(parsed.success).toBe(false);
});
// v1.4.25 W10 reconcile (code-review M4): `deviceType` was a
// batch-only field; the single-entry schema silently dropped it.
// Now it parses through so the column is populated whether the
// client posts one row or many.
it("accepts an optional deviceType tag and surfaces it on the parsed value", () => {
const parsed = createMeasurementSchema.safeParse({
...validBase,
type: "WEIGHT",
value: 82,
deviceType: "scale",
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.deviceType).toBe("scale");
}
});
it("accepts a null deviceType so a client can explicitly clear the column", () => {
const parsed = createMeasurementSchema.safeParse({
...validBase,
type: "WEIGHT",
value: 82,
deviceType: null,
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.deviceType).toBeNull();
}
});
it("treats a missing deviceType as undefined (back-compat with pre-W10 clients)", () => {
const parsed = createMeasurementSchema.safeParse({
...validBase,
type: "WEIGHT",
value: 82,
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.deviceType).toBeUndefined();
}
});
});
});
+8
View File
@@ -205,6 +205,14 @@ export const createMeasurementSchema = z
// Only applies when type === BLOOD_GLUCOSE. Mirrored by a CHECK
// constraint in Postgres (see migration 0021).
glucoseContext: glucoseContextEnum.optional(),
// v1.4.25 W10 reconcile (code-review M4): the single-entry POST
// dropped `deviceType` silently because the column existed on
// `Measurement` and was accepted by the batch route, but never
// declared in this schema. iOS clients that POST one row at a
// time (and dashboards that backfill manual rows with device
// metadata) now persist the tag instead of seeing it disappear.
// Accepts `null` so a client can explicitly clear the column.
deviceType: z.string().min(1).max(32).nullable().optional(),
})
.refine((data) => validateMeasurementRange(data.type, data.value) === null, {
message: "Value out of plausible range",