mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
The setup questionnaire the design spec asks for needs somewhere to put its answers. `OnboardingRecord` is that place: one row per RECORD rather than per person, so a guardian running a managed profile's setup writes the child's row and not their own, and the three JSON columns hold the answers, the nine ordered steps with their status, and the one task the flow ends on. Nothing here is encrypted, and the schema comment says why: the columns hold closed enum vocabularies, not free text, which is the same class of value as the module map and the goal slugs sitting in plaintext beside them. The needs to modules map lives next to the module keys rather than in the flow, because a module renamed or retired has to break the map at compile time rather than leave a chip that quietly changes nothing. The derivation is total — every directly-owned key gets an explicit value — and the merge beside it is one-directional: what the answers name is switched on, what they do not name is switched off only where the record does not already carry a deliberate on. The two delegated keys are absent by construction: `cycle` is reported separately because its real column is on the cycle profile, and the Coach is not offered by this flow at all. Readers are fail-soft. They sit on the payload that rides every app boot, and a value written by a release that named an area this one retired must not take the shell down with it. Nothing is retired: the four-step wizard, its goal slugs and the one-time tile seed keep working and keep their meaning until the new surface replaces them. The two states are disjoint — the slugs decide tile order, these answers decide which modules are on, and neither reads the other.
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
/**
|
|
* v1.39 (C1) — reading the needs-onboarding row, for the payload and for the
|
|
* three writes.
|
|
*
|
|
* One place that turns an `OnboardingRecord` row (or its absence) into the
|
|
* published DTO, so `GET /api/auth/me` and the write routes cannot disagree
|
|
* about what "nothing answered yet" looks like. A record with no row reads as
|
|
* empty answers and nine pending steps rather than as a missing object — a
|
|
* client then never branches on absence, and the account payload stays
|
|
* additive for every account that predates the flow.
|
|
*/
|
|
import type { PrismaClient } from "@/generated/prisma/client";
|
|
|
|
import {
|
|
defaultOnboardingSteps,
|
|
emptyOnboardingNeeds,
|
|
parseOnboardingFirstResult,
|
|
parseOnboardingNeeds,
|
|
parseOnboardingSteps,
|
|
type OnboardingStateDto,
|
|
} from "./needs";
|
|
import type { OnboardingRecordState } from "./needs-apply";
|
|
|
|
/** The columns every reader below needs. */
|
|
export const ONBOARDING_RECORD_SELECT = {
|
|
needsJson: true,
|
|
stepsJson: true,
|
|
firstResultJson: true,
|
|
modulesDerivedAt: true,
|
|
completedAt: true,
|
|
} as const;
|
|
|
|
export interface OnboardingRecordRow {
|
|
needsJson: unknown;
|
|
stepsJson: unknown;
|
|
firstResultJson: unknown;
|
|
modulesDerivedAt: Date | null;
|
|
completedAt: Date | null;
|
|
}
|
|
|
|
/** The answers / steps / first result, parsed fail-soft. */
|
|
export function readOnboardingRecordState(
|
|
row: OnboardingRecordRow | null,
|
|
): OnboardingRecordState {
|
|
if (!row) {
|
|
return {
|
|
needs: emptyOnboardingNeeds(),
|
|
steps: defaultOnboardingSteps(),
|
|
firstResult: null,
|
|
};
|
|
}
|
|
return {
|
|
needs: parseOnboardingNeeds(row.needsJson),
|
|
steps: parseOnboardingSteps(row.stepsJson),
|
|
firstResult: parseOnboardingFirstResult(row.firstResultJson),
|
|
};
|
|
}
|
|
|
|
/** The shape published on the account payload. */
|
|
export function toOnboardingStateDto(
|
|
row: OnboardingRecordRow | null,
|
|
): OnboardingStateDto {
|
|
const state = readOnboardingRecordState(row);
|
|
return {
|
|
steps: state.steps,
|
|
needs: state.needs,
|
|
completedAt: row?.completedAt?.toISOString() ?? null,
|
|
firstResult: state.firstResult,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load the published state for one record.
|
|
*
|
|
* Takes the delegate rather than the whole client, matching the section
|
|
* builders next door, so the route's global client and a worker's local one
|
|
* share the same read.
|
|
*/
|
|
export async function loadOnboardingState(
|
|
prisma: Pick<PrismaClient, "onboardingRecord">,
|
|
recordId: string,
|
|
): Promise<OnboardingStateDto> {
|
|
const row = await prisma.onboardingRecord.findUnique({
|
|
where: { userId: recordId },
|
|
select: ONBOARDING_RECORD_SELECT,
|
|
});
|
|
return toOnboardingStateDto(row);
|
|
}
|