This commit is contained in:
@@ -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 }))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
29
sync-story.js
Normal file
29
sync-story.js
Normal 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();
|
||||
Reference in New Issue
Block a user