1023 lines
25 KiB
JavaScript
1023 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,
|
|
form_key,
|
|
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,
|
|
form_key: form_key || 'base',
|
|
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
|
|
});
|
|
}
|
|
};
|