mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(medications): return a medication's schedules in one order everywhere
The list reads ordered schedules by their window and id, but the detail, create, update, compliance, dose-history, GLP-1 and efficacy reads took them in whatever order Postgres found them, so two schedules created in the same millisecond could swap places between two answers. Every read now uses MEDICATION_SCHEDULE_ORDER, and the guard checks every schedules include, not only the list reads.
This commit is contained in:
1 parent
a42b29a160
commit
135d5baf28
10 files changed
+56
-36
No files matched your search
@@ -57,7 +57,6 @@ interface Read {
|
||||
model: string;
|
||||
orders: string[];
|
||||
variableOrder: boolean;
|
||||
unorderedSchedules: boolean;
|
||||
}
|
||||
|
||||
function reads(): Read[] {
|
||||
@@ -83,7 +82,6 @@ function reads(): Read[] {
|
||||
model: match[1]!,
|
||||
orders,
|
||||
variableOrder: /\borderBy\s*(?:,|\})/.test(call),
|
||||
unorderedSchedules: /\bschedules\s*:\s*true\b/.test(call),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -121,9 +119,24 @@ describe("medication reads order their ties", () => {
|
||||
});
|
||||
|
||||
it("never includes a medication's schedules unordered", () => {
|
||||
expect(
|
||||
ALL.filter((read) => read.unorderedSchedules).map((read) => read.where),
|
||||
).toEqual([]);
|
||||
// Any read or write that returns rows, not only the list reads above: a
|
||||
// `findUnique` or an `update` that includes the schedules hands them to a
|
||||
// client in whatever order Postgres found them.
|
||||
const unordered: string[] = [];
|
||||
for (const rel of walkSourceFiles(SRC, { floor: 2000 }).filter(
|
||||
(file) => !file.startsWith("generated/") && !file.includes("__tests__"),
|
||||
)) {
|
||||
const text = stripComments(readSourceText(join(SRC, rel)), rel);
|
||||
for (const match of text.matchAll(/\bschedules\s*:\s*true\b/g)) {
|
||||
const before = text.slice(Math.max(0, match.index! - 200), match.index);
|
||||
// `_count: { select: { schedules: true } }` counts; it returns no rows.
|
||||
if (/_count\s*:\s*\{\s*select\s*:\s*\{[^}]*$/.test(before)) continue;
|
||||
unordered.push(
|
||||
`${rel}:${text.slice(0, match.index).split("\n").length}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(unordered).toEqual([]);
|
||||
});
|
||||
|
||||
it("names every order held in a variable, and nothing else", () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
|
||||
@@ -71,7 +72,7 @@ export const GET = apiHandler(
|
||||
const medication = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
schedules: true,
|
||||
schedules: { orderBy: MEDICATION_SCHEDULE_ORDER },
|
||||
// v1.16.3 — archived schedule eras for era-aware compliance.
|
||||
scheduleRevisions: { orderBy: { validFrom: "asc" } },
|
||||
// v1.25 — pause eras so paused days drop out of the denominator.
|
||||
|
||||
@@ -48,6 +48,7 @@ import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import { dueSchedules } from "@/lib/medications/intake-tracking";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
|
||||
@@ -132,7 +133,7 @@ export const GET = apiHandler(
|
||||
const medication = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
schedules: true,
|
||||
schedules: { orderBy: MEDICATION_SCHEDULE_ORDER },
|
||||
// v1.16.3 — archived schedule eras: past days mint against the
|
||||
// schedule that was live THEN.
|
||||
scheduleRevisions: { orderBy: { validFrom: "asc" } },
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { NextRequest } from "next/server";
|
||||
import { dueSchedules } from "@/lib/medications/intake-tracking";
|
||||
import { floorWhole } from "@/lib/medications/units-per-dose";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
|
||||
@@ -90,7 +91,7 @@ export const GET = apiHandler(
|
||||
take: 12,
|
||||
select: { takenAt: true, injectionSite: true },
|
||||
},
|
||||
schedules: true,
|
||||
schedules: { orderBy: MEDICATION_SCHEDULE_ORDER },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ function insertMedication(
|
||||
create: scheduleInputs.map((s) => scheduleCreateData(s, oneShot)),
|
||||
},
|
||||
},
|
||||
include: { schedules: true },
|
||||
include: { schedules: { orderBy: MEDICATION_SCHEDULE_ORDER } },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,13 @@ import {
|
||||
type EfficacySegmented,
|
||||
type SegmentedEffectInput,
|
||||
} from "./segmented-effect";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** The medication shape the effect needs: the efficacy builder's include. */
|
||||
export const EFFECT_MEDICATION_INCLUDE = {
|
||||
schedules: true,
|
||||
schedules: { orderBy: MEDICATION_SCHEDULE_ORDER },
|
||||
scheduleRevisions: { orderBy: { validFrom: "asc" } },
|
||||
pauseEras: { select: { pausedAt: true, resumedAt: true } },
|
||||
courses: COURSES_COMPLIANCE_SELECT,
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
courseActionAuthority,
|
||||
withCourseActionAuthority,
|
||||
} from "@/lib/medications/course-authority";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
export type ReadUser = Pick<SessionUser, "id" | "timezone" | "locale">;
|
||||
type MedicationWithSchedules = Medication & {
|
||||
@@ -193,7 +194,7 @@ export async function readMedication(input: {
|
||||
const { user, grantId, id } = input;
|
||||
const medication = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
include: { schedules: true },
|
||||
include: { schedules: { orderBy: MEDICATION_SCHEDULE_ORDER } },
|
||||
});
|
||||
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
type EraTrail,
|
||||
type MedicationEditWrite,
|
||||
} from "./write";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
type EditUser = ReadUser;
|
||||
type EditedMedication = Awaited<ReturnType<typeof writeMedicationEdit>>;
|
||||
@@ -204,7 +205,7 @@ async function settleAfterCommit(
|
||||
});
|
||||
const refreshed = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
include: { schedules: true },
|
||||
include: { schedules: { orderBy: MEDICATION_SCHEDULE_ORDER } },
|
||||
});
|
||||
if (refreshed) {
|
||||
return refreshed;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { getUserTodayBounds } from "@/lib/tz/local-day";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
import type { EditableMedication } from "./preflight";
|
||||
import type { MedicationEdit, NormalisedSchedule } from "./schedule-normalise";
|
||||
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
|
||||
|
||||
/**
|
||||
* C7. What a schedule replacement destroys, captured for the audit row: the
|
||||
@@ -431,7 +432,7 @@ export async function writeMedicationEdit(edit: MedicationEditWrite) {
|
||||
const updated = await tx.medication.update({
|
||||
where: { id },
|
||||
data: { ...baseUpdateData, ...edit.pausedAtPatch },
|
||||
include: { schedules: true },
|
||||
include: { schedules: { orderBy: MEDICATION_SCHEDULE_ORDER } },
|
||||
});
|
||||
|
||||
await recordPauseTransition(tx, edit);
|
||||
|
||||
+23
-23
@@ -61,27 +61,6 @@
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"medicationId": "<id:1>",
|
||||
"windowStart": "21:00",
|
||||
"windowEnd": "22:00",
|
||||
"label": null,
|
||||
"dose": null,
|
||||
"unitsPerDose": null,
|
||||
"daysOfWeek": "i2;1,3,5",
|
||||
"timesOfDay": [
|
||||
"21:00"
|
||||
],
|
||||
"reminderGraceMinutes": null,
|
||||
"rrule": null,
|
||||
"rollingIntervalDays": null,
|
||||
"scheduleType": "SCHEDULED",
|
||||
"cyclicOnWeeks": null,
|
||||
"cyclicOffWeeks": null,
|
||||
"doseWindows": null,
|
||||
"resolvedUnitsPerDose": 1
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"medicationId": "<id:1>",
|
||||
"windowStart": "12:00",
|
||||
"windowEnd": "13:00",
|
||||
"label": null,
|
||||
@@ -99,6 +78,27 @@
|
||||
"cyclicOffWeeks": null,
|
||||
"doseWindows": null,
|
||||
"resolvedUnitsPerDose": 1
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"medicationId": "<id:1>",
|
||||
"windowStart": "21:00",
|
||||
"windowEnd": "22:00",
|
||||
"label": null,
|
||||
"dose": null,
|
||||
"unitsPerDose": null,
|
||||
"daysOfWeek": "i2;1,3,5",
|
||||
"timesOfDay": [
|
||||
"21:00"
|
||||
],
|
||||
"reminderGraceMinutes": null,
|
||||
"rrule": null,
|
||||
"rollingIntervalDays": null,
|
||||
"scheduleType": "SCHEDULED",
|
||||
"cyclicOnWeeks": null,
|
||||
"cyclicOffWeeks": null,
|
||||
"doseWindows": null,
|
||||
"resolvedUnitsPerDose": 1
|
||||
}
|
||||
],
|
||||
"category": "OTHER",
|
||||
@@ -433,7 +433,7 @@
|
||||
"doseWindows": null
|
||||
},
|
||||
{
|
||||
"id": "<id:5>",
|
||||
"id": "<id:4>",
|
||||
"medicationId": "<id:1>",
|
||||
"windowStart": "12:00",
|
||||
"windowEnd": "13:00",
|
||||
@@ -479,7 +479,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "<id:4>",
|
||||
"id": "<id:5>",
|
||||
"medicationId": "<id:1>",
|
||||
"windowStart": "21:00",
|
||||
"windowEnd": "22:00",
|
||||
|
||||
Reference in new issue
Block a user