mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(schema): extend Measurement composite with sleepStage (Migration 0055)
The Withings Sleep v2 sync writes one Measurement row per stage segment (AWAKE | CORE | DEEP | REM) for the same night, all sharing the legacy four-column composite (user_id, type=SLEEP_DURATION, measured_at, source=WITHINGS). Migration 0055 adds sleep_stage as the fifth axis and recreates the unique index with NULLS NOT DISTINCT so non-sleep rows (sleep_stage IS NULL) still dedup on the first four columns — same effective semantics as before for every meastype other than SLEEP_DURATION. Prisma's generated compound input requires non-null fields, so the Withings sync's upsert is reshaped as findFirst + create/update for the non-sleep path (sleepStage is always NULL there). The new five-column upsert key is the canonical path for the Sleep v2 ingest landing in W17c.
This commit is contained in:
1 parent
5a7d2526c9
commit
df5a82bc6f
4 files changed
+247
-15
No files matched your search
+29
-14
@@ -196,28 +196,43 @@ export async function syncUserMeasurements(
|
||||
let imported = 0;
|
||||
|
||||
for (const m of measures) {
|
||||
// Upsert to avoid duplicates (unique on userId+type+measuredAt+source)
|
||||
const measType = m.type as MeasurementType;
|
||||
try {
|
||||
await prisma.measurement.upsert({
|
||||
// v1.4.25 W17b/c — Migration 0055 added `sleepStage` to the
|
||||
// composite unique with `NULLS NOT DISTINCT`, so a non-sleep
|
||||
// upsert keyed on (userId, type, measuredAt, source) with
|
||||
// sleepStage IS NULL is still safe. But Prisma's typed compound
|
||||
// input requires a non-null `sleepStage`, so we model the
|
||||
// idempotent write as a `findFirst` + `create`/`update`. The
|
||||
// unique index still serializes concurrent inserts, so the worst
|
||||
// case is a Prisma P2002 we catch and ignore.
|
||||
const existing = await prisma.measurement.findFirst({
|
||||
where: {
|
||||
userId_type_measuredAt_source: {
|
||||
userId,
|
||||
type: measType,
|
||||
measuredAt: m.measuredAt,
|
||||
source: "WITHINGS",
|
||||
sleepStage: null,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
await prisma.measurement.update({
|
||||
where: { id: existing.id },
|
||||
data: { value: m.value },
|
||||
});
|
||||
} else {
|
||||
await prisma.measurement.create({
|
||||
data: {
|
||||
userId,
|
||||
type: measType,
|
||||
value: m.value,
|
||||
unit: getUnitForType(m.type),
|
||||
measuredAt: m.measuredAt,
|
||||
source: "WITHINGS",
|
||||
},
|
||||
},
|
||||
update: { value: m.value },
|
||||
create: {
|
||||
userId,
|
||||
type: measType,
|
||||
value: m.value,
|
||||
unit: getUnitForType(m.type),
|
||||
measuredAt: m.measuredAt,
|
||||
source: "WITHINGS",
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
imported++;
|
||||
} catch (err) {
|
||||
getEvent()?.addWarning(`Failed to upsert measure: ${err}`);
|
||||
|
||||
Reference in new issue
Block a user