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.
63 lines
3.5 KiB
C#
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);
|
|
}
|
|
}
|