This commit is contained in:
@@ -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 }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
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