imported flash card
This commit is contained in:
170
supabase/scripts/import-flashcards.ts
Normal file
170
supabase/scripts/import-flashcards.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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<T>(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<RawTerm[]>(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<RawList[]>(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)
|
||||
})
|
||||
Reference in New Issue
Block a user