mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
1 parent
b3fea2ef59
commit
1f4ba5939d
1000 files changed
+12130
-8385
No files matched your search
@@ -1,9 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createBiomarkerSchema,
|
||||
updateBiomarkerSchema,
|
||||
} from "../biomarkers";
|
||||
import { createBiomarkerSchema, updateBiomarkerSchema } from "../biomarkers";
|
||||
|
||||
describe("createBiomarkerSchema", () => {
|
||||
it("accepts a minimal marker (name + unit only)", () => {
|
||||
|
||||
@@ -12,17 +12,16 @@ const NOW = Date.parse("2026-06-14T12:00:00.000Z");
|
||||
|
||||
describe("isPlausibleEntryInstant", () => {
|
||||
it("accepts a recent past instant", () => {
|
||||
expect(
|
||||
isPlausibleEntryInstant(new Date(NOW - 60_000), { now: NOW }),
|
||||
).toBe(true);
|
||||
expect(isPlausibleEntryInstant(new Date(NOW - 60_000), { now: NOW })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts an instant inside the clock-skew tolerance", () => {
|
||||
expect(
|
||||
isPlausibleEntryInstant(
|
||||
new Date(NOW + ENTRY_INSTANT_CLOCK_SKEW_MS - 1),
|
||||
{ now: NOW },
|
||||
),
|
||||
isPlausibleEntryInstant(new Date(NOW + ENTRY_INSTANT_CLOCK_SKEW_MS - 1), {
|
||||
now: NOW,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -37,10 +36,9 @@ describe("isPlausibleEntryInstant", () => {
|
||||
|
||||
it("rejects an instant before the 1900 far-past floor", () => {
|
||||
expect(
|
||||
isPlausibleEntryInstant(
|
||||
new Date(ENTRY_INSTANT_FAR_PAST.getTime() - 1),
|
||||
{ now: NOW },
|
||||
),
|
||||
isPlausibleEntryInstant(new Date(ENTRY_INSTANT_FAR_PAST.getTime() - 1), {
|
||||
now: NOW,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -462,7 +462,10 @@ describe("measurement validation", () => {
|
||||
const issue = parsed.error.issues.find((i) =>
|
||||
i.path.includes("source"),
|
||||
);
|
||||
expect(issue, `expected a source-path rejection for ${source}`).toBeDefined();
|
||||
expect(
|
||||
issue,
|
||||
`expected a source-path rejection for ${source}`,
|
||||
).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,12 +103,12 @@ describe("medication schemas — dosesPerUnit cap 1000 + unitsPerDose 1–100",
|
||||
});
|
||||
|
||||
it("update accepts dosesPerUnit 1000 and rejects 1001", () => {
|
||||
expect(updateMedicationSchema.safeParse({ dosesPerUnit: 1000 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(updateMedicationSchema.safeParse({ dosesPerUnit: 1001 }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
updateMedicationSchema.safeParse({ dosesPerUnit: 1000 }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
updateMedicationSchema.safeParse({ dosesPerUnit: 1001 }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("create bounds unitsPerDose to 1–100", () => {
|
||||
@@ -167,9 +167,9 @@ describe("medication schemas — dosesPerUnit cap 1000 + unitsPerDose 1–100",
|
||||
unitsPerDose: v,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(updateMedicationSchema.safeParse({ unitsPerDose: v }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
updateMedicationSchema.safeParse({ unitsPerDose: v }).success,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,9 +196,21 @@ describe("medication schemas — dosesPerUnit cap 1000 + unitsPerDose 1–100",
|
||||
|
||||
describe("glp1InventoryPostSchema — the legacy pen-ledger delta keeps ±100", () => {
|
||||
it("accepts ±100 and rejects ±101", () => {
|
||||
expect(glp1InventoryPostSchema.safeParse({ delta: 100, reason: "purchased" }).success).toBe(true);
|
||||
expect(glp1InventoryPostSchema.safeParse({ delta: -100, reason: "damaged" }).success).toBe(true);
|
||||
expect(glp1InventoryPostSchema.safeParse({ delta: 101, reason: "purchased" }).success).toBe(false);
|
||||
expect(glp1InventoryPostSchema.safeParse({ delta: -101, reason: "damaged" }).success).toBe(false);
|
||||
expect(
|
||||
glp1InventoryPostSchema.safeParse({ delta: 100, reason: "purchased" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
glp1InventoryPostSchema.safeParse({ delta: -100, reason: "damaged" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
glp1InventoryPostSchema.safeParse({ delta: 101, reason: "purchased" })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
glp1InventoryPostSchema.safeParse({ delta: -101, reason: "damaged" })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,8 @@ describe("createMoodEntrySchema — note + structured tagKeys (v1.8.5)", () => {
|
||||
|
||||
it("rejects an over-long note and an over-large tagKeys set", () => {
|
||||
expect(
|
||||
createMoodEntrySchema.safeParse({ ...base, note: "x".repeat(501) }).success,
|
||||
createMoodEntrySchema.safeParse({ ...base, note: "x".repeat(501) })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
createMoodEntrySchema.safeParse({
|
||||
|
||||
@@ -242,8 +242,12 @@ describe("DEFAULT_SOURCE_PRIORITY", () => {
|
||||
const ladder = DEFAULT_SOURCE_PRIORITY[key];
|
||||
expect(ladder).toContain("OURA");
|
||||
expect(ladder).toContain("POLAR");
|
||||
expect(ladder.indexOf("OURA")).toBeLessThan(ladder.indexOf("APPLE_HEALTH"));
|
||||
expect(ladder.indexOf("POLAR")).toBeLessThan(ladder.indexOf("APPLE_HEALTH"));
|
||||
expect(ladder.indexOf("OURA")).toBeLessThan(
|
||||
ladder.indexOf("APPLE_HEALTH"),
|
||||
);
|
||||
expect(ladder.indexOf("POLAR")).toBeLessThan(
|
||||
ladder.indexOf("APPLE_HEALTH"),
|
||||
);
|
||||
expect(ladder.indexOf("WHOOP")).toBeLessThan(ladder.indexOf("OURA"));
|
||||
}
|
||||
});
|
||||
@@ -451,10 +455,7 @@ describe("getSourceLadder + getDeviceTypeLadder helpers", () => {
|
||||
WEIGHT: ["scale", "watch"],
|
||||
},
|
||||
});
|
||||
expect(getDeviceTypeLadder(resolved, "WEIGHT")).toEqual([
|
||||
"scale",
|
||||
"watch",
|
||||
]);
|
||||
expect(getDeviceTypeLadder(resolved, "WEIGHT")).toEqual(["scale", "watch"]);
|
||||
});
|
||||
|
||||
it("getDeviceTypeLadder falls back to the user default ladder", () => {
|
||||
@@ -501,9 +502,9 @@ describe("parseSourcePriority — stored-ladder reconciliation (v1.16.11)", () =
|
||||
expect(resolved.metricPriority.sleep).toContain("WHOOP");
|
||||
expect(resolved.metricPriority.sleep).toContain("FITBIT");
|
||||
expect(resolved.metricPriority.sleep.indexOf("WHOOP")).toBeGreaterThan(3);
|
||||
expect(
|
||||
resolved.metricPriority.sleep.indexOf("WHOOP"),
|
||||
).toBeLessThan(resolved.metricPriority.sleep.indexOf("FITBIT"));
|
||||
expect(resolved.metricPriority.sleep.indexOf("WHOOP")).toBeLessThan(
|
||||
resolved.metricPriority.sleep.indexOf("FITBIT"),
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves a complete stored ladder byte-identical", () => {
|
||||
@@ -529,13 +530,7 @@ describe("parseSourcePriority — duplicate tolerance (v1.16.11)", () => {
|
||||
it("collapses duplicates so a resolved ladder can never overflow the schema cap", () => {
|
||||
const resolved = parseSourcePriority({
|
||||
metricPriority: {
|
||||
weight: [
|
||||
"MANUAL",
|
||||
"MANUAL",
|
||||
"WITHINGS",
|
||||
"MANUAL",
|
||||
"WITHINGS",
|
||||
],
|
||||
weight: ["MANUAL", "MANUAL", "WITHINGS", "MANUAL", "WITHINGS"],
|
||||
},
|
||||
});
|
||||
const ladder = resolved.metricPriority.weight;
|
||||
|
||||
@@ -135,9 +135,7 @@ describe("workoutHrSamplesSchema", () => {
|
||||
|
||||
it("rejects an out-of-range heart rate", () => {
|
||||
expect(() =>
|
||||
workoutHrSamplesSchema.parse([
|
||||
{ t: "2026-05-14T07:00:00.000Z", hr: 5 },
|
||||
]),
|
||||
workoutHrSamplesSchema.parse([{ t: "2026-05-14T07:00:00.000Z", hr: 5 }]),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
@@ -155,13 +153,10 @@ describe("workoutHrSamplesSchema", () => {
|
||||
});
|
||||
|
||||
it("accepts a series exactly at the cap", () => {
|
||||
const atCap = Array.from(
|
||||
{ length: MAX_WORKOUT_HR_SAMPLES },
|
||||
(_v, i) => ({
|
||||
t: new Date(Date.UTC(2026, 4, 14, 7, 0, 0) + i * 1000).toISOString(),
|
||||
hr: 140,
|
||||
}),
|
||||
);
|
||||
const atCap = Array.from({ length: MAX_WORKOUT_HR_SAMPLES }, (_v, i) => ({
|
||||
t: new Date(Date.UTC(2026, 4, 14, 7, 0, 0) + i * 1000).toISOString(),
|
||||
hr: 140,
|
||||
}));
|
||||
expect(workoutHrSamplesSchema.parse(atCap)).toHaveLength(
|
||||
MAX_WORKOUT_HR_SAMPLES,
|
||||
);
|
||||
@@ -370,9 +365,8 @@ describe("createBatchWorkoutSchema", () => {
|
||||
});
|
||||
|
||||
it("accepts a batch at the cap (100 workouts)", () => {
|
||||
const workouts = Array.from(
|
||||
{ length: MAX_WORKOUTS_PER_BATCH },
|
||||
(_, i) => minimalRun(`hk-uuid-${i}`),
|
||||
const workouts = Array.from({ length: MAX_WORKOUTS_PER_BATCH }, (_, i) =>
|
||||
minimalRun(`hk-uuid-${i}`),
|
||||
);
|
||||
const parsed = createBatchWorkoutSchema.parse({ workouts });
|
||||
expect(parsed.workouts).toHaveLength(MAX_WORKOUTS_PER_BATCH);
|
||||
|
||||
@@ -44,13 +44,10 @@ export const consentPostBody = z.object({
|
||||
artefact: z
|
||||
.string()
|
||||
.min(1, "artefact must not be empty")
|
||||
.refine(
|
||||
(value) => Buffer.byteLength(value, "utf8") <= ARTEFACT_MAX_BYTES,
|
||||
{ message: "artefact exceeds 64 KB cap (UTF-8 byte length)" },
|
||||
),
|
||||
signedAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
.transform((s) => new Date(s)),
|
||||
.refine((value) => Buffer.byteLength(value, "utf8") <= ARTEFACT_MAX_BYTES, {
|
||||
message: "artefact exceeds 64 KB cap (UTF-8 byte length)",
|
||||
}),
|
||||
signedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)),
|
||||
});
|
||||
|
||||
export type ConsentPostBody = z.infer<typeof consentPostBody>;
|
||||
|
||||
@@ -117,16 +117,12 @@ export const illnessEpisodeResolveSchema = z.object({
|
||||
resolvedAt: boundedInstant.optional(),
|
||||
});
|
||||
|
||||
export type IllnessEpisodeResolve = z.infer<
|
||||
typeof illnessEpisodeResolveSchema
|
||||
>;
|
||||
export type IllnessEpisodeResolve = z.infer<typeof illnessEpisodeResolveSchema>;
|
||||
|
||||
/** History/list query — newest-first, bounded. */
|
||||
export const illnessEpisodeListQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional(),
|
||||
includeResolved: z
|
||||
.union([z.literal("true"), z.literal("false")])
|
||||
.optional(),
|
||||
includeResolved: z.union([z.literal("true"), z.literal("false")]).optional(),
|
||||
});
|
||||
|
||||
/* ── day-log capture (one row per day of an episode) ─────────────── */
|
||||
|
||||
@@ -18,12 +18,7 @@ export const INVITE_MAX_USES_CAP = 50;
|
||||
export const inviteCreateSchema = z.object({
|
||||
/** Lifetime in days. The admin UI offers 7 / 14 / 30; the API accepts
|
||||
* any integer 1–30 so an operator can script shorter invites. */
|
||||
expiresInDays: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(INVITE_MAX_TTL_DAYS)
|
||||
.default(7),
|
||||
expiresInDays: z.number().int().min(1).max(INVITE_MAX_TTL_DAYS).default(7),
|
||||
/** How many signups this invite admits. Default: single-use. */
|
||||
maxUses: z.number().int().min(1).max(INVITE_MAX_USES_CAP).default(1),
|
||||
});
|
||||
|
||||
@@ -36,12 +36,7 @@ const takenAtField = validateEntryInstant(
|
||||
);
|
||||
|
||||
/** Trimmed, bounded free-text field with a min-length guard. */
|
||||
const requiredText = (max: number) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(max);
|
||||
const requiredText = (max: number) => z.string().trim().min(1).max(max);
|
||||
|
||||
const optionalPanel = z
|
||||
.string()
|
||||
@@ -160,7 +155,9 @@ export function isInvertedRange(
|
||||
effectiveHigh: number | null,
|
||||
): boolean {
|
||||
return (
|
||||
effectiveLow !== null && effectiveHigh !== null && effectiveLow > effectiveHigh
|
||||
effectiveLow !== null &&
|
||||
effectiveHigh !== null &&
|
||||
effectiveLow > effectiveHigh
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -183,10 +183,7 @@ export const measurementSourceEnum = z.enum([
|
||||
* POST validates `source` against this set; the batch route mirrors the same
|
||||
* exclusion with its own narrower `{APPLE_HEALTH, MANUAL}` allowlist.
|
||||
*/
|
||||
export const WRITABLE_MEASUREMENT_SOURCES = [
|
||||
"MANUAL",
|
||||
"APPLE_HEALTH",
|
||||
] as const;
|
||||
export const WRITABLE_MEASUREMENT_SOURCES = ["MANUAL", "APPLE_HEALTH"] as const;
|
||||
|
||||
/**
|
||||
* v1.10.0 QA — the Zod enum the single-entry POST validates `source` against.
|
||||
@@ -698,14 +695,11 @@ export const listMeasurementsSchema = z
|
||||
// the admin drain route, so a malformed CLI invocation has the
|
||||
// same blast radius. Reject the impossible shapes at the
|
||||
// validator instead.
|
||||
.refine(
|
||||
(s) => {
|
||||
const parsed = new Date(`${s}T00:00:00Z`);
|
||||
if (Number.isNaN(parsed.getTime())) return false;
|
||||
return s === parsed.toISOString().slice(0, 10);
|
||||
},
|
||||
"dayKey must be a real calendar date (YYYY-MM-DD)",
|
||||
)
|
||||
.refine((s) => {
|
||||
const parsed = new Date(`${s}T00:00:00Z`);
|
||||
if (Number.isNaN(parsed.getTime())) return false;
|
||||
return s === parsed.toISOString().slice(0, 10);
|
||||
}, "dayKey must be a real calendar date (YYYY-MM-DD)")
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
|
||||
@@ -85,7 +85,8 @@ export const MEDICATION_CATEGORY_VALUES = [
|
||||
"ANTIBIOTIC",
|
||||
"OTHER",
|
||||
] as const;
|
||||
export type MedicationCategoryValue = (typeof MEDICATION_CATEGORY_VALUES)[number];
|
||||
export type MedicationCategoryValue =
|
||||
(typeof MEDICATION_CATEGORY_VALUES)[number];
|
||||
|
||||
/**
|
||||
* v1.4.25 W4d — Prisma-level treatment class. Orthogonal to
|
||||
@@ -116,9 +117,7 @@ export const UNITS_PER_DOSE_FRACTIONS = [
|
||||
export const UNITS_PER_DOSE_MAX_WHOLE = 100;
|
||||
|
||||
export function isSupportedUnitsPerDose(value: number): boolean {
|
||||
if (
|
||||
(UNITS_PER_DOSE_FRACTIONS as readonly number[]).includes(value)
|
||||
) {
|
||||
if ((UNITS_PER_DOSE_FRACTIONS as readonly number[]).includes(value)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
@@ -168,7 +167,9 @@ export const doseWindowEntrySchema = z
|
||||
timeOfDay: z
|
||||
.string()
|
||||
.regex(timeRegex, "Format: HH:mm")
|
||||
.describe("The dose time this window applies to (matches a `timesOfDay` entry)."),
|
||||
.describe(
|
||||
"The dose time this window applies to (matches a `timesOfDay` entry).",
|
||||
),
|
||||
start: z
|
||||
.string()
|
||||
.regex(timeRegex, "Format: HH:mm")
|
||||
@@ -176,7 +177,9 @@ export const doseWindowEntrySchema = z
|
||||
end: z
|
||||
.string()
|
||||
.regex(timeRegex, "Format: HH:mm")
|
||||
.describe("On-time band upper bound (HH:mm, user local). Must be >= `start`."),
|
||||
.describe(
|
||||
"On-time band upper bound (HH:mm, user local). Must be >= `start`.",
|
||||
),
|
||||
})
|
||||
.refine((w) => hhmmToMinutes(w.start) <= hhmmToMinutes(w.end), {
|
||||
message: "Window start must be on or before end (same day)",
|
||||
@@ -241,7 +244,7 @@ export const scheduleSchema = z
|
||||
.string()
|
||||
.max(50)
|
||||
.optional()
|
||||
.describe("Optional human label (e.g. \"Morning\", \"Evening\")."),
|
||||
.describe('Optional human label (e.g. "Morning", "Evening").'),
|
||||
dose: z
|
||||
.string()
|
||||
.max(50)
|
||||
@@ -336,7 +339,7 @@ export const scheduleSchema = z
|
||||
.max(52)
|
||||
.optional()
|
||||
.describe(
|
||||
"Cyclic \"on\" weeks (1..52). Required when `scheduleType` is CYCLIC; ignored otherwise.",
|
||||
'Cyclic "on" weeks (1..52). Required when `scheduleType` is CYCLIC; ignored otherwise.',
|
||||
),
|
||||
/** v1.7.0 — cyclic "off" weeks. Required when `scheduleType === "CYCLIC"`. */
|
||||
cyclicOffWeeks: z
|
||||
@@ -346,7 +349,7 @@ export const scheduleSchema = z
|
||||
.max(52)
|
||||
.optional()
|
||||
.describe(
|
||||
"Cyclic \"off\" weeks (0..52). Required when `scheduleType` is CYCLIC; ignored otherwise.",
|
||||
'Cyclic "off" weeks (0..52). Required when `scheduleType` is CYCLIC; ignored otherwise.',
|
||||
),
|
||||
/**
|
||||
* v1.15.18 — per-dose configurable on-time intake window (the maintainer's
|
||||
@@ -364,14 +367,10 @@ export const scheduleSchema = z
|
||||
"Per-dose on-time intake windows. Each `{ timeOfDay, start, end }` HH:mm triple sets the explicit on-time band for the matching dose time; a dose time with no entry keeps the symmetric ±1h default. `timeOfDay` must match one of `timesOfDay` (or `windowStart`); `start <= end`. Up to 8 entries. Absent leaves every slot on the default derivation. The late tail stays cadence-derived.",
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(s) => !(s.rrule && s.rollingIntervalDays),
|
||||
{
|
||||
message:
|
||||
"A schedule can be calendar-anchored (rrule) or rolling, not both",
|
||||
path: ["rrule"],
|
||||
},
|
||||
)
|
||||
.refine((s) => !(s.rrule && s.rollingIntervalDays), {
|
||||
message: "A schedule can be calendar-anchored (rrule) or rolling, not both",
|
||||
path: ["rrule"],
|
||||
})
|
||||
.refine(
|
||||
(s) =>
|
||||
s.scheduleType !== "CYCLIC" ||
|
||||
@@ -409,12 +408,15 @@ export const scheduleSchema = z
|
||||
// (mirrors the engine's `effectiveTimesOfDay`).
|
||||
if (!s.doseWindows || s.doseWindows.length === 0) return true;
|
||||
const times = new Set(
|
||||
s.timesOfDay && s.timesOfDay.length > 0 ? s.timesOfDay : [s.windowStart],
|
||||
s.timesOfDay && s.timesOfDay.length > 0
|
||||
? s.timesOfDay
|
||||
: [s.windowStart],
|
||||
);
|
||||
return s.doseWindows.every((w) => times.has(w.timeOfDay));
|
||||
},
|
||||
{
|
||||
message: "Each doseWindows.timeOfDay must match one of the schedule's timesOfDay",
|
||||
message:
|
||||
"Each doseWindows.timeOfDay must match one of the schedule's timesOfDay",
|
||||
path: ["doseWindows"],
|
||||
},
|
||||
)
|
||||
@@ -614,16 +616,14 @@ export const createMedicationSchema = z
|
||||
path: ["asNeeded"],
|
||||
})
|
||||
.refine(
|
||||
(b) =>
|
||||
b.asNeeded === true || (!!b.schedules && b.schedules.length >= 1),
|
||||
(b) => b.asNeeded === true || (!!b.schedules && b.schedules.length >= 1),
|
||||
{
|
||||
message: "Mindestens ein Zeitfenster",
|
||||
path: ["schedules"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(b) =>
|
||||
b.asNeeded !== true || !b.schedules || b.schedules.length === 0,
|
||||
(b) => b.asNeeded !== true || !b.schedules || b.schedules.length === 0,
|
||||
{
|
||||
message: AS_NEEDED_SCHEDULES_MESSAGE,
|
||||
path: ["schedules"],
|
||||
@@ -632,7 +632,7 @@ export const createMedicationSchema = z
|
||||
.meta({
|
||||
id: "CreateMedicationRequest",
|
||||
description:
|
||||
"Create-medication body. The route enforces the v1.5 cross-field invariants on top of the per-schedule `rrule_xor_rolling` Zod refine: a `oneShot:true` medication may carry at most one schedule and that schedule must not declare a recurrence; `endsOn` is normalised to equal `startsOn` for one-shot doses; a recurring schedule with no `rrule`, `rollingIntervalDays`, or legacy `daysOfWeek` defaults to `rrule = \"FREQ=DAILY\"`; and `timesOfDay` is dual-written from `windowStart` when the caller omits it. v1.16.11 — `asNeeded: true` creates a PRN medication with ZERO schedules (`schedules` must be absent or empty, 422 otherwise); a scheduled medication still requires at least one schedule entry.",
|
||||
'Create-medication body. The route enforces the v1.5 cross-field invariants on top of the per-schedule `rrule_xor_rolling` Zod refine: a `oneShot:true` medication may carry at most one schedule and that schedule must not declare a recurrence; `endsOn` is normalised to equal `startsOn` for one-shot doses; a recurring schedule with no `rrule`, `rollingIntervalDays`, or legacy `daysOfWeek` defaults to `rrule = "FREQ=DAILY"`; and `timesOfDay` is dual-written from `windowStart` when the caller omits it. v1.16.11 — `asNeeded: true` creates a PRN medication with ZERO schedules (`schedules` must be absent or empty, 422 otherwise); a scheduled medication still requires at least one schedule entry.',
|
||||
});
|
||||
|
||||
export const updateMedicationSchema = z
|
||||
@@ -742,8 +742,7 @@ export const updateMedicationSchema = z
|
||||
path: ["asNeeded"],
|
||||
})
|
||||
.refine(
|
||||
(b) =>
|
||||
b.asNeeded !== true || !b.schedules || b.schedules.length === 0,
|
||||
(b) => b.asNeeded !== true || !b.schedules || b.schedules.length === 0,
|
||||
{
|
||||
message: AS_NEEDED_SCHEDULES_MESSAGE,
|
||||
path: ["schedules"],
|
||||
|
||||
@@ -269,7 +269,11 @@ export const webhookSettingsSchema = z.object({
|
||||
* the only per-user value is the recipient address.
|
||||
*/
|
||||
export const emailSettingsSchema = z.object({
|
||||
recipient: z.email("Ungültige E-Mail-Adresse").max(254).optional().or(z.literal("")),
|
||||
recipient: z
|
||||
.email("Ungültige E-Mail-Adresse")
|
||||
.max(254)
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ export const scheduleRevisionCreateSchema = z
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(body) => new Date(body.validFrom).getTime() < new Date(body.validUntil).getTime(),
|
||||
(body) =>
|
||||
new Date(body.validFrom).getTime() < new Date(body.validUntil).getTime(),
|
||||
{
|
||||
message: "validFrom must lie before validUntil",
|
||||
path: ["validUntil"],
|
||||
@@ -80,12 +81,11 @@ export type ScheduleRevisionCreateInput = z.infer<
|
||||
* row. Field-for-field identical to the create schema — only the meta
|
||||
* id differs so the OpenAPI document names the two requests apart.
|
||||
*/
|
||||
export const scheduleRevisionUpdateSchema = scheduleRevisionCreateSchema
|
||||
.meta({
|
||||
id: "UpdateScheduleRevisionRequest",
|
||||
description:
|
||||
"Corrected schedule-era payload: replacement era bounds plus the daily dose times that were live during the era.",
|
||||
});
|
||||
export const scheduleRevisionUpdateSchema = scheduleRevisionCreateSchema.meta({
|
||||
id: "UpdateScheduleRevisionRequest",
|
||||
description:
|
||||
"Corrected schedule-era payload: replacement era bounds plus the daily dose times that were live during the era.",
|
||||
});
|
||||
|
||||
export type ScheduleRevisionUpdateInput = z.infer<
|
||||
typeof scheduleRevisionUpdateSchema
|
||||
|
||||
@@ -22,12 +22,7 @@ export const seriesBatchQuerySchema = z
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0),
|
||||
)
|
||||
.pipe(
|
||||
z
|
||||
.array(measurementTypeEnum)
|
||||
.min(1)
|
||||
.max(SERIES_BATCH_MAX_TYPES),
|
||||
)
|
||||
.pipe(z.array(measurementTypeEnum).min(1).max(SERIES_BATCH_MAX_TYPES))
|
||||
.describe(
|
||||
"Comma-separated list of MeasurementType values (1.." +
|
||||
`${SERIES_BATCH_MAX_TYPES}). Each is read as a daily series through ` +
|
||||
|
||||
@@ -51,9 +51,7 @@ import {
|
||||
// The server logging module registers the real `annotate` on first import
|
||||
// (via `registerSourcePriorityParseObserver`); the browser leaves it as
|
||||
// the no-op default and never traces `node:async_hooks`.
|
||||
type ParseFailureObserver = (entry: {
|
||||
meta: Record<string, unknown>;
|
||||
}) => void;
|
||||
type ParseFailureObserver = (entry: { meta: Record<string, unknown> }) => void;
|
||||
let parseFailureObserver: ParseFailureObserver = () => {};
|
||||
|
||||
/**
|
||||
@@ -142,10 +140,12 @@ export type SourcePriorityMetricKey =
|
||||
// from `SOURCE_PRIORITY_METRIC_KEYS` so the schema and the enum cannot
|
||||
// drift. Adding a new metric class is now a single-line constant edit
|
||||
// instead of a two-place change that the lint chain doesn't enforce.
|
||||
const metricPriorityShape: Record<SourcePriorityMetricKey, typeof metricSourceLadder> =
|
||||
Object.fromEntries(
|
||||
SOURCE_PRIORITY_METRIC_KEYS.map((key) => [key, metricSourceLadder]),
|
||||
) as Record<SourcePriorityMetricKey, typeof metricSourceLadder>;
|
||||
const metricPriorityShape: Record<
|
||||
SourcePriorityMetricKey,
|
||||
typeof metricSourceLadder
|
||||
> = Object.fromEntries(
|
||||
SOURCE_PRIORITY_METRIC_KEYS.map((key) => [key, metricSourceLadder]),
|
||||
) as Record<SourcePriorityMetricKey, typeof metricSourceLadder>;
|
||||
|
||||
const metricPriorityObjectSchema = z.object(metricPriorityShape).partial();
|
||||
|
||||
@@ -222,7 +222,14 @@ export const DEFAULT_SOURCE_PRIORITY: Required<MetricPriority> = {
|
||||
// steps) but as legitimate sources above MANUAL, so an Oura/Polar-only day
|
||||
// is not dropped when another source coexists.
|
||||
steps: ["APPLE_HEALTH", "WITHINGS", "FITBIT", "OURA", "POLAR", "MANUAL"],
|
||||
activeEnergy: ["APPLE_HEALTH", "WITHINGS", "FITBIT", "OURA", "POLAR", "MANUAL"],
|
||||
activeEnergy: [
|
||||
"APPLE_HEALTH",
|
||||
"WITHINGS",
|
||||
"FITBIT",
|
||||
"OURA",
|
||||
"POLAR",
|
||||
"MANUAL",
|
||||
],
|
||||
walkingRunningDistance: [
|
||||
"APPLE_HEALTH",
|
||||
"WITHINGS",
|
||||
@@ -263,7 +270,15 @@ export const DEFAULT_SOURCE_PRIORITY: Required<MetricPriority> = {
|
||||
// Withings ScanWatch pulse-ox is the primary SpO2 sensor; WHOOP second,
|
||||
// Fitbit third, Oura's ring fourth. v1.17.1 — Polar Elixir pulse-ox slots
|
||||
// below the established straps and above the iPhone-relay / manual sources.
|
||||
spo2: ["WITHINGS", "WHOOP", "FITBIT", "OURA", "POLAR", "APPLE_HEALTH", "MANUAL"],
|
||||
spo2: [
|
||||
"WITHINGS",
|
||||
"WHOOP",
|
||||
"FITBIT",
|
||||
"OURA",
|
||||
"POLAR",
|
||||
"APPLE_HEALTH",
|
||||
"MANUAL",
|
||||
],
|
||||
vo2Max: ["WITHINGS", "APPLE_HEALTH", "FITBIT", "MANUAL"],
|
||||
// v1.11.0 — new WHOOP-overlapping keys. ScanWatch dermal reading is the
|
||||
// primary skin-temperature sensor; WHOOP's strap is second, Fitbit third,
|
||||
@@ -370,7 +385,11 @@ export function parseSourcePriority(raw: unknown): ResolvedSourcePriority {
|
||||
deviceTypePriority,
|
||||
...flatMetricLadder
|
||||
} = parsed.data;
|
||||
return buildResolved(flatMetricLadder, nested ?? {}, deviceTypePriority ?? {});
|
||||
return buildResolved(
|
||||
flatMetricLadder,
|
||||
nested ?? {},
|
||||
deviceTypePriority ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
function buildResolved(
|
||||
|
||||
@@ -226,10 +226,7 @@ export const MAX_WORKOUTS_PER_BATCH = 100;
|
||||
* workout; an optional nested `route` carries the GeoJSON LineString.
|
||||
*/
|
||||
export const createBatchWorkoutSchema = z.object({
|
||||
workouts: z
|
||||
.array(createWorkoutSchema)
|
||||
.min(1)
|
||||
.max(MAX_WORKOUTS_PER_BATCH),
|
||||
workouts: z.array(createWorkoutSchema).min(1).max(MAX_WORKOUTS_PER_BATCH),
|
||||
});
|
||||
|
||||
export type CreateBatchWorkoutInput = z.infer<typeof createBatchWorkoutSchema>;
|
||||
Reference in new issue
Block a user