const { Vocab, VocabMapping, VocabForm, VocabRelation } = require('../models'); const { Op } = require('sequelize'); /** * @swagger * components: * schemas: * Vocab: * type: object * required: * - vocab_code * - base_word * - translation * properties: * vocab_id: * type: integer * description: Auto-generated vocab ID * vocab_code: * type: string * description: Unique vocabulary code (e.g., vocab-001-eat) * base_word: * type: string * description: Base form of the word * translation: * type: string * description: Vietnamese translation * difficulty_score: * type: integer * description: Difficulty level (1-10) * category: * type: string * description: Category (e.g., Action Verbs, Nouns) * images: * type: array * items: * type: string * description: Array of image URLs * tags: * type: array * items: * type: string * description: Array of tags * VocabComplete: * allOf: * - $ref: '#/components/schemas/Vocab' * - type: object * properties: * mappings: * type: array * items: * type: object * properties: * book_id: * type: string * grade: * type: integer * unit: * type: integer * lesson: * type: integer * form_key: * type: string * forms: * type: object * additionalProperties: * type: object * properties: * text: * type: string * phonetic: * type: string * audio: * type: string * min_grade: * type: integer * relations: * type: object * properties: * synonyms: * type: array * items: * type: string * antonyms: * type: array * items: * type: string */ /** * Create a new vocabulary entry with all related data */ exports.createVocab = async (req, res) => { const transaction = await Vocab.sequelize.transaction(); try { const { vocab_code, base_word, translation, attributes = {}, mappings = [], forms = {}, relations = {}, syntax = {}, semantics = {}, constraints = {} } = req.body; // 1. Create main vocab entry const vocab = await Vocab.create({ vocab_code, base_word, translation, difficulty_score: attributes.difficulty_score || 1, category: attributes.category, images: attributes.images || [], tags: attributes.tags || [], syntax: syntax, semantics: semantics, constraints: constraints }, { transaction }); // 2. Create curriculum mappings if (mappings.length > 0) { const mappingData = mappings.map(m => ({ vocab_id: vocab.vocab_id, book_id: m.book_id, grade: m.grade, unit: m.unit, lesson: m.lesson, form_key: m.form_key, context_note: m.context_note })); await VocabMapping.bulkCreate(mappingData, { transaction }); } // 3. Create word forms if (Object.keys(forms).length > 0) { const formData = Object.entries(forms).map(([form_key, formInfo]) => ({ vocab_id: vocab.vocab_id, form_key, text: formInfo.text, phonetic: formInfo.phonetic, audio_url: formInfo.audio, min_grade: formInfo.min_grade || 1, description: formInfo.description })); await VocabForm.bulkCreate(formData, { transaction }); } // 4. Create relations (synonyms and antonyms) const relationData = []; if (relations.synonyms && relations.synonyms.length > 0) { relations.synonyms.forEach(word => { relationData.push({ vocab_id: vocab.vocab_id, relation_type: 'synonym', related_word: word }); }); } if (relations.antonyms && relations.antonyms.length > 0) { relations.antonyms.forEach(word => { relationData.push({ vocab_id: vocab.vocab_id, relation_type: 'antonym', related_word: word }); }); } if (relationData.length > 0) { await VocabRelation.bulkCreate(relationData, { transaction }); } await transaction.commit(); // Fetch complete vocab data const completeVocab = await getCompleteVocab(vocab.vocab_id); res.status(201).json({ message: 'Vocabulary created successfully', data: completeVocab }); } catch (error) { await transaction.rollback(); console.error('Error creating vocab:', error); res.status(500).json({ message: 'Error creating vocabulary', error: error.message }); } }; /** * Get all vocabulary entries with pagination and filters */ exports.getAllVocabs = async (req, res) => { try { const { page = 1, limit = 20, category, grade, book_id, difficulty_min, difficulty_max, search, include_relations = 'false' } = req.query; const offset = (page - 1) * limit; const where = { is_active: true }; // Apply filters if (category) { where.category = category; } if (difficulty_min || difficulty_max) { where.difficulty_score = {}; if (difficulty_min) where.difficulty_score[Op.gte] = parseInt(difficulty_min); if (difficulty_max) where.difficulty_score[Op.lte] = parseInt(difficulty_max); } if (search) { where[Op.or] = [ { base_word: { [Op.like]: `%${search}%` } }, { translation: { [Op.like]: `%${search}%` } }, { vocab_code: { [Op.like]: `%${search}%` } } ]; } // Build include array const include = [ { model: VocabMapping, as: 'mappings', required: false }, { model: VocabForm, as: 'forms', required: false } ]; if (include_relations === 'true') { include.push({ model: VocabRelation, as: 'relations', required: false }); } // Apply grade or book_id filter through mappings if (grade || book_id) { const mappingWhere = {}; if (grade) mappingWhere.grade = parseInt(grade); if (book_id) mappingWhere.book_id = book_id; include[0].where = mappingWhere; include[0].required = true; } const { count, rows } = await Vocab.findAndCountAll({ where, include, limit: parseInt(limit), offset: parseInt(offset), order: [['vocab_code', 'ASC']], distinct: true }); // Format response const formattedVocabs = rows.map(vocab => formatVocabResponse(vocab)); res.json({ message: 'Vocabularies retrieved successfully', data: formattedVocabs, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } }); } catch (error) { console.error('Error getting vocabs:', error); res.status(500).json({ message: 'Error retrieving vocabularies', error: error.message }); } }; /** * Get single vocabulary by ID or code */ exports.getVocabById = async (req, res) => { try { const { id } = req.params; // Check if id is numeric (vocab_id) or string (vocab_code) const where = isNaN(id) ? { vocab_code: id, is_active: true } : { vocab_id: parseInt(id), is_active: true }; const vocab = await Vocab.findOne({ where, include: [ { model: VocabMapping, as: 'mappings' }, { model: VocabForm, as: 'forms' }, { model: VocabRelation, as: 'relations' } ] }); if (!vocab) { return res.status(404).json({ message: 'Vocabulary not found' }); } const formattedVocab = formatVocabResponse(vocab); res.json({ message: 'Vocabulary retrieved successfully', data: formattedVocab }); } catch (error) { console.error('Error getting vocab:', error); res.status(500).json({ message: 'Error retrieving vocabulary', error: error.message }); } }; /** * Update vocabulary entry */ exports.updateVocab = async (req, res) => { const transaction = await Vocab.sequelize.transaction(); try { const { id } = req.params; const { base_word, translation, attributes, mappings, forms, relations, syntax, semantics, constraints } = req.body; // Find vocab const where = isNaN(id) ? { vocab_code: id } : { vocab_id: parseInt(id) }; const vocab = await Vocab.findOne({ where }); if (!vocab) { await transaction.rollback(); return res.status(404).json({ message: 'Vocabulary not found' }); } // 1. Update main vocab entry const updateData = {}; if (base_word) updateData.base_word = base_word; if (translation) updateData.translation = translation; if (attributes) { if (attributes.difficulty_score !== undefined) updateData.difficulty_score = attributes.difficulty_score; if (attributes.category !== undefined) updateData.category = attributes.category; if (attributes.images !== undefined) updateData.images = attributes.images; if (attributes.tags !== undefined) updateData.tags = attributes.tags; } if (syntax !== undefined) updateData.syntax = syntax; if (semantics !== undefined) updateData.semantics = semantics; if (constraints !== undefined) updateData.constraints = constraints; if (Object.keys(updateData).length > 0) { await vocab.update(updateData, { transaction }); } // 2. Update mappings (replace all) if (mappings !== undefined) { await VocabMapping.destroy({ where: { vocab_id: vocab.vocab_id }, transaction }); if (mappings.length > 0) { const mappingData = mappings.map(m => ({ vocab_id: vocab.vocab_id, book_id: m.book_id, grade: m.grade, unit: m.unit, lesson: m.lesson, form_key: m.form_key, context_note: m.context_note })); await VocabMapping.bulkCreate(mappingData, { transaction }); } } // 3. Update forms (replace all) if (forms !== undefined) { await VocabForm.destroy({ where: { vocab_id: vocab.vocab_id }, transaction }); if (Object.keys(forms).length > 0) { const formData = Object.entries(forms).map(([form_key, formInfo]) => ({ vocab_id: vocab.vocab_id, form_key, text: formInfo.text, phonetic: formInfo.phonetic, audio_url: formInfo.audio, min_grade: formInfo.min_grade || 1, description: formInfo.description })); await VocabForm.bulkCreate(formData, { transaction }); } } // 4. Update relations (replace all) if (relations !== undefined) { await VocabRelation.destroy({ where: { vocab_id: vocab.vocab_id }, transaction }); const relationData = []; if (relations.synonyms && relations.synonyms.length > 0) { relations.synonyms.forEach(word => { relationData.push({ vocab_id: vocab.vocab_id, relation_type: 'synonym', related_word: word }); }); } if (relations.antonyms && relations.antonyms.length > 0) { relations.antonyms.forEach(word => { relationData.push({ vocab_id: vocab.vocab_id, relation_type: 'antonym', related_word: word }); }); } if (relationData.length > 0) { await VocabRelation.bulkCreate(relationData, { transaction }); } } await transaction.commit(); // Fetch updated vocab const updatedVocab = await getCompleteVocab(vocab.vocab_id); res.json({ message: 'Vocabulary updated successfully', data: updatedVocab }); } catch (error) { await transaction.rollback(); console.error('Error updating vocab:', error); res.status(500).json({ message: 'Error updating vocabulary', error: error.message }); } }; /** * Delete vocabulary (soft delete) */ exports.deleteVocab = async (req, res) => { try { const { id } = req.params; const where = isNaN(id) ? { vocab_code: id } : { vocab_id: parseInt(id) }; const vocab = await Vocab.findOne({ where }); if (!vocab) { return res.status(404).json({ message: 'Vocabulary not found' }); } await vocab.update({ is_active: false }); res.json({ message: 'Vocabulary deleted successfully' }); } catch (error) { console.error('Error deleting vocab:', error); res.status(500).json({ message: 'Error deleting vocabulary', error: error.message }); } }; /** * Get vocabularies by curriculum (book, grade, unit, lesson) */ exports.getVocabsByCurriculum = async (req, res) => { try { const { book_id, grade, unit, lesson } = req.query; if (!book_id && !grade) { return res.status(400).json({ message: 'At least book_id or grade is required' }); } const mappingWhere = {}; if (book_id) mappingWhere.book_id = book_id; if (grade) mappingWhere.grade = parseInt(grade); if (unit) mappingWhere.unit = parseInt(unit); if (lesson) mappingWhere.lesson = parseInt(lesson); const vocabs = await Vocab.findAll({ where: { is_active: true }, include: [ { model: VocabMapping, as: 'mappings', where: mappingWhere, required: true }, { model: VocabForm, as: 'forms' }, { model: VocabRelation, as: 'relations' } ], order: [['vocab_code', 'ASC']] }); const formattedVocabs = vocabs.map(vocab => formatVocabResponse(vocab)); res.json({ message: 'Vocabularies retrieved successfully', data: formattedVocabs, count: formattedVocabs.length }); } catch (error) { console.error('Error getting vocabs by curriculum:', error); res.status(500).json({ message: 'Error retrieving vocabularies', error: error.message }); } }; /** * Get vocabulary statistics */ exports.getVocabStats = async (req, res) => { try { const { sequelize } = Vocab; // Total vocabs const totalVocabs = await Vocab.count({ where: { is_active: true } }); // By category const byCategory = await Vocab.findAll({ where: { is_active: true }, attributes: [ 'category', [sequelize.fn('COUNT', sequelize.col('vocab_id')), 'count'] ], group: ['category'], raw: true }); // By difficulty const byDifficulty = await Vocab.findAll({ where: { is_active: true }, attributes: [ 'difficulty_score', [sequelize.fn('COUNT', sequelize.col('vocab_id')), 'count'] ], group: ['difficulty_score'], order: [['difficulty_score', 'ASC']], raw: true }); // By grade (from mappings) const byGrade = await VocabMapping.findAll({ attributes: [ 'grade', [sequelize.fn('COUNT', sequelize.fn('DISTINCT', sequelize.col('vocab_id'))), 'count'] ], group: ['grade'], order: [['grade', 'ASC']], raw: true }); res.json({ message: 'Vocabulary statistics retrieved successfully', data: { total: totalVocabs, by_category: byCategory, by_difficulty: byDifficulty, by_grade: byGrade } }); } catch (error) { console.error('Error getting vocab stats:', error); res.status(500).json({ message: 'Error retrieving statistics', error: error.message }); } }; /** * Get comprehensive guide for AI to create vocabulary entries */ exports.getVocabGuide = async (req, res) => { try { const guide = { guide_version: "2.0.0", last_updated: "2026-01-26", description: "Comprehensive guide for AI to understand and create vocabulary entries for Grammar Engine", data_structure: { required_fields: { vocab_code: { type: "string", format: "vocab-{sequence}-{base_word}", example: "vocab-001-eat", rule: "Must be unique, use 3-digit sequence number" }, base_word: { type: "string", example: "eat", rule: "Base form of the word in English" }, translation: { type: "string", example: "ăn", rule: "Vietnamese translation" } }, optional_fields: { attributes: { difficulty_score: { type: "integer", range: "1-10", default: 1, guide: "1-2: Basic, 3-4: Intermediate, 5-6: Advanced, 7-8: Difficult, 9-10: Expert" }, category: { type: "string", options: ["Action Verbs", "Nouns", "Adjectives", "Adverbs", "Articles", "Pronouns", "Prepositions", "Conjunctions"], example: "Action Verbs" }, images: { type: "array", item_type: "string (URL)", example: ["https://cdn.sena.tech/img/eat-main.png"] }, tags: { type: "array", item_type: "string", example: ["daily-routine", "verb", "food"] } }, mappings: { type: "array of objects", description: "Curriculum mapping - where this word appears in textbooks", fields: { book_id: { type: "string", example: "global-success-1" }, grade: { type: "integer", example: 1 }, unit: { type: "integer", example: 2 }, lesson: { type: "integer", example: 3 }, form_key: { type: "string", example: "v1", description: "Which form to use at this point" }, context_note: { type: "string", optional: true } } }, forms: { type: "object", description: "Different grammatical forms of the word", structure: { "{form_key}": { text: { type: "string", example: "eat" }, phonetic: { type: "string", format: "IPA", example: "/iːt/" }, audio: { type: "string", format: "URL", example: "https://cdn.sena.tech/audio/eat_v1.mp3" }, min_grade: { type: "integer", example: 1, description: "Minimum grade to unlock" }, description: { type: "string", optional: true } } } }, relations: { type: "object", fields: { synonyms: { type: "array", example: ["consume", "dine"] }, antonyms: { type: "array", example: ["fast", "starve"] }, related: { type: "array", example: ["food", "meal"], optional: true } } }, syntax: { type: "object", description: "Syntax roles for Grammar Engine", critical: true, fields: { is_subject: { type: "boolean", description: "Can be used as subject" }, is_verb: { type: "boolean", description: "Is a verb" }, is_object: { type: "boolean", description: "Can be used as object" }, is_be: { type: "boolean", description: "Is 'be' verb" }, is_adj: { type: "boolean", description: "Is adjective" }, is_adv: { type: "boolean", description: "Is adverb" }, is_article: { type: "boolean", description: "Is article (a/an/the)" }, is_pronoun: { type: "boolean", description: "Is pronoun" }, is_preposition: { type: "boolean", description: "Is preposition" }, verb_type: { type: "string", options: ["transitive", "intransitive", "linking"], when: "is_verb=true" }, article_type: { type: "string", options: ["definite", "indefinite"], when: "is_article=true" }, adv_type: { type: "string", options: ["manner", "frequency", "degree", "time", "place"], when: "is_adv=true" }, position: { type: "string", description: "Word position in sentence" }, priority: { type: "integer", description: "Selection priority for Grammar Engine" }, person: { type: "string", options: ["first", "second", "third"], when: "is_pronoun=true" }, number: { type: "string", options: ["singular", "plural"], when: "is_pronoun=true" } } }, semantics: { type: "object", description: "Semantic constraints to ensure meaningful sentences", critical: true, fields: { can_be_subject_type: { type: "array", options: ["human", "animal", "object", "food", "plant", "abstract", "place", "time"], when: "is_verb=true", description: "What types can be subject with this verb" }, can_take_object_type: { type: "array", options: ["human", "animal", "object", "food", "plant", "abstract", "place", "time"], when: "verb_type=transitive", description: "What types this verb can take as object" }, can_modify: { type: "array", options: ["action_verb", "stative_verb", "be_verb", "adjective", "adverb", "noun"], when: "is_adv=true", description: "What this adverb can modify" }, cannot_modify: { type: "array", options: ["action_verb", "stative_verb", "be_verb", "adjective", "adverb", "noun"], when: "is_adv=true", description: "What this adverb cannot modify" }, word_type: { type: "string", options: ["action", "state", "entity", "property", "concept", "relation"], required: true, description: "Semantic type of the word" }, is_countable: { type: "boolean", when: "is_object=true", description: "Can this noun be counted" }, person_type: { type: "string", options: ["1st", "2nd", "3rd"], when: "is_pronoun=true" } } }, constraints: { type: "object", description: "Grammar constraints for word combination", fields: { followed_by: { type: "string", options: ["vowel_sound", "consonant_sound", "any"], when: "is_article=true", description: "What sound type must follow" }, match_subject: { type: "object", when: "is_be=true", example: { "I": "am", "he": "is", "you": "are" }, description: "Subject-verb agreement rules" }, match_with: { type: "string", description: "Must match with specific word type" }, position_rules: { type: "array", description: "Possible positions in sentence" }, requires_object: { type: "boolean", when: "verb_type=transitive" } } } } }, form_keys_reference: { verbs: { v1: "Base form (eat, run)", v_s_es: "Third person singular (eats, runs)", v_ing: "Present participle (eating, running)", v2: "Past simple (ate, ran)", v3: "Past participle (eaten, run)" }, nouns: { n_singular: "Singular form (cat, apple)", n_plural: "Plural form (cats, apples)" }, adjectives: { adj_base: "Base form (big, happy)", adj_comparative: "Comparative (bigger, happier)", adj_superlative: "Superlative (biggest, happiest)" }, adverbs: { adv_base: "Base form (quickly, slowly)", adv_comparative: "Comparative (more quickly)", adv_superlative: "Superlative (most quickly)" }, pronouns: { subject: "Subject pronoun (I, you, he)", object: "Object pronoun (me, you, him)" } }, rules: { vocab_code_format: "Must follow pattern: vocab-{3-digit-number}-{base_word}", phonetic_format: "Use IPA notation enclosed in /slashes/", audio_url_format: "Must be valid HTTPS URL pointing to MP3 or OGG file", semantic_compatibility: { rule: "Ensure semantic constraints create meaningful sentences", examples: { valid: "human eats food (human can eat, food can be eaten)", invalid: "table eats book (table cannot eat, book is not food)" } }, article_selection: { rule: "Use phonetic to determine a/an", algorithm: "Check first phoneme: if vowel sound use 'an', else use 'a'", examples: { an: "apple (/ˈæp.əl/ starts with /æ/)", a: "cat (/kæt/ starts with /k/)" } }, be_verb_agreement: { rule: "Match be verb form with subject", mapping: { "I": "am", "you": "are", "he/she/it": "is", "we/they": "are" } } }, examples: { transitive_verb: { vocab_code: "vocab-001-eat", base_word: "eat", translation: "ăn", attributes: { difficulty_score: 1, category: "Action Verbs", tags: ["verb", "action", "daily-routine"] }, forms: { v1: { text: "eat", phonetic: "/iːt/", min_grade: 1 }, v_s_es: { text: "eats", phonetic: "/iːts/", min_grade: 2 }, v_ing: { text: "eating", phonetic: "/ˈiː.tɪŋ/", min_grade: 2 }, v2: { text: "ate", phonetic: "/eɪt/", min_grade: 3 } }, syntax: { is_verb: true, verb_type: "transitive", is_subject: false, is_object: false }, semantics: { can_be_subject_type: ["human", "animal"], can_take_object_type: ["food", "plant"], word_type: "action" }, constraints: { requires_object: true } }, article: { vocab_code: "vocab-art-01", base_word: "a", translation: "một (mạo từ bất định)", attributes: { difficulty_score: 1, category: "Articles", tags: ["article", "function-word", "grammar"] }, forms: { base: { text: "a", phonetic: "/ə/", min_grade: 1 } }, syntax: { is_article: true, article_type: "indefinite", priority: 1 }, constraints: { followed_by: "consonant_sound" } }, adverb_manner: { vocab_code: "vocab-adv-01", base_word: "quickly", translation: "nhanh chóng", attributes: { difficulty_score: 2, category: "Adverbs", tags: ["adverb", "manner"] }, forms: { base: { text: "quickly", phonetic: "/ˈkwɪk.li/", min_grade: 2 } }, syntax: { is_adv: true, adv_type: "manner", position: "after_verb" }, semantics: { can_modify: ["action_verb"], cannot_modify: ["stative_verb", "be_verb"], word_type: "property" } }, noun: { vocab_code: "vocab-200-apple", base_word: "apple", translation: "quả táo", attributes: { difficulty_score: 1, category: "Nouns", tags: ["noun", "food", "fruit"] }, forms: { n_singular: { text: "apple", phonetic: "/ˈæp.əl/", min_grade: 1 }, n_plural: { text: "apples", phonetic: "/ˈæp.əlz/", min_grade: 1 } }, syntax: { is_subject: true, is_object: true, is_verb: false }, semantics: { word_type: "entity", is_countable: true } }, pronoun: { vocab_code: "vocab-pron-01", base_word: "I", translation: "tôi", attributes: { difficulty_score: 1, category: "Pronouns", tags: ["pronoun", "personal"] }, forms: { subject: { text: "I", phonetic: "/aɪ/", min_grade: 1 }, object: { text: "me", phonetic: "/miː/", min_grade: 1 } }, syntax: { is_subject: true, is_pronoun: true, pronoun_type: "personal", person: "first", number: "singular" }, semantics: { person_type: "1st", word_type: "entity" }, constraints: { match_subject: { "I": "am" } } } }, validation_checklist: { before_submit: [ "✓ vocab_code follows format vocab-XXX-{word}", "✓ All phonetic notations use IPA format with /slashes/", "✓ At least one form is defined in 'forms' object", "✓ syntax object has at least one role set to true", "✓ semantics.word_type is specified", "✓ If is_verb=true, verb_type is specified", "✓ If verb_type=transitive, can_take_object_type is specified", "✓ If is_article=true, constraints.followed_by is specified", "✓ If is_adv=true, can_modify array is specified" ] }, common_mistakes: [ { mistake: "Not setting any syntax role", fix: "Set at least one is_{role} to true" }, { mistake: "Using 'a' before vowel sound words", fix: "Check phonetic - if starts with vowel sound, use 'an'" }, { mistake: "Transitive verb without can_take_object_type", fix: "Specify what types this verb can take as object" }, { mistake: "Missing word_type in semantics", fix: "Always specify: action, state, entity, property, concept, or relation" }, { mistake: "Incorrect phonetic format", fix: "Use IPA notation: /iːt/ not 'eet' or 'eat'" } ], ai_tips: { efficiency: "Create related words together (eat, eats, eating) to maintain consistency", accuracy: "Double-check phonetic transcription using Cambridge Dictionary or similar", completeness: "Fill all relevant fields - more data means better Grammar Engine performance", testing: "After creating words, test sentence generation to verify semantic constraints work", documentation: "Use descriptive context_note in mappings to help future AI understand usage" } }; res.json({ message: 'Vocabulary guide retrieved successfully', data: guide }); } catch (error) { console.error('Error getting vocab guide:', error); res.status(500).json({ message: 'Error retrieving guide', error: error.message }); } }; // Helper functions /** * Get complete vocabulary data with all relations */ async function getCompleteVocab(vocab_id) { const vocab = await Vocab.findByPk(vocab_id, { include: [ { model: VocabMapping, as: 'mappings' }, { model: VocabForm, as: 'forms' }, { model: VocabRelation, as: 'relations' } ] }); return formatVocabResponse(vocab); } /** * Format vocabulary response to match the expected structure */ function formatVocabResponse(vocab) { const vocabJson = vocab.toJSON(); // Format attributes const attributes = { difficulty_score: vocabJson.difficulty_score, category: vocabJson.category, images: vocabJson.images || [], tags: vocabJson.tags || [] }; // Format forms as object keyed by form_key const forms = {}; if (vocabJson.forms) { vocabJson.forms.forEach(form => { forms[form.form_key] = { text: form.text, phonetic: form.phonetic, audio: form.audio_url, min_grade: form.min_grade, description: form.description }; }); } // Format relations grouped by type const relations = { synonyms: [], antonyms: [], related: [] }; if (vocabJson.relations) { vocabJson.relations.forEach(rel => { if (rel.relation_type === 'synonym') { relations.synonyms.push(rel.related_word); } else if (rel.relation_type === 'antonym') { relations.antonyms.push(rel.related_word); } else if (rel.relation_type === 'related') { relations.related.push(rel.related_word); } }); } return { id: vocabJson.vocab_code, vocab_id: vocabJson.vocab_id, base_word: vocabJson.base_word, translation: vocabJson.translation, attributes, mappings: vocabJson.mappings || [], forms, relations, syntax: vocabJson.syntax || {}, semantics: vocabJson.semantics || {}, constraints: vocabJson.constraints || {} }; }