Files
sena_db_api_layer/controllers/vocabController.js
silverpro89 085f36078a
All checks were successful
Deploy to Production / deploy (push) Successful in 20s
update vocab
2026-02-24 16:59:17 +07:00

1021 lines
25 KiB
JavaScript

const { Vocab } = require('../models');
const { Op } = require('sequelize');
/**
* ============================================
* CREATE VOCAB
* ============================================
* Tạo mới một vocab entry
*
* @route POST /api/vocab
* @access Private (requires authentication)
*
* INPUT:
* {
* text: String (required) - từ thực tế (wash, washes, washing, ate, eaten...)
* ipa: String - phiên âm IPA (ví dụ: /wɒʃ/)
* base_word: String (required) - từ gốc để nhóm lại (wash, eat...)
* form_key: JSON - loại biến thể (V1, V2, V3, V_ing, Noun_Form...), mặc định 'base'
* vi: String - nghĩa tiếng Việt
* category: String - category của từ (Action Verbs, Nouns, etc.)
* topic: String - chủ đề (Food, Travel, Education, etc.)
* image_small: JSON Array - mảng URLs của hình ảnh nhỏ
* image_square: JSON Array - mảng URLs của hình ảnh vuông
* image_normal: JSON Array - mảng URLs của hình ảnh bình thường
* audio: JSON Array - mảng URLs của audio files
* example_sentences: JSON - các câu ví dụ
* tags: JSON Array - các tags để phân loại
* syntax: JSON - vai trò cú pháp (is_subject, is_verb, is_object, is_be, is_adj, is_adv, is_article, verb_type, etc.)
* semantics: JSON - ràng buộc ngữ nghĩa (can_be_subject_type, can_take_object_type, can_modify, word_type, is_countable, person_type, etc.)
* constraints: JSON - ràng buộc ngữ pháp (followed_by, match_subject, match_with, phonetic_rules, etc.)
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: {
* vocab_id: UUID,
* text: String,
* ipa: String,
* base_word: String,
* form_key: JSON,
* vi: String,
* category: String,
* topic: String,
* image_small: JSON Array,
* image_square: JSON Array,
* image_normal: JSON Array,
* audio: JSON Array,
* example_sentences: JSON,
* tags: JSON Array,
* syntax: JSON,
* semantics: JSON,
* constraints: JSON,
* is_active: Boolean,
* created_at: Date,
* updated_at: Date
* }
* }
*/
exports.createVocab = async (req, res) => {
try {
const {
text,
ipa,
base_word,
vi,
category,
topic,
image_small,
image_square,
image_normal,
audio,
example_sentences,
tags,
syntax,
semantics,
constraints
} = req.body;
// Validate required fields
if (!text || !base_word) {
return res.status(400).json({
success: false,
message: 'text and base_word are required fields'
});
}
// Create vocab entry
const vocab = await Vocab.create({
text,
ipa: ipa || null,
base_word,
vi: vi || '',
category: category || null,
topic: topic || null,
image_small: image_small || null,
image_square: image_square || null,
image_normal: image_normal || null,
audio: audio || null,
example_sentences: example_sentences || null,
tags: tags || null,
syntax: syntax || null,
semantics: semantics || null,
constraints: constraints || null,
is_active: true
});
res.status(201).json({
success: true,
message: 'Vocabulary created successfully',
data: vocab
});
} catch (error) {
console.error('Error creating vocab:', error);
res.status(500).json({
success: false,
message: 'Error creating vocabulary',
error: error.message
});
}
};
/**
* ============================================
* GET ALL VOCABS
* ============================================
* Lấy danh sách vocab với phân trang và filter
*
* @route GET /api/vocab
* @access Private
*
* INPUT (Query Parameters):
* {
* page: Number - trang hiện tại (mặc định: 1)
* limit: Number - số items mỗi trang (mặc định: 20)
* category: String - lọc theo category
* topic: String - lọc theo topic
* base_word: String - lọc theo base_word chính xác
* text: String - lọc theo text chính xác
* search: String - tìm kiếm trong text, base_word và vi
* is_active: Boolean - lọc theo trạng thái active (mặc định: true)
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Array of Vocab objects,
* pagination: {
* total: Number - tổng số records,
* page: Number - trang hiện tại,
* limit: Number - số items mỗi trang,
* totalPages: Number - tổng số trang
* }
* }
*/
exports.getAllVocabs = async (req, res) => {
try {
const {
page = 1,
limit = 20,
category,
topic,
base_word,
text,
search,
is_active = true
} = req.query;
const offset = (page - 1) * limit;
const where = {};
// Apply filters
if (is_active !== undefined) {
where.is_active = is_active === 'true' || is_active === true;
}
if (category) {
where.category = category;
}
if (topic) {
where.topic = topic;
}
if (base_word) {
where.base_word = base_word;
}
if (text) {
where.text = text;
}
// Search across multiple fields
if (search) {
where[Op.or] = [
{ text: { [Op.like]: `%${search}%` } },
{ base_word: { [Op.like]: `%${search}%` } },
{ vi: { [Op.like]: `%${search}%` } }
];
}
const { count, rows } = await Vocab.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['created_at', 'DESC']]
});
res.json({
success: true,
message: 'Vocabularies retrieved successfully',
data: rows,
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({
success: false,
message: 'Error retrieving vocabularies',
error: error.message
});
}
};
/**
* ============================================
* GET VOCAB BY ID
* ============================================
* Lấy chi tiết một vocab theo vocab_id
*
* @route GET /api/vocab/:id
* @access Private
*
* INPUT (URL Parameter):
* {
* id: UUID - vocab_id của vocab cần lấy
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Vocab object với đầy đủ thông tin
* }
*/
exports.getVocabById = async (req, res) => {
try {
const { id } = req.params;
const vocab = await Vocab.findOne({
where: {
vocab_id: id,
is_active: true
}
});
if (!vocab) {
return res.status(404).json({
success: false,
message: 'Vocabulary not found'
});
}
res.json({
success: true,
message: 'Vocabulary retrieved successfully',
data: vocab
});
} catch (error) {
console.error('Error getting vocab:', error);
res.status(500).json({
success: false,
message: 'Error retrieving vocabulary',
error: error.message
});
}
};
/**
* ============================================
* GET VOCABS WITHOUT IPA
* ============================================
* Lấy tất cả các vocab chưa có IPA
*
* @route GET /api/vocabs/missing/ipa
* @access Private
*
* INPUT (Query Parameters):
* {
* page: Number - trang hiện tại (mặc định: 1)
* limit: Number - số items mỗi trang (mặc định: 50)
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Array of Vocab objects - các vocab chưa có IPA,
* pagination: {
* total: Number,
* page: Number,
* limit: Number,
* totalPages: Number
* }
* }
*/
exports.getVocabsWithoutIpa = async (req, res) => {
try {
const { page = 1, limit = 50 } = req.query;
const offset = (page - 1) * limit;
const { count, rows } = await Vocab.findAndCountAll({
where: {
is_active: true,
[Op.or]: [
{ ipa: null },
{ ipa: '' }
]
},
limit: parseInt(limit),
offset: parseInt(offset),
order: [['text', 'ASC']]
});
res.json({
success: true,
message: 'Vocabularies without IPA retrieved successfully',
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('Error getting vocabs without IPA:', error);
res.status(500).json({
success: false,
message: 'Error retrieving vocabularies without IPA',
error: error.message
});
}
};
/**
* ============================================
* GET VOCABS WITHOUT IMAGES
* ============================================
* Lấy tất cả các vocab chưa đủ hình ảnh (thiếu image_small, image_square hoặc image_normal)
*
* @route GET /api/vocabs/missing/images
* @access Private
*
* INPUT (Query Parameters):
* {
* page: Number - trang hiện tại (mặc định: 1)
* limit: Number - số items mỗi trang (mặc định: 50)
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Array of Vocab objects - các vocab chưa đủ hình ảnh,
* pagination: {
* total: Number,
* page: Number,
* limit: Number,
* totalPages: Number
* }
* }
*/
exports.getVocabsWithoutImages = async (req, res) => {
try {
const { page = 1, limit = 50 } = req.query;
const offset = (page - 1) * limit;
const { count, rows } = await Vocab.findAndCountAll({
where: {
is_active: true,
[Op.or]: [
{ image_small: null },
{ image_square: null },
{ image_normal: null }
]
},
limit: parseInt(limit),
offset: parseInt(offset),
order: [['text', 'ASC']]
});
res.json({
success: true,
message: 'Vocabularies without images retrieved successfully',
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('Error getting vocabs without images:', error);
res.status(500).json({
success: false,
message: 'Error retrieving vocabularies without images',
error: error.message
});
}
};
/**
* ============================================
* UPDATE VOCAB
* ============================================
* Cập nhật thông tin vocab
*
* @route PUT /api/vocab/:id
* @access Private
*
* INPUT (URL Parameter + Body):
* {
* id: UUID - vocab_id cần update
* Body: Object - các trường cần update (có thể update một hoặc nhiều trường)
* {
* text: String,
* ipa: String,
* base_word: String,
* grade: Number,
* form_key: JSON,
* vi: String,
* category: String,
* etc: String,
* topic: String,
* image_small: JSON Array,
* image_square: JSON Array,
* image_normal: JSON Array,
* audio: JSON Array,
* example_sentences: JSON,
* tags: JSON Array,
* syntax: JSON,
* semantics: JSON,
* constraints: JSON,
* is_active: Boolean
* }
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Updated Vocab object
* }
*/
exports.updateVocab = async (req, res) => {
try {
const { id } = req.params;
const updateData = req.body;
const vocab = await Vocab.findOne({
where: { vocab_id: id }
});
if (!vocab) {
return res.status(404).json({
success: false,
message: 'Vocabulary not found'
});
}
// Update vocab
await vocab.update(updateData);
res.json({
success: true,
message: 'Vocabulary updated successfully',
data: vocab
});
} catch (error) {
console.error('Error updating vocab:', error);
res.status(500).json({
success: false,
message: 'Error updating vocabulary',
error: error.message
});
}
};
/**
* ============================================
* DELETE VOCAB (SOFT DELETE)
* ============================================
* Xóa mềm vocab (set is_active = false)
*
* @route DELETE /api/vocab/:id
* @access Private
*
* INPUT (URL Parameter):
* {
* id: UUID - vocab_id cần xóa
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String - "Vocabulary deleted successfully"
* }
*/
exports.deleteVocab = async (req, res) => {
try {
const { id } = req.params;
const vocab = await Vocab.findOne({
where: { vocab_id: id }
});
if (!vocab) {
return res.status(404).json({
success: false,
message: 'Vocabulary not found'
});
}
// Soft delete
await vocab.update({ is_active: false });
res.json({
success: true,
message: 'Vocabulary deleted successfully'
});
} catch (error) {
console.error('Error deleting vocab:', error);
res.status(500).json({
success: false,
message: 'Error deleting vocabulary',
error: error.message
});
}
};
/**
* ============================================
* BULK CREATE VOCABS
* ============================================
* Tạo nhiều vocab cùng lúc
*
* @route POST /api/vocab/bulk
* @access Private
*
* INPUT (Body):
* {
* vocabs: Array of Vocab objects - mỗi object phải có đầy đủ trường required
* [
* {
* text: String (required),
* base_word: String (required),
* ipa: String,
* grade: Number,
* form_key: JSON,
* vi: String,
* ...
* },
* ...
* ]
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String - "X vocabularies created successfully",
* data: Array of created Vocab objects,
* count: Number - số lượng vocab đã tạo
* }
*/
exports.bulkCreateVocabs = async (req, res) => {
try {
const { vocabs } = req.body;
if (!Array.isArray(vocabs) || vocabs.length === 0) {
return res.status(400).json({
success: false,
message: 'vocabs must be a non-empty array'
});
}
// Validate each vocab has required fields
for (let i = 0; i < vocabs.length; i++) {
if (!vocabs[i].text || !vocabs[i].base_word) {
return res.status(400).json({
success: false,
message: `Vocab at index ${i} is missing required fields (text, base_word)`
});
}
}
const createdVocabs = await Vocab.bulkCreate(vocabs);
res.status(201).json({
success: true,
message: `${createdVocabs.length} vocabularies created successfully`,
data: createdVocabs,
count: createdVocabs.length
});
} catch (error) {
console.error('Error bulk creating vocabs:', error);
res.status(500).json({
success: false,
message: 'Error creating vocabularies',
error: error.message
});
}
};
/**
* ============================================
* GET VOCAB STATISTICS
* ============================================
* Lấy thống kê về vocab
*
* @route GET /api/vocab/stats/overview
* @access Private
*
* INPUT: Không có
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: {
* total: {
* active: Number - số vocab đang active,
* inactive: Number - số vocab bị xóa,
* all: Number - tổng số vocab
* },
* unique_base_words: Number - số lượng từ gốc duy nhất,
* by_category: Array - thống kê theo category
* [
* {
* category: String,
* count: Number
* },
* ...
* ],
* by_topic: Array - thống kê theo topic
* [
* {
* topic: String,
* count: Number
* },
* ...
* ]
* }
* }
*/
exports.getVocabStats = async (req, res) => {
try {
const { sequelize } = Vocab;
// Total active vocabs
const totalActive = await Vocab.count({ where: { is_active: true } });
const totalInactive = await Vocab.count({ where: { is_active: false } });
// By category
const byCategory = await Vocab.findAll({
where: { is_active: true },
attributes: [
'category',
[sequelize.fn('COUNT', sequelize.col('vocab_id')), 'count']
],
group: ['category'],
order: [[sequelize.fn('COUNT', sequelize.col('vocab_id')), 'DESC']],
raw: true
});
// By topic
const byTopic = await Vocab.findAll({
where: { is_active: true },
attributes: [
'topic',
[sequelize.fn('COUNT', sequelize.col('vocab_id')), 'count']
],
group: ['topic'],
order: [[sequelize.fn('COUNT', sequelize.col('vocab_id')), 'DESC']],
raw: true
});
// Count unique base words
const uniqueBaseWords = await Vocab.count({
where: { is_active: true },
distinct: true,
col: 'base_word'
});
res.json({
success: true,
message: 'Statistics retrieved successfully',
data: {
total: {
active: totalActive,
inactive: totalInactive,
all: totalActive + totalInactive
},
unique_base_words: uniqueBaseWords,
by_category: byCategory,
by_topic: byTopic
}
});
} catch (error) {
console.error('Error getting vocab stats:', error);
res.status(500).json({
success: false,
message: 'Error retrieving statistics',
error: error.message
});
}
};
/**
* ============================================
* SEARCH VOCABS
* ============================================
* Tìm kiếm vocab nâng cao với nhiều filter
*
* @route POST /api/vocabs/search
* @access Private
*
* INPUT (Body):
* {
* topic: String (optional) - chủ đề
* category: String (optional) - loại từ
* v_type: Boolean (optional) - tìm các biến thể khác của cùng một base_word
* base_word_filter: String (optional) - base_word cụ thể (dùng khi v_type=true)
*
* shuffle_pos: Object (optional) - tìm từ thay thế dựa trên syntax
* {
* is_subject: Boolean,
* is_verb: Boolean,
* is_object: Boolean,
* is_be: Boolean,
* is_adj: Boolean,
* is_adv: Boolean,
* is_article: Boolean
* }
*
* page: Number - trang hiện tại (mặc định: 1)
* limit: Number - số items mỗi trang (mặc định: 100)
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Array of Vocab objects,
* pagination: {
* total: Number,
* page: Number,
* limit: Number,
* totalPages: Number
* }
* }
*/
exports.searchVocabs = async (req, res) => {
try {
const {
topic,
category,
base_word,
form_key,
text,
vi,
v_type,
shuffle_pos,
page = 1,
limit = 100
} = req.body;
const offset = (page - 1) * limit;
const where = { is_active: true };
// Filter by topic
if (topic) {
where.topic = topic;
}
// Filter by category
if (category) {
where.category = category;
}
// Filter by base_word
if (base_word) {
where.base_word = { [Op.like]: `%${base_word}%` };
}
// Filter by form_key
if (form_key) {
where.form_key = form_key;
}
// Filter by text (partial match)
if (text) {
where.text = { [Op.like]: `%${text}%` };
}
// Filter by vi (Vietnamese meaning - partial match)
if (vi) {
where.vi = { [Op.like]: `%${vi}%` };
}
// Handle v_type: find variants of same base_word
if (v_type === true) {
// Nếu có v_type, cần tìm tất cả các từ có cùng base_word
// Thông thường sẽ kết hợp với một vocab_id hoặc base_word cụ thể
// Để linh hoạt, ta có thể group by base_word và lấy tất cả variants
// Hoặc nếu user cung cấp thêm base_word trong query
const { base_word_filter } = req.body;
if (base_word_filter) {
where.base_word = base_word_filter;
}
}
// Handle shuffle_pos: find replacement words based on syntax
if (shuffle_pos && typeof shuffle_pos === 'object') {
const syntaxConditions = [];
if (shuffle_pos.is_subject === true) {
syntaxConditions.push({ 'syntax.is_subject': true });
}
if (shuffle_pos.is_verb === true) {
syntaxConditions.push({ 'syntax.is_verb': true });
}
if (shuffle_pos.is_object === true) {
syntaxConditions.push({ 'syntax.is_object': true });
}
if (shuffle_pos.is_be === true) {
syntaxConditions.push({ 'syntax.is_be': true });
}
if (shuffle_pos.is_adj === true) {
syntaxConditions.push({ 'syntax.is_adj': true });
}
if (shuffle_pos.is_adv === true) {
syntaxConditions.push({ 'syntax.is_adv': true });
}
if (shuffle_pos.is_article === true) {
syntaxConditions.push({ 'syntax.is_article': true });
}
// Nếu có điều kiện syntax, ta cần query JSON field
// Sequelize hỗ trợ JSON query cho MySQL/PostgreSQL
if (syntaxConditions.length > 0) {
const { Sequelize } = require('sequelize');
const orConditions = syntaxConditions.map(condition => {
const key = Object.keys(condition)[0];
const jsonPath = key.replace('syntax.', '');
return Sequelize.where(
Sequelize.fn('JSON_EXTRACT', Sequelize.col('syntax'), `$.${jsonPath}`),
true
);
});
if (orConditions.length === 1) {
where[Op.and] = where[Op.and] || [];
where[Op.and].push(orConditions[0]);
} else {
where[Op.and] = where[Op.and] || [];
where[Op.and].push({
[Op.or]: orConditions
});
}
}
}
const { count, rows } = await Vocab.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['text', 'ASC']]
});
res.json({
success: true,
message: 'Search completed successfully',
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('Error searching vocabs:', error);
res.status(500).json({
success: false,
message: 'Error searching vocabularies',
error: error.message
});
}
};
/**
* ============================================
* GET ALL CATEGORIES
* ============================================
* Lấy danh sách tất cả categories có trong database
*
* @route GET /api/vocab/meta/categories
* @access Private
*
* INPUT: Không có
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Array of String - danh sách categories,
* count: Number - số lượng categories
* }
*/
exports.getAllCategories = async (req, res) => {
try {
const categories = await Vocab.findAll({
where: {
is_active: true,
category: { [Op.ne]: null }
},
attributes: ['category'],
group: ['category'],
order: [['category', 'ASC']],
raw: true
});
const categoryList = categories.map(c => c.category);
res.json({
success: true,
message: 'Categories retrieved successfully',
data: categoryList,
count: categoryList.length
});
} catch (error) {
console.error('Error getting categories:', error);
res.status(500).json({
success: false,
message: 'Error retrieving categories',
error: error.message
});
}
};
/**
* ============================================
* GET ALL TOPICS
* ============================================
* Lấy danh sách tất cả topics có trong database
*
* @route GET /api/vocab/meta/topics
* @access Private
*
* INPUT: Không có
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Array of String - danh sách topics,
* count: Number - số lượng topics
* }
*/
exports.getAllTopics = async (req, res) => {
try {
const topics = await Vocab.findAll({
where: {
is_active: true,
topic: { [Op.ne]: null }
},
attributes: ['topic'],
group: ['topic'],
order: [['topic', 'ASC']],
raw: true
});
const topicList = topics.map(t => t.topic);
res.json({
success: true,
message: 'Topics retrieved successfully',
data: topicList,
count: topicList.length
});
} catch (error) {
console.error('Error getting topics:', error);
res.status(500).json({
success: false,
message: 'Error retrieving topics',
error: error.message
});
}
};