mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
fix: answer a duplicate measurement with 409 on both write paths
The batch path had no unique-violation handling at all, so one duplicate anywhere in the transaction answered 500 — indistinguishable from a server fault, and what every client retries against. The single path narrowed with instanceof, the form prisma-errors.ts exists to replace.
This commit is contained in:
1 parent
bcf2322f65
commit
c11c462e57
2 files changed
+219
-28
No files matched your search
@@ -30,6 +30,7 @@ import {
|
||||
localDayWindow,
|
||||
} from "@/lib/measurements/drain-per-sample-cumulative";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { isP2002 } from "@/lib/prisma-errors";
|
||||
import { encryptNote, shapeMeasurementNotes } from "@/lib/crypto/note-cipher";
|
||||
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit";
|
||||
@@ -797,30 +798,45 @@ async function postMeasurement(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const results = await prisma.$transaction(
|
||||
parsed.data.measurements.map((m) =>
|
||||
prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: m.type as MeasurementType,
|
||||
value: m.value,
|
||||
unit: getUnitForType(m.type),
|
||||
source: (m.source ?? "MANUAL") as MeasurementSource,
|
||||
measuredAt: m.measuredAt,
|
||||
// v1.23 — encrypt the note at rest; the legacy plaintext column is
|
||||
// written null for new rows.
|
||||
notes: null,
|
||||
notesEncrypted: encryptNote(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,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
// The whole batch is one transaction, so a duplicate anywhere in it rolls
|
||||
// the rest back — and the caller has to be told which condition it hit.
|
||||
// Untouched, that duplicate escaped as an unhandled error and the batch
|
||||
// answered 500, which is indistinguishable from a server fault and is what
|
||||
// every client retries against.
|
||||
let results;
|
||||
try {
|
||||
results = await prisma.$transaction(
|
||||
parsed.data.measurements.map((m) =>
|
||||
prisma.measurement.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: m.type as MeasurementType,
|
||||
value: m.value,
|
||||
unit: getUnitForType(m.type),
|
||||
source: (m.source ?? "MANUAL") as MeasurementSource,
|
||||
measuredAt: m.measuredAt,
|
||||
// v1.23 — encrypt the note at rest; the legacy plaintext column is
|
||||
// written null for new rows.
|
||||
notes: null,
|
||||
notesEncrypted: encryptNote(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,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (isP2002(err)) {
|
||||
return apiError("A measurement with this data already exists", 409, {
|
||||
errorCode: "measurement.duplicate_timestamp",
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const shapedResults = results.map(shapeMeasurementNotes);
|
||||
|
||||
@@ -984,10 +1000,13 @@ async function postMeasurement(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
err.code === "P2002"
|
||||
) {
|
||||
// `isP2002` rather than `instanceof Prisma.PrismaClientKnownRequestError`.
|
||||
// The helper's own docblock says why it exists: the structural check
|
||||
// survives client-bundling quirks, and the `instanceof` form does not —
|
||||
// when it misses, a duplicate escapes as an unhandled error and the caller
|
||||
// gets a 500 for a condition the route has always meant to answer with a
|
||||
// 409. A retry loop that meets a 500 keeps retrying.
|
||||
if (isP2002(err)) {
|
||||
return apiError("A measurement with this data already exists", 409, {
|
||||
errorCode: "measurement.duplicate_timestamp",
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user