Files
HealthLog/src/lib/onboarding/needs-store.ts
T
Marc-André Bombeck fea1d1e0b5 chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment
lines in 1,308 files lose the work-item identifier at their head. Release
versions and issue references stay; nothing outside comment text
changes, which the script checks per file against a comment-free
printout. healthlog/no-planning-tags is enabled at error level with this
commit, so the tree it lands on is clean.
2026-10-20 09:08:40 +02:00

104 lines
3.3 KiB
TypeScript

/**
* v1.39 — 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,
resolveOnboardingSteps,
type HeldUnitPreferences,
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.
*
* `held` is what the RECORD's account already carries in its two unit columns,
* and it is a required argument rather than an optional one: the Q6 "skipped
* when the account already holds them" rule has to hold for every reader of
* this DTO, and a caller that has not thought about it should fail to compile.
*/
export function toOnboardingStateDto(
row: OnboardingRecordRow | null,
held: HeldUnitPreferences,
): OnboardingStateDto {
const state = readOnboardingRecordState(row);
return {
steps: resolveOnboardingSteps(state.steps, held),
needs: state.needs,
completedAt: row?.completedAt?.toISOString() ?? null,
firstResult: state.firstResult,
};
}
/**
* Load one record's stored onboarding row, or `null` when it never entered the
* flow.
*
* The row rather than the DTO, because the DTO needs the record's unit columns
* as well and the caller is the one holding them — the account payload already
* reads that user row for the cycle gate, and a second read of the same row on
* the hottest endpoint in the app would buy nothing.
*
* 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 loadOnboardingRecordRow(
prisma: Pick<PrismaClient, "onboardingRecord">,
recordId: string,
): Promise<OnboardingRecordRow | null> {
return prisma.onboardingRecord.findUnique({
where: { userId: recordId },
select: ONBOARDING_RECORD_SELECT,
});
}