refactor(integrations): single Nightscout entry-to-measurement mapper

Route the sync write through mapSgvEntryToMeasurement so the
entry-to-measurement shape has one source of truth instead of an
inline copy plus an unused parallel mapper that could drift.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 19:09:52 +02:00
1 parent f5d0b2f696
commit d33fed60d4
3 files changed
+42 -40

No files matched your search

+12 -7
View File
@@ -73,8 +73,12 @@ describe("parseSgvEntries", () => {
});
describe("mapSgvEntryToMeasurement", () => {
it("maps an SGV entry to a BLOOD_GLUCOSE mg/dL measurement", () => {
const m = mapSgvEntryToMeasurement(SAMPLE[0]!);
it("maps a parsed SGV entry to a BLOOD_GLUCOSE mg/dL measurement", () => {
const m = mapSgvEntryToMeasurement({
id: "abc123",
sgv: 112,
date: 1718000000000,
});
expect(m.type).toBe("BLOOD_GLUCOSE");
expect(m.unit).toBe("mg/dL");
expect(m.value).toBe(112);
@@ -82,15 +86,16 @@ describe("mapSgvEntryToMeasurement", () => {
});
it("derives a stable externalId so re-sync is idempotent", () => {
const a = mapSgvEntryToMeasurement(SAMPLE[0]!);
const b = mapSgvEntryToMeasurement(SAMPLE[0]!);
const entry = { id: "abc123", sgv: 112, date: 1718000000000 };
const a = mapSgvEntryToMeasurement(entry);
const b = mapSgvEntryToMeasurement(entry);
expect(a.externalId).toBe(b.externalId);
expect(a.externalId).toContain("abc123");
expect(a.externalId).toBe("ns:abc123");
});
it("falls back to the timestamp when the entry has no _id", () => {
it("falls back to the timestamp when the entry has no id", () => {
const m = mapSgvEntryToMeasurement({
type: "sgv",
id: null,
sgv: 90,
date: 1718000000000,
});
+22 -23
View File
@@ -135,34 +135,33 @@ export function parseSgvEntries(payload: unknown): ParsedSgvEntry[] {
return out;
}
/**
* Map one SGV entry to a `BLOOD_GLUCOSE` mg/dL measurement. The `externalId`
* is `ns:<id>` when the entry carries Nightscout's `_id`, falling back to
* `ns:date:<epochMs>` — both stable per reading, so a re-sync collapses onto
* the existing row via the `(userId, type, source, externalId)` unique. mg/dL
* is stored canonical (Nightscout always reports mg/dL; the display unit is a
* user preference resolved elsewhere).
*/
export function mapSgvEntryToMeasurement(
entry: NightscoutSgvEntry,
): NightscoutMeasurement {
const externalId = entry._id
? `ns:${entry._id}`
: `ns:date:${entry.date ?? 0}`;
return {
type: "BLOOD_GLUCOSE",
value: entry.sgv as number,
unit: NIGHTSCOUT_GLUCOSE_UNIT,
measuredAt: new Date(entry.date as number),
externalId,
};
}
/** Externalid for a parsed entry — the canonical idempotency key. */
export function externalIdFor(entry: ParsedSgvEntry): string {
return entry.id ? `ns:${entry.id}` : `ns:date:${entry.date}`;
}
/**
* Map one parsed SGV entry to a `BLOOD_GLUCOSE` mg/dL measurement. The
* `externalId` is `ns:<_id>` when the entry carries Nightscout's `_id`, falling
* back to `ns:date:<epochMs>` (via `externalIdFor`) — both stable per reading,
* so a re-sync collapses onto the existing row via the
* `(userId, type, source, externalId)` unique. mg/dL is stored canonical
* (Nightscout always reports mg/dL; the display unit is a user preference
* resolved elsewhere). This is the SINGLE entry→measurement mapping — the sync
* write path consumes it, so the shape can never drift between two copies.
*/
export function mapSgvEntryToMeasurement(
entry: ParsedSgvEntry,
): NightscoutMeasurement {
return {
type: "BLOOD_GLUCOSE",
value: entry.sgv,
unit: NIGHTSCOUT_GLUCOSE_UNIT,
measuredAt: new Date(entry.date),
externalId: externalIdFor(entry),
};
}
/**
* Raised when an instance answers non-2xx or is unreachable. The connect
* route + sync classifier switch on `status` to tell "wrong token" (401/403)
+8 -10
View File
@@ -33,10 +33,9 @@ import {
} from "@/lib/rollups/measurement-rollups";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import {
externalIdFor,
fetchSgvEntries,
mapSgvEntryToMeasurement,
NightscoutApiError,
NIGHTSCOUT_GLUCOSE_UNIT,
type ParsedSgvEntry,
} from "./client";
import { getUserNightscoutCredentials } from "./credentials";
@@ -119,8 +118,7 @@ export async function upsertNightscoutEntries(
const touched: Array<{ type: MeasurementType; measuredAt: Date }> = [];
for (const entry of entries) {
const externalId = externalIdFor(entry);
const measuredAt = new Date(entry.date);
const mapped = mapSgvEntryToMeasurement(entry);
try {
await prisma.measurement.upsert({
where: {
@@ -128,23 +126,23 @@ export async function upsertNightscoutEntries(
userId,
type,
source: "NIGHTSCOUT",
externalId,
externalId: mapped.externalId,
},
},
create: {
userId,
type,
source: "NIGHTSCOUT",
value: entry.sgv,
unit: NIGHTSCOUT_GLUCOSE_UNIT,
measuredAt,
externalId,
value: mapped.value,
unit: mapped.unit,
measuredAt: mapped.measuredAt,
externalId: mapped.externalId,
},
// First-write-wins: a CGM sample is immutable, so a re-sync of the
// same reading must not rewrite the stored value or measuredAt.
update: {},
});
touched.push({ type, measuredAt });
touched.push({ type, measuredAt: mapped.measuredAt });
imported++;
} catch (err) {
getEvent()?.addWarning(