/** * seed-test-data.js * * Seeds a test user and sample kanji into the local MongoDB. * Run from host: docker exec zen_server node /app/seed-test-data.js * * After seeding, use API key "test-key-12345" to log in. * The auth service is patched at runtime — this script inserts the * user directly so no WaniKani API call is needed. */ import mongoose from 'mongoose'; import { User } from './src/models/User.js'; import { StudyItem } from './src/models/StudyItem.js'; import { Vocabulary } from './src/models/Vocabulary.js'; const MONGO_URI = process.env.MONGO_URI || 'mongodb://mongo:27017/zenkanji'; const TEST_API_KEY = 'test-key-12345'; // Sample kanji for testing stroke drawing const SAMPLE_KANJI = [ { wkSubjectId: 440, char: '一', meaning: 'One', level: 1, onyomi: ['イチ', 'イツ'], kunyomi: ['ひと'] }, { wkSubjectId: 441, char: '二', meaning: 'Two', level: 1, onyomi: ['ニ'], kunyomi: ['ふた'] }, { wkSubjectId: 442, char: '三', meaning: 'Three', level: 1, onyomi: ['サン'], kunyomi: ['み'] }, { wkSubjectId: 443, char: '十', meaning: 'Ten', level: 1, onyomi: ['ジュウ'], kunyomi: ['とお'] }, { wkSubjectId: 444, char: '人', meaning: 'Person', level: 1, onyomi: ['ジン', 'ニン'], kunyomi: ['ひと'] }, { wkSubjectId: 445, char: '大', meaning: 'Big', level: 1, onyomi: ['ダイ', 'タイ'], kunyomi: ['おお'] }, { wkSubjectId: 446, char: '山', meaning: 'Mountain', level: 1, onyomi: ['サン'], kunyomi: ['やま'] }, { wkSubjectId: 447, char: '川', meaning: 'River', level: 1, onyomi: ['セン'], kunyomi: ['かわ'] }, { wkSubjectId: 448, char: '口', meaning: 'Mouth', level: 1, onyomi: ['コウ', 'ク'], kunyomi: ['くち'] }, { wkSubjectId: 449, char: '日', meaning: 'Day / Sun', level: 1, onyomi: ['ニチ', 'ジツ'], kunyomi: ['ひ'] }, { wkSubjectId: 450, char: '月', meaning: 'Moon / Month', level: 1, onyomi: ['ゲツ', 'ガツ'], kunyomi: ['つき'] }, { wkSubjectId: 451, char: '水', meaning: 'Water', level: 1, onyomi: ['スイ'], kunyomi: ['みず'] }, { wkSubjectId: 452, char: '火', meaning: 'Fire', level: 2, onyomi: ['カ'], kunyomi: ['ひ'] }, { wkSubjectId: 453, char: '木', meaning: 'Tree / Wood', level: 2, onyomi: ['ボク', 'モク'], kunyomi: ['き'] }, { wkSubjectId: 454, char: '金', meaning: 'Gold / Money', level: 2, onyomi: ['キン', 'コン'], kunyomi: ['かね'] }, { wkSubjectId: 455, char: '土', meaning: 'Earth / Soil', level: 2, onyomi: ['ド', 'ト'], kunyomi: ['つち'] }, ]; // Sample vocabulary for testing the vocabulary browser const SAMPLE_VOCAB = [ // Level 1 { wkSubjectId: 2467, characters: '一', level: 1, meanings: ['One'], readings: [{ reading: 'いち', primary: true }], partsOfSpeech: ['numeral'], meaningMnemonic: 'As a vocab, this is the number one. When a kanji is all alone with no okurigana (hiragana attached to the kanji), it usually uses its on\'yomi reading.', readingMnemonic: 'Since this word is made up of a single kanji with no hiragana, you should use the on\'yomi reading. The on\'yomi reading for this kanji is いち.', componentSubjectIds: [440], contextSentences: [ { ja: '一は一番小さい正の整数です。', en: 'One is the smallest positive integer.' }, { ja: 'もう一ください。', en: 'One more, please.' }, ], }, { wkSubjectId: 2468, characters: '一つ', level: 1, meanings: ['One Thing'], readings: [{ reading: 'ひとつ', primary: true }], partsOfSpeech: ['numeral'], meaningMnemonic: 'This word means one thing, as in counting things. The つ on the end is the counter for things.', readingMnemonic: 'Since there\'s okurigana on this word, you know it\'s going to be a kun\'yomi reading. Think about it as "one thing that you hit."', componentSubjectIds: [440], }, { wkSubjectId: 2469, characters: '二', level: 1, meanings: ['Two'], readings: [{ reading: 'に', primary: true }], partsOfSpeech: ['numeral'], meaningMnemonic: 'When this kanji is by itself with no okurigana, it takes on the meaning of two, the number.', readingMnemonic: 'Since this is a single kanji with no okurigana, you should use the on\'yomi reading. The reading is に.', componentSubjectIds: [441], }, { wkSubjectId: 2470, characters: '二つ', level: 1, meanings: ['Two Things'], readings: [{ reading: 'ふたつ', primary: true }], partsOfSpeech: ['numeral'], meaningMnemonic: 'This is two things. You know it\'s a counter because of the つ on the end.', readingMnemonic: 'Since this word has okurigana, you know it\'s a kun\'yomi word. Think of "two footsies."', componentSubjectIds: [441], }, { wkSubjectId: 2471, characters: '十', level: 1, meanings: ['Ten'], readings: [{ reading: 'じゅう', primary: true }], partsOfSpeech: ['numeral'], meaningMnemonic: 'When the kanji is all alone with no okurigana, it is the number ten.', readingMnemonic: 'Since this word is a single kanji with no okurigana, you should use the on\'yomi reading. It\'s じゅう.', componentSubjectIds: [443], }, { wkSubjectId: 2472, characters: '人', level: 1, meanings: ['Person'], readings: [{ reading: 'ひと', primary: true }, { reading: 'じん', primary: false }], partsOfSpeech: ['noun'], meaningMnemonic: 'As a standalone word, this kanji means person.', readingMnemonic: 'When this is a standalone word, it uses the kun\'yomi reading ひと.', componentSubjectIds: [444], }, { wkSubjectId: 2473, characters: '大人', level: 1, meanings: ['Adult', 'Grown-up'], readings: [{ reading: 'おとな', primary: true }], partsOfSpeech: ['noun', 'na_adjective'], meaningMnemonic: 'A big person is an adult. Pretty straightforward, right?', readingMnemonic: 'This is a jukujikun word, meaning it ignores all the usual reading rules. You just have to remember that おとな is the reading.', componentSubjectIds: [445, 444], contextSentences: [ { ja: '大人になったら何になりたいですか?', en: 'What do you want to be when you grow up?' }, { ja: 'この映画は大人向けです。', en: 'This movie is intended for adults.' }, { ja: '大人二人と子供一人です。', en: 'Two adults and one child.' }, ], }, { wkSubjectId: 2474, characters: '大きい', level: 1, meanings: ['Big', 'Large'], readings: [{ reading: 'おおきい', primary: true }], partsOfSpeech: ['i_adjective'], meaningMnemonic: 'This is an adjective meaning big or large.', readingMnemonic: 'The okurigana tells you to use the kun\'yomi reading. The reading is おおきい.', componentSubjectIds: [445], }, { wkSubjectId: 2475, characters: '山', level: 1, meanings: ['Mountain'], readings: [{ reading: 'やま', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'As a word on its own, this kanji simply means mountain.', readingMnemonic: 'When this is a standalone word, it uses the kun\'yomi reading やま.', componentSubjectIds: [446], }, { wkSubjectId: 2476, characters: '口', level: 1, meanings: ['Mouth'], readings: [{ reading: 'くち', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'When this kanji is alone, it means mouth.', readingMnemonic: 'This is a standalone word so it uses the kun\'yomi reading くち.', componentSubjectIds: [448], }, { wkSubjectId: 2477, characters: '入り口', level: 1, meanings: ['Entrance', 'Entry'], readings: [{ reading: 'いりぐち', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'The mouth where you enter — that\'s the entrance!', readingMnemonic: 'This uses kun\'yomi readings for both kanji. The り comes from 入る (いる) and 口 becomes ぐち through rendaku.', componentSubjectIds: [448], }, { wkSubjectId: 2478, characters: '日', level: 1, meanings: ['Day', 'Sun'], readings: [{ reading: 'ひ', primary: true }, { reading: 'にち', primary: false }], partsOfSpeech: ['noun'], meaningMnemonic: 'This word can mean either day or sun depending on context.', readingMnemonic: 'As a standalone word, it usually reads ひ (kun\'yomi).', componentSubjectIds: [449], }, // Level 2 { wkSubjectId: 2500, characters: '火', level: 2, meanings: ['Fire'], readings: [{ reading: 'ひ', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'As a standalone word, fire.', readingMnemonic: 'Standalone word, uses kun\'yomi: ひ.', componentSubjectIds: [452], }, { wkSubjectId: 2501, characters: '水', level: 2, meanings: ['Water'], readings: [{ reading: 'みず', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'By itself, this kanji is the word for water.', readingMnemonic: 'As a standalone word, you use the kun\'yomi reading: みず.', componentSubjectIds: [451], contextSentences: [ { ja: '水を一杯ください。', en: 'A glass of water, please.' }, { ja: 'この水はとても冷たいです。', en: 'This water is very cold.' }, ], }, { wkSubjectId: 2502, characters: '木', level: 2, meanings: ['Tree', 'Wood'], readings: [{ reading: 'き', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'On its own, this kanji means tree or wood.', readingMnemonic: 'This standalone word uses kun\'yomi: き.', componentSubjectIds: [453], }, { wkSubjectId: 2503, characters: '金', level: 2, meanings: ['Gold', 'Money'], readings: [{ reading: 'かね', primary: true }, { reading: 'きん', primary: false }], partsOfSpeech: ['noun'], meaningMnemonic: 'This word means gold or money, depending on context.', readingMnemonic: 'As a standalone noun, this uses the kun\'yomi reading かね.', componentSubjectIds: [454], }, { wkSubjectId: 2504, characters: '土', level: 2, meanings: ['Earth', 'Soil', 'Ground'], readings: [{ reading: 'つち', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'By itself, this means earth, soil, or ground.', readingMnemonic: 'Standalone word, uses kun\'yomi: つち.', componentSubjectIds: [455], }, { wkSubjectId: 2505, characters: '火山', level: 2, meanings: ['Volcano'], readings: [{ reading: 'かざん', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'A fire mountain — that\'s a volcano! When fire comes from a mountain, it\'s erupting.', readingMnemonic: 'Both kanji use on\'yomi here. 火 is か and 山 is ざん (rendaku of さん).', componentSubjectIds: [452, 446], }, { wkSubjectId: 2506, characters: '人口', level: 2, meanings: ['Population'], readings: [{ reading: 'じんこう', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'Person mouths — if you count all the mouths, you get the population!', readingMnemonic: 'Both kanji use on\'yomi readings here. 人 is じん and 口 is こう.', componentSubjectIds: [444, 448], }, // Level 3 { wkSubjectId: 2550, characters: '三日', level: 3, meanings: ['Three Days', 'Third Day'], readings: [{ reading: 'みっか', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'Three days or the third day of the month.', readingMnemonic: 'This is an exceptional reading. Japanese day counters have special readings for the first ten days. You just need to memorize みっか.', componentSubjectIds: [442, 449], }, { wkSubjectId: 2551, characters: '大金', level: 3, meanings: ['Large Sum of Money', 'Fortune'], readings: [{ reading: 'たいきん', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'Big money — a large sum of money, or a fortune!', readingMnemonic: 'Both kanji use on\'yomi. 大 is たい and 金 is きん.', componentSubjectIds: [445, 454], }, { wkSubjectId: 2552, characters: '山火事', level: 3, meanings: ['Wildfire', 'Forest Fire'], readings: [{ reading: 'やまかじ', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'A mountain fire incident — that\'s a wildfire or forest fire!', readingMnemonic: 'This uses kun\'yomi readings. Mountain is やま, fire is か, and incident/thing is じ.', componentSubjectIds: [446, 452], }, { wkSubjectId: 2553, characters: '一人', level: 3, meanings: ['Alone', 'One Person'], readings: [{ reading: 'ひとり', primary: true }], partsOfSpeech: ['noun', 'adverb'], meaningMnemonic: 'One person — being alone.', readingMnemonic: 'This is an exceptional reading: ひとり. It doesn\'t follow the usual on\'yomi or kun\'yomi patterns.', componentSubjectIds: [440, 444], }, { wkSubjectId: 2554, characters: '二人', level: 3, meanings: ['Two People', 'Pair', 'Couple'], readings: [{ reading: 'ふたり', primary: true }], partsOfSpeech: ['noun'], meaningMnemonic: 'Two people — a pair or couple.', readingMnemonic: 'Another exceptional reading: ふたり. Like ひとり, this is a set phrase with a unique reading.', componentSubjectIds: [441, 444], contextSentences: [ { ja: '二人で映画を見ました。', en: 'We watched a movie together (the two of us).' }, ], }, // Verbs — transitive / intransitive pairs { wkSubjectId: 2600, characters: '入る', level: 3, meanings: ['To Enter', 'To Go In'], readings: [{ reading: 'はいる', primary: true }], partsOfSpeech: ['intransitive_verb', 'godan_verb'], meaningMnemonic: 'To enter or go into a place. This is the intransitive version — the subject enters on its own.', readingMnemonic: 'The reading is はいる.', componentSubjectIds: [], contextSentences: [ { ja: '部屋に入ってください。', en: 'Please enter the room.' }, { ja: 'お風呂に入る。', en: 'To take a bath (literally: to enter the bath).' }, ], }, { wkSubjectId: 2601, characters: '入れる', level: 3, meanings: ['To Insert', 'To Put In'], readings: [{ reading: 'いれる', primary: true }], partsOfSpeech: ['transitive_verb', 'ichidan_verb'], meaningMnemonic: 'To put something in or insert. This is the transitive version — you are putting something into something else.', readingMnemonic: 'The reading is いれる.', componentSubjectIds: [], contextSentences: [ { ja: 'コーヒーに砂糖を入れますか?', en: 'Do you put sugar in your coffee?' }, { ja: 'かばんに本を入れた。', en: 'I put the book in my bag.' }, ], }, { wkSubjectId: 2602, characters: '上がる', level: 3, meanings: ['To Rise', 'To Go Up'], readings: [{ reading: 'あがる', primary: true }], partsOfSpeech: ['intransitive_verb', 'godan_verb'], meaningMnemonic: 'To rise or go up. This is intransitive — things rise on their own.', readingMnemonic: 'The reading is あがる.', componentSubjectIds: [], contextSentences: [ { ja: '温度が上がった。', en: 'The temperature went up.' }, { ja: '二階に上がってください。', en: 'Please go up to the second floor.' }, ], }, { wkSubjectId: 2603, characters: '上げる', level: 3, meanings: ['To Raise', 'To Give'], readings: [{ reading: 'あげる', primary: true }], partsOfSpeech: ['transitive_verb', 'ichidan_verb'], meaningMnemonic: 'To raise something up or to give something. This is transitive — you raise or give something.', readingMnemonic: 'The reading is あげる.', componentSubjectIds: [], contextSentences: [ { ja: '手を上げてください。', en: 'Please raise your hand.' }, { ja: '友達にプレゼントを上げた。', en: 'I gave a present to my friend.' }, ], }, ]; async function seed() { await mongoose.connect(MONGO_URI); console.log('Connected to MongoDB'); // Create or find test user let user = await User.findOne({ wkApiKey: TEST_API_KEY }); if (!user) { user = await User.create({ wkApiKey: TEST_API_KEY, tokenVersion: 0, lastSync: new Date(), stats: { totalReviews: 42, correctReviews: 35, currentStreak: 3, maxStreak: 7 }, settings: { batchSize: 20 }, }); console.log('Created test user'); } else { console.log('Test user already exists'); } // Insert study items (skip duplicates) let inserted = 0; for (const kanji of SAMPLE_KANJI) { const exists = await StudyItem.findOne({ userId: user._id, wkSubjectId: kanji.wkSubjectId }); if (!exists) { await StudyItem.create({ userId: user._id, ...kanji, srsLevel: 1, nextReview: new Date(), // due now }); inserted++; } } console.log(`Inserted ${inserted} kanji (${SAMPLE_KANJI.length - inserted} already existed)`); // Insert vocabulary items (skip duplicates) let vocabInserted = 0; for (const vocab of SAMPLE_VOCAB) { const exists = await Vocabulary.findOne({ userId: user._id, wkSubjectId: vocab.wkSubjectId }); if (!exists) { await Vocabulary.create({ userId: user._id, ...vocab, }); vocabInserted++; } } console.log(`Inserted ${vocabInserted} vocabulary (${SAMPLE_VOCAB.length - vocabInserted} already existed)`); console.log(`\n✅ Login with API key: ${TEST_API_KEY}\n`); await mongoose.disconnect(); } seed().catch((err) => { console.error('Seed failed:', err); process.exit(1); });