/** * Import flashcard data from JSON files into Supabase. * * Usage: * SUPABASE_URL=https://xxx.supabase.co SUPABASE_SERVICE_ROLE_KEY=ey... npx tsx supabase/scripts/import-flashcards.ts * * Requires: @supabase/supabase-js (already in project deps) */ import { createClient } from "@supabase/supabase-js" import { readFileSync, existsSync } from "fs" import { resolve, dirname } from "path" import { fileURLToPath } from "url" const __dirname = dirname(fileURLToPath(import.meta.url)) const DATA_DIR = resolve(__dirname, "../data") const supabaseUrl = process.env.SUPABASE_URL const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY if (!supabaseUrl || !serviceRoleKey) { console.error("Missing env vars: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY") process.exit(1) } const supabase = createClient(supabaseUrl, serviceRoleKey) interface RawList { id: string title: string is_public: boolean type: "parent" | "term" parent_id: string | null child_ids: string[] } interface RawTerm { word: string part_of_speech: string | null phonetic: string | null definition: string | null example: string | null image_url: string | null audio_tts_text: string | null audio_lang: string | null display_order: number } function readJson(path: string): T { return JSON.parse(readFileSync(path, "utf-8")) } async function importLists(lists: RawList[]) { // Insert parent lists first (parent_id = null), then children const parents = lists.filter((l) => l.parent_id === null) const children = lists.filter((l) => l.parent_id !== null) for (const batch of [parents, children]) { const rows = batch.map((l) => ({ id: Number(l.id), title: l.title, is_public: l.is_public, type: l.type, parent_id: l.parent_id ? Number(l.parent_id) : null, })) if (rows.length === 0) continue const { error } = await supabase.from("flashcard_list").upsert(rows, { onConflict: "id", }) if (error) { console.error(`Error inserting lists batch:`, error.message) } else { console.log(`Imported ${rows.length} lists`) } } } async function importTerms(listId: number) { const filePath = resolve(DATA_DIR, `terms/flashcard_terms_${listId}.json`) if (!existsSync(filePath)) { console.log(` No terms file for list ${listId}, skipping`) return 0 } const terms = readJson(filePath) if (terms.length === 0) { console.log(` List ${listId}: empty, skipping`) return 0 } const rows = terms.map((t) => ({ list_id: listId, word: t.word, part_of_speech: t.part_of_speech, phonetic: t.phonetic, definition: t.definition, example: t.example, image_url: t.image_url, audio_tts_text: t.audio_tts_text, audio_lang: t.audio_lang || "en-US", display_order: t.display_order, })) // Supabase has a row limit per request, batch in chunks of 500 const BATCH_SIZE = 500 let imported = 0 for (let i = 0; i < rows.length; i += BATCH_SIZE) { const chunk = rows.slice(i, i + BATCH_SIZE) const { error } = await supabase.from("flashcard_term").insert(chunk) if (error) { console.error(` List ${listId} batch ${i}: ${error.message}`) } else { imported += chunk.length } } console.log(` List ${listId}: ${imported}/${terms.length} terms`) return imported } async function updateTotalWords(listId: number) { const { count } = await supabase .from("flashcard_term") .select("*", { count: "exact", head: true }) .eq("list_id", listId) if (count !== null) { await supabase .from("flashcard_list") .update({ total_words: count }) .eq("id", listId) } } async function main() { console.log("=== Flashcard Import ===\n") // 1. Import lists const listsPath = resolve(DATA_DIR, "flashcard_lists.json") const lists = readJson(listsPath) console.log(`Found ${lists.length} lists`) await importLists(lists) // 2. Import terms for each list console.log("\nImporting terms...") let totalTerms = 0 for (const list of lists) { const count = await importTerms(Number(list.id)) totalTerms += count } // 3. Update total_words counts console.log("\nUpdating word counts...") for (const list of lists) { await updateTotalWords(Number(list.id)) } console.log(`\nDone! ${lists.length} lists, ${totalTerms} terms imported.`) } main().catch((err) => { console.error("Fatal:", err) process.exit(1) })