Files
HealthLog/src/lib/mood/note-backfill.ts
T
Marc-André Bombeck 11325e31a0 feat(mood): add a first-class note column replacing the tag-based workaround
R-E H-5. Up to v0.4.1.1 the iOS app stored mood-entry prose as
`tags: ["note:<text>"]` because the server had no dedicated column.
The workaround contaminated the tags axis with prose the Coach
evidence shelf misread as taxonomic labels.

Schema (migration 0063, additive):
  - mood_entries.note (TEXT, nullable)

Validation + routes:
  - createMoodEntrySchema gains optional note (max 500 chars)
  - updateMoodEntrySchema accepts note + null to clear
  - POST /api/mood-entries persists note alongside tags
  - PUT /api/mood-entries/[id] honours partial note updates
  - POST /api/mood-entries/bulk carries note through the bulk path

Backfill script at scripts/backfill-mood-note-column.ts lifts the
legacy note:<text> tag entries into the new column. Idempotent
dry-run mode is the default; --confirm commits. Helper logic lives
in src/lib/mood/note-backfill.ts for tight unit coverage.

iOS contract: additive. Old iOS payloads (no note field) round-trip
unchanged. New iOS code paths can read or write note without
touching tags.
2026-05-16 12:08:53 +02:00

41 lines
1.1 KiB
TypeScript

/**
* v1.4.30 — pure helper for the `MoodEntry.note` backfill (R-E H-5).
*
* Pulls the first `note:<text>` entry out of a JSON-encoded tags array
* and returns the cleaned tags JSON alongside the extracted note. Used
* by `scripts/backfill-mood-note-column.ts` (CLI) — kept in `src/lib/`
* so unit tests can exercise the bucketing without booting `dotenv`
* + the Prisma client.
*/
export function extractNoteAndSlimTags(tagsJson: string): {
note: string | null;
newTags: string | null;
} {
let parsed: unknown;
try {
parsed = JSON.parse(tagsJson);
} catch {
return { note: null, newTags: tagsJson };
}
if (!Array.isArray(parsed)) {
return { note: null, newTags: tagsJson };
}
let extracted: string | null = null;
const remaining: string[] = [];
for (const tag of parsed) {
if (typeof tag !== "string") {
remaining.push(String(tag));
continue;
}
if (extracted === null && tag.startsWith("note:")) {
extracted = tag.slice("note:".length);
} else {
remaining.push(tag);
}
}
return {
note: extracted,
newTags: remaining.length > 0 ? JSON.stringify(remaining) : null,
};
}