update sentences API
All checks were successful
Deploy to Production / deploy (push) Successful in 22s
All checks were successful
Deploy to Production / deploy (push) Successful in 22s
This commit is contained in:
3
app.js
3
app.js
@@ -36,6 +36,7 @@ const gameTypeRoutes = require('./routes/gameTypeRoutes');
|
||||
const lessonRoutes = require('./routes/lessonRoutes');
|
||||
const chapterLessonRoutes = require('./routes/chapterLessonRoutes');
|
||||
const vocabRoutes = require('./routes/vocabRoutes');
|
||||
const sentencesRoutes = require('./routes/sentencesRoutes');
|
||||
const grammarRoutes = require('./routes/grammarRoutes');
|
||||
const storyRoutes = require('./routes/storyRoutes');
|
||||
const learningContentRoutes = require('./routes/learningContentRoutes');
|
||||
@@ -167,6 +168,7 @@ app.get('/api', (req, res) => {
|
||||
games: '/api/games',
|
||||
gameTypes: '/api/game-types',
|
||||
vocabs: '/api/vocabs',
|
||||
sentences: '/api/sentences',
|
||||
contexts: '/api/contexts',
|
||||
contextGuides: '/api/context-guides',
|
||||
upload: '/api/upload',
|
||||
@@ -224,6 +226,7 @@ app.use('/api/games', gameRoutes);
|
||||
app.use('/api/game-types', gameTypeRoutes);
|
||||
app.use('/api/lessons', lessonRoutes);
|
||||
app.use('/api/vocabs', vocabRoutes);
|
||||
app.use('/api/sentences', sentencesRoutes);
|
||||
app.use('/api/grammar', grammarRoutes);
|
||||
app.use('/api/stories', storyRoutes);
|
||||
app.use('/api/learning-content', learningContentRoutes);
|
||||
|
||||
@@ -387,6 +387,97 @@ class ContextController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search contexts with partial match on title/context + filter by type_image
|
||||
*
|
||||
* POST /api/contexts/search
|
||||
* Body:
|
||||
* {
|
||||
* search : String - tìm kiếm một phần trong title HOẶC context (OR)
|
||||
* title : String - tìm riêng trong title (LIKE)
|
||||
* context_text : String - tìm riêng trong context (LIKE)
|
||||
* type_image : String - exact match (e.g., 'small', 'square', 'normal')
|
||||
* type : String - exact match
|
||||
* status : Number - exact match
|
||||
* grade : Number - exact match
|
||||
* page : Number (default: 1)
|
||||
* limit : Number (default: 50)
|
||||
* }
|
||||
*/
|
||||
async searchContexts(req, res, next) {
|
||||
try {
|
||||
const {
|
||||
search,
|
||||
title,
|
||||
context_text,
|
||||
type_image,
|
||||
type,
|
||||
status,
|
||||
grade,
|
||||
page = 1,
|
||||
limit = 50
|
||||
} = req.body;
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
const offset = (page - 1) * limit;
|
||||
const where = {};
|
||||
|
||||
// ── Exact-match filters ──────────────────────────────────────────────
|
||||
if (type_image !== undefined && type_image !== null && type_image !== '') {
|
||||
where.type_image = type_image;
|
||||
}
|
||||
if (type) where.type = type;
|
||||
if (status !== undefined && status !== null) where.status = parseInt(status);
|
||||
if (grade !== undefined && grade !== null) where.grade = parseInt(grade);
|
||||
|
||||
// ── Text search ──────────────────────────────────────────────────────
|
||||
// `search` → title OR context (cả hai cùng lúc)
|
||||
// `title` → chỉ title
|
||||
// `context_text` → chỉ context
|
||||
const textConditions = [];
|
||||
|
||||
if (search) {
|
||||
textConditions.push(
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ context: { [Op.like]: `%${search}%` } }
|
||||
);
|
||||
}
|
||||
if (title) {
|
||||
textConditions.push({ title: { [Op.like]: `%${title}%` } });
|
||||
}
|
||||
if (context_text) {
|
||||
textConditions.push({ context: { [Op.like]: `%${context_text}%` } });
|
||||
}
|
||||
|
||||
if (textConditions.length > 0) {
|
||||
where[Op.or] = textConditions;
|
||||
}
|
||||
|
||||
const { count, rows } = await Context.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Search completed successfully',
|
||||
data: {
|
||||
contexts: rows,
|
||||
pagination: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
totalPages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update context (general update - use with caution)
|
||||
*/
|
||||
|
||||
796
controllers/sentencesController.js
Normal file
796
controllers/sentencesController.js
Normal file
@@ -0,0 +1,796 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
138
import-context-to-sentences.js
Normal file
138
import-context-to-sentences.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Import Context (status=5) → Sentences
|
||||
*
|
||||
* Logic:
|
||||
* 1. Lấy tất cả Context có status = 5
|
||||
* 2. So sánh Context.title với Sentences.text
|
||||
* - Nếu chưa có → tạo mới Sentence
|
||||
* - Nếu đã có → cập nhật image vào đúng mảng
|
||||
* 3. Xác định slot image dựa vào Context.image.split("_")[1]:
|
||||
* - "square" → image_square
|
||||
* - "small" → image_small
|
||||
* - "normal" → image_normal
|
||||
* 4. Chuyển Context.status → 6
|
||||
*/
|
||||
|
||||
const { sequelize } = require('./config/database');
|
||||
const Context = require('./models/Context');
|
||||
const Sentences = require('./models/Sentences');
|
||||
|
||||
// ─── helper: thêm URL vào mảng JSON (không trùng lặp) ───────────────────────
|
||||
function addToArray(existing, url) {
|
||||
const arr = Array.isArray(existing) ? [...existing] : [];
|
||||
if (!arr.includes(url)) arr.push(url);
|
||||
return arr;
|
||||
}
|
||||
|
||||
// ─── helper: xác định slot image từ tên file/URL ────────────────────────────
|
||||
function resolveImageSlot(imageUrl) {
|
||||
if (!imageUrl) return null;
|
||||
|
||||
// Lấy phần tên file (bỏ path/query)
|
||||
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||
const parts = filename.split('_');
|
||||
|
||||
// Duyệt tất cả phần tử, tìm keyword
|
||||
for (const part of parts) {
|
||||
const key = part.toLowerCase();
|
||||
if (key === 'square') return 'image_square';
|
||||
if (key === 'small') return 'image_small';
|
||||
if (key === 'normal') return 'image_normal';
|
||||
}
|
||||
|
||||
return null; // không xác định được
|
||||
}
|
||||
|
||||
// ─── main ────────────────────────────────────────────────────────────────────
|
||||
async function run() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Database connected\n');
|
||||
|
||||
// 1. Lấy tất cả Context có status = 5
|
||||
const contexts = await Context.findAll({
|
||||
where: { status: 5 }
|
||||
});
|
||||
|
||||
console.log(`📦 Tìm thấy ${contexts.length} Context(s) có status = 5\n`);
|
||||
|
||||
if (contexts.length === 0) {
|
||||
console.log('Không có gì để xử lý.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const ctx of contexts) {
|
||||
const text = ctx.context; // so sánh với Sentences.text
|
||||
const imageUrl = ctx.image;
|
||||
const imageSlot = resolveImageSlot(imageUrl);
|
||||
|
||||
if (!text) {
|
||||
console.warn(` ⚠️ Context [${ctx.uuid}] không có context — bỏ qua`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Tìm Sentence có text khớp
|
||||
let sentence = await Sentences.findOne({
|
||||
where: { text }
|
||||
});
|
||||
|
||||
if (!sentence) {
|
||||
// ── Tạo mới ──────────────────────────────────────────────────────────
|
||||
const newData = {
|
||||
text,
|
||||
is_active: true,
|
||||
image_small: [],
|
||||
image_square: [],
|
||||
image_normal: [],
|
||||
};
|
||||
|
||||
// Gán image vào đúng slot
|
||||
if (imageUrl && imageSlot) {
|
||||
newData[imageSlot] = [imageUrl];
|
||||
} else if (imageUrl) {
|
||||
console.warn(` ⚠️ Không xác định slot image từ URL: "${imageUrl}" — bỏ qua image`);
|
||||
}
|
||||
|
||||
await Sentences.create(newData);
|
||||
console.log(` ✅ [TẠO MỚI] "${text}"${imageSlot ? ` → ${imageSlot}` : ''}`);
|
||||
created++;
|
||||
|
||||
} else {
|
||||
// ── Cập nhật image ───────────────────────────────────────────────────
|
||||
if (imageUrl && imageSlot) {
|
||||
const updatedArr = addToArray(sentence[imageSlot], imageUrl);
|
||||
await sentence.update({ [imageSlot]: updatedArr });
|
||||
console.log(` 🔄 [CẬP NHẬT] "${text}" → ${imageSlot} (+1 ảnh)`);
|
||||
updated++;
|
||||
} else {
|
||||
console.warn(` ⚠️ [BỎ QUA IMAGE] "${text}" — URL trống hoặc không xác định slot`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Chuyển Context.status → 6
|
||||
await ctx.update({ status: 6 });
|
||||
}
|
||||
|
||||
console.log('\n─────────────────────────────────────');
|
||||
console.log(`📊 Kết quả:`);
|
||||
console.log(` ✅ Tạo mới : ${created}`);
|
||||
console.log(` 🔄 Cập nhật : ${updated}`);
|
||||
console.log(` ⚠️ Bỏ qua : ${skipped}`);
|
||||
console.log(` 📌 Tổng : ${contexts.length}`);
|
||||
console.log('─────────────────────────────────────');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Lỗi:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -35,34 +35,16 @@ const Lesson = sequelize.define('lessons', {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Mô tả bài học'
|
||||
},
|
||||
|
||||
// Dạng 1: JSON Content - Nội dung học tập dạng JSON
|
||||
// Cấu trúc content_json phụ thuộc vào lesson_content_type:
|
||||
// - vocabulary: { type: "vocabulary", vocabulary_ids: [uuid1, uuid2, ...], exercises: [...] }
|
||||
// - grammar: { type: "grammar", grammar_ids: [uuid1, uuid2, ...], examples: [...], exercises: [...] }
|
||||
// - phonics: { type: "phonics", phonics_rules: [{ipa: "/æ/", words: [...]}], exercises: [...] }
|
||||
// - review: { type: "review", sections: [{type: "vocabulary", ...}, {type: "grammar", ...}, {type: "phonics", ...}] }
|
||||
content_json: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Nội dung học tập dạng JSON: vocabulary, grammar, phonics, review'
|
||||
},
|
||||
|
||||
// Loại nội dung của bài học (để query dễ dàng)
|
||||
lesson_content_type: {
|
||||
type: DataTypes.ENUM('vocabulary', 'grammar', 'phonics', 'review', 'mixed'),
|
||||
allowNull: true,
|
||||
comment: 'Loại nội dung: vocabulary, grammar, phonics, review, mixed'
|
||||
},
|
||||
|
||||
// Dạng 2: URL Content - Chứa link external
|
||||
content_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
comment: 'URL nội dung: video, audio, document, external link'
|
||||
},
|
||||
content_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
comment: 'Loại content cho URL: video, audio, pdf, external_link, youtube, etc.'
|
||||
},
|
||||
duration_minutes: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Thời lượng (phút)'
|
||||
@@ -83,7 +65,7 @@ const Lesson = sequelize.define('lessons', {
|
||||
comment: 'Thứ tự hiển thị'
|
||||
},
|
||||
thumbnail_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'URL ảnh thumbnail'
|
||||
},
|
||||
created_at: {
|
||||
|
||||
103
models/Sentences.js
Normal file
103
models/Sentences.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Sentences = sequelize.define('Sentences', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
comment: 'Unique identifier for sentence entry'
|
||||
},
|
||||
// Từ thực tế (wash, washes, washing, ate, eaten...)
|
||||
text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
ipa : {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'International Phonetic Alphabet representation'
|
||||
},
|
||||
// Nội dung dùng chung (có thể lưu JSON để dễ quản lý hoặc dùng chung cho cả group)
|
||||
vi: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Vietnamese meaning'
|
||||
},
|
||||
grade : {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '0',
|
||||
comment: 'Grade level (e.g., Grade 1, Grade 2)'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Category of the sentence (e.g., Action Verbs, Nouns)'
|
||||
},
|
||||
topic: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Topic of the sentence (e.g., Food, Travel, Education)'
|
||||
},
|
||||
image_small: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_square: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
image_normal: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of image URLs'
|
||||
},
|
||||
audio : {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Array of audio URLs'
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: 'Array of tags for categorization'
|
||||
},
|
||||
usage_note: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Lưu ý về ngữ cảnh sử dụng câu này'
|
||||
},
|
||||
etc : {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: 'Các thông tin khác liên quan đến câu này (ví dụ: level, grammar points, etc.)'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Whether this sentence entry is active'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'sentences',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_sentences_text',
|
||||
fields: [{ name: 'text', length: 191 }]
|
||||
},
|
||||
{
|
||||
name: 'idx_category',
|
||||
fields: [{ name: 'category', length: 191 }]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Sentences;
|
||||
@@ -36,6 +36,9 @@ const LessonLeaderboard = require('./LessonLeaderboard');
|
||||
// Group 3.2: Vocabulary System (NEW)
|
||||
const Vocab = require('./Vocab');
|
||||
|
||||
// Group 3.2.1: Sentences System (NEW)
|
||||
const Sentences = require('./Sentences');
|
||||
|
||||
// Group 3.3: Grammar System (NEW)
|
||||
const Grammar = require('./Grammar');
|
||||
const GrammarMapping = require('./GrammarMapping');
|
||||
@@ -289,6 +292,9 @@ module.exports = {
|
||||
|
||||
// Group 3.2: Vocabulary System (NEW)
|
||||
Vocab,
|
||||
|
||||
// Group 3.2.1: Sentences System (NEW)
|
||||
Sentences,
|
||||
|
||||
// Group 3.3: Grammar System (NEW)
|
||||
Grammar,
|
||||
|
||||
@@ -32,6 +32,10 @@ router.post('/:id/add-images', contextController.addImages);
|
||||
// Status 4 -> 5: Approve
|
||||
router.post('/:id/approve', contextController.approveContext);
|
||||
|
||||
// Search contexts: partial match on title/context + filter by type_image, status, etc.
|
||||
// Body: { search, title, context_text, type_image, type, status, grade, page, limit }
|
||||
router.post('/search', contextController.searchContexts);
|
||||
|
||||
// Get all contexts (with optional filters)
|
||||
router.get('/', contextController.getAllContexts);
|
||||
|
||||
|
||||
271
routes/sentencesRoutes.js
Normal file
271
routes/sentencesRoutes.js
Normal file
@@ -0,0 +1,271 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sentencesController = require('../controllers/sentencesController');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* POST /api/sentences
|
||||
* ============================================
|
||||
* Tạo một sentence entry mới
|
||||
*
|
||||
* 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
|
||||
* topic: String - chủ đề
|
||||
* image_small: JSON Array - mảng URLs hình ảnh nhỏ
|
||||
* image_square: JSON Array - mảng URLs hình ảnh vuông
|
||||
* image_normal: JSON Array - mảng URLs hình ảnh bình thường
|
||||
* audio: JSON Array - mảng URLs audio files
|
||||
* tags: JSON Array - các tags phân loại
|
||||
* usage_note: String - lưu ý 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 (bao gồm id, created_at, updated_at)
|
||||
* }
|
||||
**/
|
||||
router.post('/', sentencesController.createSentence);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* POST /api/sentences/bulk
|
||||
* ============================================
|
||||
* Tạo nhiều sentence entries cùng lúc
|
||||
*
|
||||
* INPUT:
|
||||
* {
|
||||
* 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
|
||||
* }
|
||||
**/
|
||||
router.post('/bulk', sentencesController.bulkCreateSentences);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* POST /api/sentences/search
|
||||
* ============================================
|
||||
* Tìm kiếm sentence nâng cao với nhiều filter
|
||||
*
|
||||
* INPUT:
|
||||
* {
|
||||
* topic: String (optional)
|
||||
* category: String (optional)
|
||||
* text: String (optional) - partial match
|
||||
* vi: String (optional) - partial match
|
||||
* 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 }
|
||||
* }
|
||||
**/
|
||||
router.post('/search', sentencesController.searchSentences);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences
|
||||
* ============================================
|
||||
* Lấy danh sách tất cả sentences với phân trang và filter
|
||||
*
|
||||
* INPUT (Query Parameters):
|
||||
* {
|
||||
* page: Number (mặc định: 1)
|
||||
* limit: Number (mặc định: 20)
|
||||
* category: String
|
||||
* topic: String
|
||||
* text: String
|
||||
* search: String - tìm kiếm trong text và vi
|
||||
* is_active: Boolean (mặc định: true)
|
||||
* }
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String,
|
||||
* data: Array of Sentence objects,
|
||||
* pagination: { total, page, limit, totalPages }
|
||||
* }
|
||||
**/
|
||||
router.get('/', sentencesController.getAllSentences);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences/stats/overview
|
||||
* ============================================
|
||||
* Lấy thống kê tổng quan về sentences
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String,
|
||||
* data: {
|
||||
* total: { active, inactive, all },
|
||||
* by_category: Array [{category, count}],
|
||||
* by_topic: Array [{topic, count}]
|
||||
* }
|
||||
* }
|
||||
**/
|
||||
router.get('/stats/overview', sentencesController.getSentenceStats);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences/meta/categories
|
||||
* ============================================
|
||||
* Lấy danh sách tất cả categories
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String,
|
||||
* data: Array of String,
|
||||
* count: Number
|
||||
* }
|
||||
**/
|
||||
router.get('/meta/categories', sentencesController.getAllCategories);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences/meta/topics
|
||||
* ============================================
|
||||
* Lấy danh sách tất cả topics
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String,
|
||||
* data: Array of String,
|
||||
* count: Number
|
||||
* }
|
||||
**/
|
||||
router.get('/meta/topics', sentencesController.getAllTopics);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences/missing/ipa
|
||||
* ============================================
|
||||
* Lấy tất cả sentences chưa có IPA
|
||||
*
|
||||
* 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 }
|
||||
* }
|
||||
**/
|
||||
router.get('/missing/ipa', sentencesController.getSentencesWithoutIpa);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences/missing/images
|
||||
* ============================================
|
||||
* Lấy tất cả sentences chưa đủ hình ảnh
|
||||
*
|
||||
* 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 }
|
||||
* }
|
||||
**/
|
||||
router.get('/missing/images', sentencesController.getSentencesWithoutImages);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* GET /api/sentences/:id
|
||||
* ============================================
|
||||
* Lấy chi tiết một sentence theo ID
|
||||
*
|
||||
* INPUT (URL Parameter):
|
||||
* {
|
||||
* id: UUID - id của sentence cần lấy
|
||||
* }
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String,
|
||||
* data: Sentence object
|
||||
* }
|
||||
**/
|
||||
router.get('/:id', sentencesController.getSentenceById);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* PUT /api/sentences/:id
|
||||
* ============================================
|
||||
* Cập nhật thông tin sentence
|
||||
*
|
||||
* INPUT (URL Parameter + Body):
|
||||
* {
|
||||
* id: UUID - id cần update
|
||||
* Body: Object - các trường cần update
|
||||
* {
|
||||
* text, ipa, vi, category, topic,
|
||||
* image_small, image_square, image_normal,
|
||||
* audio, tags, usage_note, etc, is_active
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String,
|
||||
* data: Updated Sentence object
|
||||
* }
|
||||
**/
|
||||
router.put('/:id', sentencesController.updateSentence);
|
||||
|
||||
/**
|
||||
* ============================================
|
||||
* DELETE /api/sentences/:id
|
||||
* ============================================
|
||||
* Xóa mềm sentence (set is_active = false)
|
||||
*
|
||||
* INPUT (URL Parameter):
|
||||
* {
|
||||
* id: UUID - id cần xóa
|
||||
* }
|
||||
*
|
||||
* OUTPUT:
|
||||
* {
|
||||
* success: Boolean,
|
||||
* message: String
|
||||
* }
|
||||
**/
|
||||
router.delete('/:id', sentencesController.deleteSentence);
|
||||
|
||||
module.exports = router;
|
||||
34
sync-sentences.js
Normal file
34
sync-sentences.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Sync only the Sentences model to database
|
||||
*/
|
||||
const { sequelize } = require('./config/database');
|
||||
const Sentences = require('./models/Sentences');
|
||||
|
||||
async function syncSentences() {
|
||||
try {
|
||||
console.log('🔄 Syncing Sentences table...');
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Database connection OK');
|
||||
|
||||
await Sentences.sync({ alter: true });
|
||||
console.log('✅ Sentences table synced successfully');
|
||||
|
||||
const [tables] = await sequelize.query("SHOW TABLES LIKE 'sentences'");
|
||||
if (tables.length > 0) {
|
||||
console.log('✅ Table "sentences" confirmed in database');
|
||||
}
|
||||
|
||||
const [columns] = await sequelize.query("SHOW COLUMNS FROM sentences");
|
||||
console.log('\n📋 Columns in sentences table:');
|
||||
columns.forEach(col => {
|
||||
console.log(` - ${col.Field} (${col.Type})`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error syncing Sentences:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
syncSentences();
|
||||
Reference in New Issue
Block a user