Files
step3/api/QuestionsApi/Services/SeedFlashcardsCommand.cs
T
hz 7f76ca0fdc Add system (curated) vs. user-uploaded flashcards
System decks/notes are owned by a reserved sentinel user id, reusing
all existing per-user store/index plumbing unchanged. Regular users
see them under a separate "Official Decks" section, can study them
with fully independent per-user FSRS progress (lazily materialized on
first visit), but can never edit or archive them - enforced both in
the client (no Edit/Archive controls) and server-side (ownership guard
on every mutating endpoint). Includes a small idempotent seed command
(`dotnet run -- seed-flashcards`) for testing, since there's no in-app
authoring UI yet.
2026-07-22 15:52:19 -05:00

63 lines
3.5 KiB
C#

using QuestionsApi.Models;
using QuestionsApi.Services.Flashcards;
namespace QuestionsApi.Services;
// Idempotent, single-target seed for testing "system" (shared, admin-curated) flashcards.
// Unlike ContentPromotionCommand (cross-environment copy, source+target connections), this only
// ever writes to the one database the app is already configured against, using the exact same
// store methods a real user's create/edit path uses - just under the reserved FlashcardOwners.System
// identity - so seeded content is created/updated through the same validation and revision
// handling as any other deck/note.
public static class SeedFlashcardsCommand
{
public static async Task<int> RunAsync(IConfiguration configuration, TextWriter output)
{
var mongo = new MongoDatabaseProvider(configuration);
var scheduleStore = new FlashcardScheduleStore(mongo, new FsrsFlashcardScheduler());
var deckStore = new FlashcardDeckStore(mongo);
var noteStore = new FlashcardNoteStore(mongo, scheduleStore);
var deck = await UpsertSystemDeckAsync(deckStore, "Step 3 Essentials", "Official curated review deck");
await UpsertSystemNoteAsync(noteStore, deck.Id!, FlashcardTypes.Basic,
"A 62-year-old woman presents with sudden, painless, monocular vision loss. What is the diagnosis?",
"Central retinal artery occlusion", "\"Cherry-red spot\" on fundoscopy is the classic finding.");
await UpsertSystemNoteAsync(noteStore, deck.Id!, FlashcardTypes.Basic,
"Thyroid storm: correct order of pharmacologic treatment?",
"Beta-blocker (propranolol) first -> thionamide (PTU/methimazole) -> iodine solution (started >=1hr after thionamide) -> glucocorticoids",
"Giving iodine before the thionamide can fuel new hormone synthesis (Wolff-Chaikoff escape risk).");
await UpsertSystemNoteAsync(noteStore, deck.Id!, FlashcardTypes.Cloze,
"First-line treatment for uncomplicated hypertension in a Black patient is {{c1::a thiazide diuretic or calcium channel blocker}}.",
"", "");
await output.WriteLineAsync($"Seeded system flashcards into deck '{deck.Name}' ({deck.Id}).");
return 0;
}
private static async Task<FlashcardDeck> UpsertSystemDeckAsync(FlashcardDeckStore deckStore, string name, string description)
{
var existing = await deckStore.GetByNameAsync(FlashcardOwners.System, FlashcardDeckStore.Normalize(name));
var request = new UpsertFlashcardDeckRequest { Name = name, Description = description };
var result = await deckStore.UpsertAsync(FlashcardOwners.System, request, existing?.Id);
return result.Deck ?? existing ?? throw new InvalidOperationException($"failed to seed deck '{name}'");
}
private static async Task UpsertSystemNoteAsync(
FlashcardNoteStore noteStore, string deckId, string type, string prompt, string answer, string extra)
{
var fingerprint = FlashcardNoteStore.Fingerprint(type, prompt, answer);
var existingNotes = await noteStore.ListByDeckAsync(FlashcardOwners.System, deckId);
var existing = existingNotes.FirstOrDefault(v => v.ContentFingerprint == fingerprint);
var request = new UpsertFlashcardNoteRequest
{
DeckId = deckId,
Type = type,
Prompt = prompt,
Answer = answer,
Extra = extra,
ExpectedRevision = existing?.Revision
};
await noteStore.UpsertAsync(FlashcardOwners.System, request, existing?.Id);
}
}