mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Introduce the persistence layer for cycle tracking: eight enums (CycleTrackingGoal, FlowLevel, OvulationTest, CervicalMucus, HomeTestResult, ContraceptiveKind, CyclePhase, PredictionMethod) and seven models. CycleProfile is a 1:1 per-user settings row (lazily upserted on first access via getOrCreateCycleProfile). MenstrualCycle and CycleDayLog are the capture rows, carrying the MoodEntry soft-delete + syncVersion shape so they ride the sync delta feed; CycleDayLog repeats the MoodEntry dual-unique — the canonical (userId, date) key plus the NULL-distinct (userId, source, externalId) key for HealthKit re-ingest. Free-text notes encrypt at rest; queryable physiology (flow, BBT, OPK, mucus, dates) stays plaintext for the rollup and correlation tier. The symptom taxonomy (CycleSymptomCategory / CycleSymptom / CycleSymptomLink) clones the mood-tag trio and seeds fifteen symptoms across three categories with cycle.symptom.* i18n keys across all six locales. CyclePrediction caches one forecast per user, regenerated debounced. Migrations 0128-0130 are additive; the testcontainer integration suite applies the full history from scratch and passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 lines
1.5 KiB
TypeScript
37 lines
1.5 KiB
TypeScript
/**
|
|
* Cycle-tracking per-user settings row.
|
|
*
|
|
* `CycleProfile` is a 1:1 settings row (one per user, `@@unique(userId)`)
|
|
* that holds the goal + prediction / privacy preferences. There is no
|
|
* onboarding step that creates it eagerly; instead the read path lazily
|
|
* upserts a default row on first cycle-surface access, exactly like the
|
|
* other upsert-on-read settings rows in the tree (the Coach usage ledger
|
|
* in `src/lib/ai/coach/budget.ts`). The default goal is `GENERAL_HEALTH`
|
|
* — the inclusive, conception-framing-free default — and
|
|
* `sensitiveCategoryEncryption` defaults ON for this category given the
|
|
* reproductive-health threat model.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import type { CycleProfile } from "@/generated/prisma/client";
|
|
|
|
/**
|
|
* Return the user's `CycleProfile`, creating a default row on first
|
|
* access. Idempotent + race-safe: the `(userId)` unique lets the upsert
|
|
* collapse concurrent first-reads to a single row (a second caller's
|
|
* `create` loses the race and falls through to the no-op update).
|
|
*/
|
|
export async function getOrCreateCycleProfile(
|
|
userId: string,
|
|
): Promise<CycleProfile> {
|
|
return prisma.cycleProfile.upsert({
|
|
where: { userId },
|
|
// Field-by-field create (no mass assignment): every default is the
|
|
// schema default, spelled out so the row shape is explicit at the
|
|
// call site.
|
|
create: { userId },
|
|
// No-op on an existing row — `update: {}` keeps the upsert a pure
|
|
// get-or-create without touching the stored preferences.
|
|
update: {},
|
|
});
|
|
}
|