diff --git a/controllers/storyController.js b/controllers/storyController.js index d4155f8..1536ec6 100644 --- a/controllers/storyController.js +++ b/controllers/storyController.js @@ -14,6 +14,8 @@ exports.createStory = async (req, res) => { vocabulary = [], context = [], grade = [], + grade_number = 0, + type = 'story', tag = [] } = req.body; @@ -33,6 +35,8 @@ exports.createStory = async (req, res) => { vocabulary, context, grade, + grade_number, + type, tag }, { transaction }); @@ -66,6 +70,9 @@ exports.getAllStories = async (req, res) => { search, grade_filter, tag_filter, + type, + grade_start, + grade_end, sort_by = 'created_at', sort_order = 'DESC' } = req.query; @@ -78,7 +85,27 @@ exports.getAllStories = async (req, res) => { 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) { where.grade = { [Op.contains]: [grade_filter] @@ -165,6 +192,8 @@ exports.updateStory = async (req, res) => { vocabulary, context, grade, + grade_number, + type, tag } = req.body; @@ -185,6 +214,8 @@ exports.updateStory = async (req, res) => { if (vocabulary !== undefined) updateData.vocabulary = vocabulary; if (context !== undefined) updateData.context = context; 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; await story.update(updateData, { transaction }); @@ -245,23 +276,37 @@ exports.deleteStory = async (req, res) => { */ exports.getStoriesByGrade = async (req, res) => { 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({ success: false, - message: 'Grade parameter is required' + message: 'Provide grade, or start/end parameters' }); } const stories = await Story.findAll({ - where: { - grade: { - [Op.contains]: [grade] - } - }, - order: [['created_at', 'DESC']], - attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at'] + where, + order: [['grade_number', 'ASC'], ['created_at', 'DESC']], + attributes: ['id', 'name', 'logo', 'type', 'grade', 'grade_number', 'tag', 'created_at'] }); 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 */ @@ -327,12 +407,24 @@ exports.getStoryStats = async (req, res) => { try { 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({ attributes: ['grade', 'tag'] }); - // Count by grade + // Count by legacy grade array const gradeStats = {}; allStories.forEach(story => { if (story.grade && Array.isArray(story.grade)) { @@ -356,6 +448,8 @@ exports.getStoryStats = async (req, res) => { success: true, data: { 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_tag: Object.entries(tagStats).map(([tag, count]) => ({ tag, count })) } diff --git a/models/Story.js b/models/Story.js index 990628b..916a106 100644 --- a/models/Story.js +++ b/models/Story.js @@ -36,12 +36,24 @@ const Story = sequelize.define('stories', { defaultValue: [], 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: { type: DataTypes.JSON, allowNull: true, defaultValue: [], 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: { type: DataTypes.JSON, allowNull: true, diff --git a/routes/storyRoutes.js b/routes/storyRoutes.js index 2e4bbf7..a65c537 100644 --- a/routes/storyRoutes.js +++ b/routes/storyRoutes.js @@ -65,6 +65,13 @@ const { authenticateToken } = require('../middleware/auth'); * items: * type: string * 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: * type: array * items: @@ -107,10 +114,25 @@ router.post('/', storyController.createStory); * type: string * description: Search in story name * - 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 * schema: * type: string - * description: Filter by grade (e.g., "Grade 1") + * description: "Legacy: filter by grade JSON array value (e.g. \"Grade 1\")" * - in: query * name: tag_filter * schema: @@ -148,10 +170,19 @@ router.get('/', storyController.getAllStories); * parameters: * - in: query * name: grade - * required: true * schema: * 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: * 200: * description: List of stories for the specified grade @@ -187,6 +218,31 @@ router.get('/grade', storyController.getStoriesByGrade); */ 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 * /api/stories/guide: diff --git a/sync-story.js b/sync-story.js new file mode 100644 index 0000000..cad8cf3 --- /dev/null +++ b/sync-story.js @@ -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();