update Stories
All checks were successful
Deploy to Production / deploy (push) Successful in 20s

This commit is contained in:
silverpro89
2026-02-24 16:31:06 +07:00
parent d0f41920f7
commit 65820bb938
4 changed files with 207 additions and 16 deletions

View File

@@ -14,6 +14,8 @@ exports.createStory = async (req, res) => {
vocabulary = [], vocabulary = [],
context = [], context = [],
grade = [], grade = [],
grade_number = 0,
type = 'story',
tag = [] tag = []
} = req.body; } = req.body;
@@ -33,6 +35,8 @@ exports.createStory = async (req, res) => {
vocabulary, vocabulary,
context, context,
grade, grade,
grade_number,
type,
tag tag
}, { transaction }); }, { transaction });
@@ -66,6 +70,9 @@ exports.getAllStories = async (req, res) => {
search, search,
grade_filter, grade_filter,
tag_filter, tag_filter,
type,
grade_start,
grade_end,
sort_by = 'created_at', sort_by = 'created_at',
sort_order = 'DESC' sort_order = 'DESC'
} = req.query; } = req.query;
@@ -78,7 +85,27 @@ exports.getAllStories = async (req, res) => {
where.name = { [Op.like]: `%${search}%` }; where.name = { [Op.like]: `%${search}%` };
} }
// Grade filter // Type filter (exact match)
if (type) {
where.type = type;
}
// Grade number range filter
if (grade_start !== undefined && grade_end !== undefined) {
const s = parseInt(grade_start);
const e = parseInt(grade_end);
if (s === e) {
where.grade_number = s;
} else if (s < e) {
where.grade_number = { [Op.between]: [s, e] };
}
} else if (grade_start !== undefined) {
where.grade_number = { [Op.gte]: parseInt(grade_start) };
} else if (grade_end !== undefined) {
where.grade_number = { [Op.lte]: parseInt(grade_end) };
}
// Grade filter (legacy JSON array search)
if (grade_filter) { if (grade_filter) {
where.grade = { where.grade = {
[Op.contains]: [grade_filter] [Op.contains]: [grade_filter]
@@ -165,6 +192,8 @@ exports.updateStory = async (req, res) => {
vocabulary, vocabulary,
context, context,
grade, grade,
grade_number,
type,
tag tag
} = req.body; } = req.body;
@@ -185,6 +214,8 @@ exports.updateStory = async (req, res) => {
if (vocabulary !== undefined) updateData.vocabulary = vocabulary; if (vocabulary !== undefined) updateData.vocabulary = vocabulary;
if (context !== undefined) updateData.context = context; if (context !== undefined) updateData.context = context;
if (grade !== undefined) updateData.grade = grade; if (grade !== undefined) updateData.grade = grade;
if (grade_number !== undefined) updateData.grade_number = grade_number;
if (type !== undefined) updateData.type = type;
if (tag !== undefined) updateData.tag = tag; if (tag !== undefined) updateData.tag = tag;
await story.update(updateData, { transaction }); await story.update(updateData, { transaction });
@@ -245,23 +276,37 @@ exports.deleteStory = async (req, res) => {
*/ */
exports.getStoriesByGrade = async (req, res) => { exports.getStoriesByGrade = async (req, res) => {
try { try {
const { grade } = req.query; const { grade, start, end } = req.query;
if (!grade) { const where = {};
// grade_number range search
if (start !== undefined && end !== undefined) {
const s = parseInt(start);
const e = parseInt(end);
if (s === e) {
where.grade_number = s;
} else if (s < e) {
where.grade_number = { [Op.between]: [s, e] };
}
} else if (start !== undefined) {
where.grade_number = { [Op.gte]: parseInt(start) };
} else if (end !== undefined) {
where.grade_number = { [Op.lte]: parseInt(end) };
} else if (grade) {
// Legacy: search by JSON array
where.grade = { [Op.contains]: [grade] };
} else {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Grade parameter is required' message: 'Provide grade, or start/end parameters'
}); });
} }
const stories = await Story.findAll({ const stories = await Story.findAll({
where: { where,
grade: { order: [['grade_number', 'ASC'], ['created_at', 'DESC']],
[Op.contains]: [grade] attributes: ['id', 'name', 'logo', 'type', 'grade', 'grade_number', 'tag', 'created_at']
}
},
order: [['created_at', 'DESC']],
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at']
}); });
res.json({ res.json({
@@ -320,6 +365,41 @@ exports.getStoriesByTag = async (req, res) => {
} }
}; };
/**
* READ: Get stories by type
*/
exports.getStoriesByType = async (req, res) => {
try {
const { type } = req.query;
if (!type) {
return res.status(400).json({
success: false,
message: 'Type parameter is required'
});
}
const stories = await Story.findAll({
where: { type },
order: [['grade_number', 'ASC'], ['created_at', 'DESC']]
});
res.json({
success: true,
data: stories,
count: stories.length
});
} catch (error) {
console.error('Error fetching stories by type:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch stories by type',
error: error.message
});
}
};
/** /**
* READ: Get story statistics * READ: Get story statistics
*/ */
@@ -327,12 +407,24 @@ exports.getStoryStats = async (req, res) => {
try { try {
const totalStories = await Story.count(); const totalStories = await Story.count();
// Get all stories to analyze grades and tags // Count by type (SQL GROUP BY)
const typeRows = await sequelize.query(
`SELECT type, COUNT(*) as count FROM stories GROUP BY type ORDER BY count DESC`,
{ type: sequelize.QueryTypes.SELECT }
);
// Count by grade_number (SQL GROUP BY)
const gradeNumberRows = await sequelize.query(
`SELECT grade_number, COUNT(*) as count FROM stories GROUP BY grade_number ORDER BY grade_number ASC`,
{ type: sequelize.QueryTypes.SELECT }
);
// Get all stories to analyze legacy grade array and tags
const allStories = await Story.findAll({ const allStories = await Story.findAll({
attributes: ['grade', 'tag'] attributes: ['grade', 'tag']
}); });
// Count by grade // Count by legacy grade array
const gradeStats = {}; const gradeStats = {};
allStories.forEach(story => { allStories.forEach(story => {
if (story.grade && Array.isArray(story.grade)) { if (story.grade && Array.isArray(story.grade)) {
@@ -356,6 +448,8 @@ exports.getStoryStats = async (req, res) => {
success: true, success: true,
data: { data: {
total: totalStories, total: totalStories,
by_type: typeRows.map(r => ({ type: r.type || 'story', count: parseInt(r.count) })),
by_grade_number: gradeNumberRows.map(r => ({ grade_number: r.grade_number, count: parseInt(r.count) })),
by_grade: Object.entries(gradeStats).map(([grade, count]) => ({ grade, count })), by_grade: Object.entries(gradeStats).map(([grade, count]) => ({ grade, count })),
by_tag: Object.entries(tagStats).map(([tag, count]) => ({ tag, count })) by_tag: Object.entries(tagStats).map(([tag, count]) => ({ tag, count }))
} }

View File

@@ -36,12 +36,24 @@ const Story = sequelize.define('stories', {
defaultValue: [], defaultValue: [],
comment: 'Array of story context objects with images, text, audio data' comment: 'Array of story context objects with images, text, audio data'
}, },
type: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: 'story',
comment: 'Type of media content'
},
grade: { grade: {
type: DataTypes.JSON, type: DataTypes.JSON,
allowNull: true, allowNull: true,
defaultValue: [], defaultValue: [],
comment: 'Array of grade levels (e.g., ["Grade 1", "Grade 2"])' comment: 'Array of grade levels (e.g., ["Grade 1", "Grade 2"])'
}, },
grade_number: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 0,
comment: 'Numeric representation of grade unit lesson as GG UU LL'
},
tag: { tag: {
type: DataTypes.JSON, type: DataTypes.JSON,
allowNull: true, allowNull: true,

View File

@@ -65,6 +65,13 @@ const { authenticateToken } = require('../middleware/auth');
* items: * items:
* type: string * type: string
* example: ["Grade 1", "Grade 2"] * example: ["Grade 1", "Grade 2"]
* grade_number:
* type: integer
* example: 10101
* description: "Numeric code GG UU LL (e.g. 10101 = grade 1, unit 1, lesson 1)"
* type:
* type: string
* example: "story"
* tag: * tag:
* type: array * type: array
* items: * items:
@@ -107,10 +114,25 @@ router.post('/', storyController.createStory);
* type: string * type: string
* description: Search in story name * description: Search in story name
* - in: query * - in: query
* name: type
* schema:
* type: string
* description: Filter by story type (exact match, e.g. "story", "poem")
* - in: query
* name: grade_start
* schema:
* type: integer
* description: "grade_number range start (GG UU LL). If equal to grade_end → exact match"
* - in: query
* name: grade_end
* schema:
* type: integer
* description: "grade_number range end. Returns stories where grade_number >= grade_start AND <= grade_end"
* - in: query
* name: grade_filter * name: grade_filter
* schema: * schema:
* type: string * type: string
* description: Filter by grade (e.g., "Grade 1") * description: "Legacy: filter by grade JSON array value (e.g. \"Grade 1\")"
* - in: query * - in: query
* name: tag_filter * name: tag_filter
* schema: * schema:
@@ -148,10 +170,19 @@ router.get('/', storyController.getAllStories);
* parameters: * parameters:
* - in: query * - in: query
* name: grade * name: grade
* required: true
* schema: * schema:
* type: string * type: string
* description: Grade level (e.g., "Grade 1") * description: "Legacy: grade level string (e.g. \"Grade 1\")"
* - in: query
* name: start
* schema:
* type: integer
* description: "grade_number range start (GG UU LL). If start == end → exact match"
* - in: query
* name: end
* schema:
* type: integer
* description: "grade_number range end. Returns stories where grade_number >= start AND <= end"
* responses: * responses:
* 200: * 200:
* description: List of stories for the specified grade * description: List of stories for the specified grade
@@ -187,6 +218,31 @@ router.get('/grade', storyController.getStoriesByGrade);
*/ */
router.get('/tag', storyController.getStoriesByTag); router.get('/tag', storyController.getStoriesByTag);
/**
* @swagger
* /api/stories/type:
* get:
* summary: Get stories by type
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* required: true
* schema:
* type: string
* description: Story type (e.g. "story", "poem", "dialogue")
* responses:
* 200:
* description: List of stories with the specified type
* 400:
* description: Missing type parameter
* 500:
* description: Server error
*/
router.get('/type', storyController.getStoriesByType);
/** /**
* @swagger * @swagger
* /api/stories/guide: * /api/stories/guide:

29
sync-story.js Normal file
View File

@@ -0,0 +1,29 @@
/**
* Sync only the Story model to database
*/
const { sequelize } = require('./config/database');
const Story = require('./models/Story');
async function syncStory() {
try {
console.log('🔄 Syncing Story (stories) table...');
await sequelize.authenticate();
console.log('✅ Database connection OK');
await Story.sync({ alter: true });
console.log('✅ Story table synced successfully');
const [columns] = await sequelize.query('SHOW COLUMNS FROM stories');
console.log('\n📋 Columns in stories table:');
columns.forEach(col => {
console.log(` - ${col.Field} (${col.Type}) default: ${col.Default}`);
});
process.exit(0);
} catch (error) {
console.error('❌ Error syncing Story:', error.message);
process.exit(1);
}
}
syncStory();