Files
HealthLog/src/lib/cycle/profile.ts
T
Marc-André BombeckandClaude Opus 4.8 a5946e6209 feat(cycle): add cycle-tracking schema foundation
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>
2026-06-06 08:19:32 +02:00

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: {},
});
}