797 lines
18 KiB
JavaScript
797 lines
18 KiB
JavaScript
const { Sentences } = require('../models');
|
|
const { Op } = require('sequelize');
|
|
|
|
/**
|
|
* ============================================
|
|
* CREATE SENTENCE
|
|
* ============================================
|
|
* Tạo mới một sentence entry
|
|
*
|
|
* @route POST /api/sentences
|
|
* @access Private (requires authentication)
|
|
*
|
|
* INPUT:
|
|
* {
|
|
* text: String (required) - nội dung câu
|
|
* ipa: String - phiên âm IPA
|
|
* vi: String - nghĩa tiếng Việt
|
|
* category: String - category của câu (e.g., Action Verbs, Nouns)
|
|
* topic: String - chủ đề (e.g., Food, Travel, Education)
|
|
* 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
|
|
* tags: JSON Array - các tags để phân loại
|
|
* usage_note: String - lưu ý về ngữ cảnh sử dụng
|
|
* etc: String - các thông tin khác
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Sentence object đã tạo
|
|
* }
|
|
*/
|
|
exports.createSentence = async (req, res) => {
|
|
try {
|
|
const {
|
|
text,
|
|
ipa,
|
|
vi,
|
|
category,
|
|
topic,
|
|
image_small,
|
|
image_square,
|
|
image_normal,
|
|
audio,
|
|
tags,
|
|
usage_note,
|
|
etc
|
|
} = req.body;
|
|
|
|
// Validate required fields
|
|
if (!text) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'text is a required field'
|
|
});
|
|
}
|
|
|
|
const sentence = await Sentences.create({
|
|
text,
|
|
ipa: ipa || null,
|
|
vi: vi || '',
|
|
category: category || null,
|
|
topic: topic || null,
|
|
image_small: image_small || [],
|
|
image_square: image_square || [],
|
|
image_normal: image_normal || [],
|
|
audio: audio || null,
|
|
tags: tags || [],
|
|
usage_note: usage_note || '',
|
|
etc: etc || '',
|
|
is_active: true
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Sentence created successfully',
|
|
data: sentence
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error creating sentence:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error creating sentence',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* GET ALL SENTENCES
|
|
* ============================================
|
|
* Lấy danh sách sentences với phân trang và filter
|
|
*
|
|
* @route GET /api/sentences
|
|
* @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
|
|
* text: String - lọc theo text chính xác
|
|
* search: String - tìm kiếm trong text 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 Sentence objects,
|
|
* pagination: { total, page, limit, totalPages }
|
|
* }
|
|
*/
|
|
exports.getAllSentences = async (req, res) => {
|
|
try {
|
|
const {
|
|
page = 1,
|
|
limit = 20,
|
|
category,
|
|
topic,
|
|
text,
|
|
search,
|
|
is_active = true
|
|
} = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const where = {};
|
|
|
|
if (is_active !== undefined) {
|
|
where.is_active = is_active === 'true' || is_active === true;
|
|
}
|
|
|
|
if (category) {
|
|
where.category = category;
|
|
}
|
|
|
|
if (topic) {
|
|
where.topic = topic;
|
|
}
|
|
|
|
if (text) {
|
|
where.text = text;
|
|
}
|
|
|
|
if (search) {
|
|
where[Op.or] = [
|
|
{ text: { [Op.like]: `%${search}%` } },
|
|
{ vi: { [Op.like]: `%${search}%` } }
|
|
];
|
|
}
|
|
|
|
const { count, rows } = await Sentences.findAndCountAll({
|
|
where,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
order: [['created_at', 'DESC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Sentences retrieved successfully',
|
|
data: rows,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(count / limit)
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error getting sentences:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error retrieving sentences',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* GET SENTENCE BY ID
|
|
* ============================================
|
|
* Lấy chi tiết một sentence theo id
|
|
*
|
|
* @route GET /api/sentences/:id
|
|
* @access Private
|
|
*
|
|
* INPUT (URL Parameter):
|
|
* {
|
|
* id: UUID - id của sentence cần lấy
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Sentence object với đầy đủ thông tin
|
|
* }
|
|
*/
|
|
exports.getSentenceById = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const sentence = await Sentences.findOne({
|
|
where: {
|
|
id,
|
|
is_active: true
|
|
}
|
|
});
|
|
|
|
if (!sentence) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Sentence not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Sentence retrieved successfully',
|
|
data: sentence
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error getting sentence:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error retrieving sentence',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* GET SENTENCES WITHOUT IPA
|
|
* ============================================
|
|
* Lấy tất cả các sentences chưa có IPA
|
|
*
|
|
* @route GET /api/sentences/missing/ipa
|
|
* @access Private
|
|
*
|
|
* INPUT (Query Parameters):
|
|
* {
|
|
* page: Number (mặc định: 1)
|
|
* limit: Number (mặc định: 50)
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Array of Sentence objects,
|
|
* pagination: { total, page, limit, totalPages }
|
|
* }
|
|
*/
|
|
exports.getSentencesWithoutIpa = async (req, res) => {
|
|
try {
|
|
const { page = 1, limit = 50 } = req.query;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const { count, rows } = await Sentences.findAndCountAll({
|
|
where: {
|
|
is_active: true,
|
|
[Op.or]: [
|
|
{ ipa: null },
|
|
{ ipa: '' }
|
|
]
|
|
},
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
order: [['text', 'ASC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Sentences 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 sentences without IPA:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error retrieving sentences without IPA',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* GET SENTENCES WITHOUT IMAGES
|
|
* ============================================
|
|
* Lấy tất cả các sentences chưa đủ hình ảnh
|
|
*
|
|
* @route GET /api/sentences/missing/images
|
|
* @access Private
|
|
*
|
|
* INPUT (Query Parameters):
|
|
* {
|
|
* page: Number (mặc định: 1)
|
|
* limit: Number (mặc định: 50)
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Array of Sentence objects,
|
|
* pagination: { total, page, limit, totalPages }
|
|
* }
|
|
*/
|
|
exports.getSentencesWithoutImages = async (req, res) => {
|
|
try {
|
|
const { page = 1, limit = 50 } = req.query;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const { count, rows } = await Sentences.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: 'Sentences 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 sentences without images:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error retrieving sentences without images',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* UPDATE SENTENCE
|
|
* ============================================
|
|
* Cập nhật thông tin sentence
|
|
*
|
|
* @route PUT /api/sentences/:id
|
|
* @access Private
|
|
*
|
|
* INPUT (URL Parameter + Body):
|
|
* {
|
|
* id: UUID - id cần update
|
|
* Body: Object - các trường cần update
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Updated Sentence object
|
|
* }
|
|
*/
|
|
exports.updateSentence = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const updateData = req.body;
|
|
|
|
const sentence = await Sentences.findOne({
|
|
where: { id }
|
|
});
|
|
|
|
if (!sentence) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Sentence not found'
|
|
});
|
|
}
|
|
|
|
await sentence.update(updateData);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Sentence updated successfully',
|
|
data: sentence
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error updating sentence:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error updating sentence',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* DELETE SENTENCE (SOFT DELETE)
|
|
* ============================================
|
|
* Xóa mềm sentence (set is_active = false)
|
|
*
|
|
* @route DELETE /api/sentences/:id
|
|
* @access Private
|
|
*
|
|
* INPUT (URL Parameter):
|
|
* {
|
|
* id: UUID - id cần xóa
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String
|
|
* }
|
|
*/
|
|
exports.deleteSentence = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const sentence = await Sentences.findOne({
|
|
where: { id }
|
|
});
|
|
|
|
if (!sentence) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Sentence not found'
|
|
});
|
|
}
|
|
|
|
await sentence.update({ is_active: false });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Sentence deleted successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting sentence:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error deleting sentence',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* BULK CREATE SENTENCES
|
|
* ============================================
|
|
* Tạo nhiều sentences cùng lúc
|
|
*
|
|
* @route POST /api/sentences/bulk
|
|
* @access Private
|
|
*
|
|
* INPUT (Body):
|
|
* {
|
|
* sentences: Array of Sentence objects - mỗi object phải có text
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Array of created Sentence objects,
|
|
* count: Number - số lượng đã tạo
|
|
* }
|
|
*/
|
|
exports.bulkCreateSentences = async (req, res) => {
|
|
try {
|
|
const { sentences } = req.body;
|
|
|
|
if (!Array.isArray(sentences) || sentences.length === 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'sentences must be a non-empty array'
|
|
});
|
|
}
|
|
|
|
// Validate each sentence has required fields
|
|
for (let i = 0; i < sentences.length; i++) {
|
|
if (!sentences[i].text) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: `Sentence at index ${i} is missing required field (text)`
|
|
});
|
|
}
|
|
}
|
|
|
|
const createdSentences = await Sentences.bulkCreate(sentences);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: `${createdSentences.length} sentences created successfully`,
|
|
data: createdSentences,
|
|
count: createdSentences.length
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error bulk creating sentences:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error creating sentences',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* GET SENTENCE STATISTICS
|
|
* ============================================
|
|
* Lấy thống kê về sentences
|
|
*
|
|
* @route GET /api/sentences/stats/overview
|
|
* @access Private
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: {
|
|
* total: { active, inactive, all },
|
|
* by_category: Array [{category, count}],
|
|
* by_topic: Array [{topic, count}]
|
|
* }
|
|
* }
|
|
*/
|
|
exports.getSentenceStats = async (req, res) => {
|
|
try {
|
|
const { sequelize } = Sentences;
|
|
|
|
const totalActive = await Sentences.count({ where: { is_active: true } });
|
|
const totalInactive = await Sentences.count({ where: { is_active: false } });
|
|
|
|
const byCategory = await Sentences.findAll({
|
|
where: { is_active: true },
|
|
attributes: [
|
|
'category',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['category'],
|
|
order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']],
|
|
raw: true
|
|
});
|
|
|
|
const byTopic = await Sentences.findAll({
|
|
where: { is_active: true },
|
|
attributes: [
|
|
'topic',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['topic'],
|
|
order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']],
|
|
raw: true
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Statistics retrieved successfully',
|
|
data: {
|
|
total: {
|
|
active: totalActive,
|
|
inactive: totalInactive,
|
|
all: totalActive + totalInactive
|
|
},
|
|
by_category: byCategory,
|
|
by_topic: byTopic
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error getting sentence stats:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error retrieving statistics',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* SEARCH SENTENCES
|
|
* ============================================
|
|
* Tìm kiếm sentences nâng cao với nhiều filter
|
|
*
|
|
* @route POST /api/sentences/search
|
|
* @access Private
|
|
*
|
|
* INPUT (Body):
|
|
* {
|
|
* topic: String (optional)
|
|
* category: String (optional)
|
|
* text: String (optional) - partial match
|
|
* vi: String (optional) - partial match
|
|
* tags: Array (optional) - lọc theo tags
|
|
* page: Number (mặc định: 1)
|
|
* limit: Number (mặc định: 100)
|
|
* }
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Array of Sentence objects,
|
|
* pagination: { total, page, limit, totalPages }
|
|
* }
|
|
*/
|
|
exports.searchSentences = async (req, res) => {
|
|
try {
|
|
const {
|
|
topic,
|
|
category,
|
|
text,
|
|
vi,
|
|
page = 1,
|
|
limit = 100
|
|
} = req.body;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const where = { is_active: true };
|
|
|
|
if (topic) {
|
|
where.topic = topic;
|
|
}
|
|
|
|
if (category) {
|
|
where.category = category;
|
|
}
|
|
|
|
if (text) {
|
|
where.text = { [Op.like]: `%${text}%` };
|
|
}
|
|
|
|
if (vi) {
|
|
where.vi = { [Op.like]: `%${vi}%` };
|
|
}
|
|
|
|
const { count, rows } = await Sentences.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 sentences:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error searching sentences',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ============================================
|
|
* GET ALL CATEGORIES
|
|
* ============================================
|
|
* Lấy danh sách tất cả categories có trong database
|
|
*
|
|
* @route GET /api/sentences/meta/categories
|
|
* @access Private
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Array of String,
|
|
* count: Number
|
|
* }
|
|
*/
|
|
exports.getAllCategories = async (req, res) => {
|
|
try {
|
|
const categories = await Sentences.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/sentences/meta/topics
|
|
* @access Private
|
|
*
|
|
* OUTPUT:
|
|
* {
|
|
* success: Boolean,
|
|
* message: String,
|
|
* data: Array of String,
|
|
* count: Number
|
|
* }
|
|
*/
|
|
exports.getAllTopics = async (req, res) => {
|
|
try {
|
|
const topics = await Sentences.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
|
|
});
|
|
}
|
|
};
|