// Import TOEIC test JSON files into Supabase. // Usage: SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... node scripts/import-tests.mjs // Requires service_role key to bypass RLS. Idempotent: skips if slug already exists. import { readdir, readFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { createClient } from '@supabase/supabase-js' const __dirname = dirname(fileURLToPath(import.meta.url)) const TEST_DIR = join(__dirname, '..', 'test') const SUPABASE_URL = process.env.SUPABASE_URL const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY if (!SUPABASE_URL || !SERVICE_KEY) { console.error('Missing env: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY') process.exit(1) } const db = createClient(SUPABASE_URL, SERVICE_KEY, { auth: { persistSession: false }, }) async function insertReturningId(table, payload) { const { data, error } = await db.from(table).insert(payload).select('id').single() if (error) throw new Error(`${table} insert failed: ${error.message}`) return data.id } async function insertMany(table, rows) { if (rows.length === 0) return const { error } = await db.from(table).insert(rows) if (error) throw new Error(`${table} bulk insert failed: ${error.message}`) } async function importTest(data) { const { data: existing } = await db.from('test').select('id').eq('slug', data.slug).maybeSingle() if (existing) { console.log(` skip (exists): ${data.slug}`) return { skipped: true } } const testId = await insertReturningId('test', { title: data.title, slug: data.slug, total_questions: data.total_questions ?? 0, }) let totalQuestions = 0 for (const part of data.parts ?? []) { const partId = await insertReturningId('part', { test_id: testId, part_number: part.part_number, title: part.title, display_order: part.display_order ?? 0, }) let partCount = 0 for (const group of part.groups ?? []) { const groupId = await insertReturningId('question_group', { part_id: partId, audio_url: group.audio_url ?? null, image_url: group.image_url ?? null, passage_text: group.passage_text ?? null, transcript: group.transcript ?? null, display_order: group.display_order ?? 0, }) for (const q of group.questions ?? []) { const questionId = await insertReturningId('question', { group_id: groupId, question_number: q.question_number, question_text: q.question_text ?? null, display_order: q.display_order ?? 0, }) const choices = (q.choices ?? []).map((c) => ({ question_id: questionId, value: c.value, label_text: c.label_text ?? null, is_correct: c.is_correct ?? false, })) await insertMany('answer_choice', choices) partCount++ totalQuestions++ } } await db.from('part').update({ question_count: partCount }).eq('id', partId) } return { testId, totalQuestions } } async function rollback(slug) { // CASCADE will clean up parts, groups, questions, choices when test row is deleted. await db.from('test').delete().eq('slug', slug) } async function main() { const files = (await readdir(TEST_DIR)) .filter((f) => f.startsWith('test_') && f.endsWith('.json')) .sort() console.log(`Found ${files.length} file(s) in ${TEST_DIR}\n`) let imported = 0 let skipped = 0 let failed = 0 for (const file of files) { const path = join(TEST_DIR, file) process.stdout.write(`${file}: `) let slug = null try { const data = JSON.parse(await readFile(path, 'utf-8')) slug = data.slug const res = await importTest(data) if (res.skipped) skipped++ else { imported++ console.log(` ok (${res.totalQuestions} questions)`) } } catch (err) { failed++ console.error(` FAIL: ${err.message}`) if (slug) { console.error(` rolling back ${slug}...`) await rollback(slug).catch((e) => console.error(` rollback failed: ${e.message}`)) } } } console.log(`\nDone. imported=${imported} skipped=${skipped} failed=${failed}`) process.exit(failed > 0 ? 1 : 0) } main().catch((err) => { console.error('Fatal:', err) process.exit(1) })