update
This commit is contained in:
642
controllers/storyController.js
Normal file
642
controllers/storyController.js
Normal file
@@ -0,0 +1,642 @@
|
||||
const { Story, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* CREATE: Add new story
|
||||
*/
|
||||
exports.createStory = async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
logo,
|
||||
vocabulary = [],
|
||||
context = [],
|
||||
grade = [],
|
||||
tag = []
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Missing required field: name'
|
||||
});
|
||||
}
|
||||
|
||||
// Create story
|
||||
const story = await Story.create({
|
||||
name,
|
||||
logo,
|
||||
vocabulary,
|
||||
context,
|
||||
grade,
|
||||
tag
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Story created successfully',
|
||||
data: story
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('Error creating story:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create story',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* READ: Get all stories with pagination and filters
|
||||
*/
|
||||
exports.getAllStories = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
search,
|
||||
grade_filter,
|
||||
tag_filter,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'DESC'
|
||||
} = req.query;
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(limit);
|
||||
const where = {};
|
||||
|
||||
// Search filter
|
||||
if (search) {
|
||||
where.name = { [Op.like]: `%${search}%` };
|
||||
}
|
||||
|
||||
// Grade filter
|
||||
if (grade_filter) {
|
||||
where.grade = {
|
||||
[Op.contains]: [grade_filter]
|
||||
};
|
||||
}
|
||||
|
||||
// Tag filter
|
||||
if (tag_filter) {
|
||||
where.tag = {
|
||||
[Op.contains]: [tag_filter]
|
||||
};
|
||||
}
|
||||
|
||||
const { count, rows } = await Story.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset,
|
||||
order: [[sort_by, sort_order.toUpperCase()]],
|
||||
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at', 'updated_at']
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
pagination: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
totalPages: Math.ceil(count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching stories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch stories',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* READ: Get story by ID
|
||||
*/
|
||||
exports.getStoryById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const story = await Story.findByPk(id);
|
||||
|
||||
if (!story) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Story not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: story
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching story:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch story',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UPDATE: Update story
|
||||
*/
|
||||
exports.updateStory = async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
name,
|
||||
logo,
|
||||
vocabulary,
|
||||
context,
|
||||
grade,
|
||||
tag
|
||||
} = req.body;
|
||||
|
||||
const story = await Story.findByPk(id);
|
||||
|
||||
if (!story) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Story not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Update fields
|
||||
const updateData = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (logo !== undefined) updateData.logo = logo;
|
||||
if (vocabulary !== undefined) updateData.vocabulary = vocabulary;
|
||||
if (context !== undefined) updateData.context = context;
|
||||
if (grade !== undefined) updateData.grade = grade;
|
||||
if (tag !== undefined) updateData.tag = tag;
|
||||
|
||||
await story.update(updateData, { transaction });
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Story updated successfully',
|
||||
data: story
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('Error updating story:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update story',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE: Delete story
|
||||
*/
|
||||
exports.deleteStory = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const story = await Story.findByPk(id);
|
||||
|
||||
if (!story) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Story not found'
|
||||
});
|
||||
}
|
||||
|
||||
await story.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Story deleted successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting story:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to delete story',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* READ: Get stories by grade
|
||||
*/
|
||||
exports.getStoriesByGrade = async (req, res) => {
|
||||
try {
|
||||
const { grade } = req.query;
|
||||
|
||||
if (!grade) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Grade parameter is required'
|
||||
});
|
||||
}
|
||||
|
||||
const stories = await Story.findAll({
|
||||
where: {
|
||||
grade: {
|
||||
[Op.contains]: [grade]
|
||||
}
|
||||
},
|
||||
order: [['created_at', 'DESC']],
|
||||
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at']
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stories,
|
||||
count: stories.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching stories by grade:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch stories by grade',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* READ: Get stories by tag
|
||||
*/
|
||||
exports.getStoriesByTag = async (req, res) => {
|
||||
try {
|
||||
const { tag } = req.query;
|
||||
|
||||
if (!tag) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tag parameter is required'
|
||||
});
|
||||
}
|
||||
|
||||
const stories = await Story.findAll({
|
||||
where: {
|
||||
tag: {
|
||||
[Op.contains]: [tag]
|
||||
}
|
||||
},
|
||||
order: [['created_at', 'DESC']],
|
||||
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at']
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stories,
|
||||
count: stories.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching stories by tag:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch stories by tag',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* READ: Get story statistics
|
||||
*/
|
||||
exports.getStoryStats = async (req, res) => {
|
||||
try {
|
||||
const totalStories = await Story.count();
|
||||
|
||||
// Get all stories to analyze grades and tags
|
||||
const allStories = await Story.findAll({
|
||||
attributes: ['grade', 'tag']
|
||||
});
|
||||
|
||||
// Count by grade
|
||||
const gradeStats = {};
|
||||
allStories.forEach(story => {
|
||||
if (story.grade && Array.isArray(story.grade)) {
|
||||
story.grade.forEach(g => {
|
||||
gradeStats[g] = (gradeStats[g] || 0) + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Count by tag
|
||||
const tagStats = {};
|
||||
allStories.forEach(story => {
|
||||
if (story.tag && Array.isArray(story.tag)) {
|
||||
story.tag.forEach(t => {
|
||||
tagStats[t] = (tagStats[t] || 0) + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: totalStories,
|
||||
by_grade: Object.entries(gradeStats).map(([grade, count]) => ({ grade, count })),
|
||||
by_tag: Object.entries(tagStats).map(([tag, count]) => ({ tag, count }))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching story stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch story statistics',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GET GUIDE: Comprehensive guide for AI to create stories
|
||||
*/
|
||||
exports.getStoryGuide = async (req, res) => {
|
||||
try {
|
||||
const guide = {
|
||||
guide_version: "1.0.0",
|
||||
last_updated: new Date().toISOString(),
|
||||
|
||||
data_structure: {
|
||||
required_fields: {
|
||||
name: {
|
||||
type: "string(255)",
|
||||
example: "The Greedy Cat",
|
||||
description: "Story title/name"
|
||||
}
|
||||
},
|
||||
|
||||
optional_fields: {
|
||||
logo: {
|
||||
type: "text (URL)",
|
||||
example: "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
|
||||
description: "URL to story logo/thumbnail image"
|
||||
},
|
||||
vocabulary: {
|
||||
type: "JSON array",
|
||||
example: ["cat", "eat", "apple", "happy"],
|
||||
description: "Array of vocabulary words used in the story"
|
||||
},
|
||||
context: {
|
||||
type: "JSON array of objects",
|
||||
description: "Array of story context objects with images, text, audio data",
|
||||
item_structure: {
|
||||
image: "URL to context image",
|
||||
text: "Story text for this context",
|
||||
audio: "URL to audio narration",
|
||||
order: "Sequence number (integer)"
|
||||
},
|
||||
example: [
|
||||
{
|
||||
image: "https://cdn.sena.tech/story/scene1.jpg",
|
||||
text: "Once upon a time, there was a greedy cat.",
|
||||
audio: "https://cdn.sena.tech/audio/scene1.mp3",
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
image: "https://cdn.sena.tech/story/scene2.jpg",
|
||||
text: "The cat loved eating apples.",
|
||||
audio: "https://cdn.sena.tech/audio/scene2.mp3",
|
||||
order: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
grade: {
|
||||
type: "JSON array",
|
||||
example: ["Grade 1", "Grade 2", "Grade 3"],
|
||||
description: "Array of grade levels this story is suitable for"
|
||||
},
|
||||
tag: {
|
||||
type: "JSON array",
|
||||
example: ["adventure", "friendship", "animals", "food"],
|
||||
description: "Array of tags for categorization and filtering"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
context_structure: {
|
||||
description: "Each context object represents a scene/page in the story",
|
||||
required_properties: {
|
||||
text: "Story text content (required)",
|
||||
order: "Sequence number starting from 1 (required)"
|
||||
},
|
||||
optional_properties: {
|
||||
image: "URL to scene image",
|
||||
audio: "URL to audio narration"
|
||||
},
|
||||
best_practices: [
|
||||
"Keep text concise and age-appropriate",
|
||||
"Use order numbers sequentially (1, 2, 3, ...)",
|
||||
"Provide audio for better engagement",
|
||||
"Images should be high quality and relevant"
|
||||
]
|
||||
},
|
||||
|
||||
vocabulary_guidelines: {
|
||||
description: "Link vocabulary words to existing vocab entries",
|
||||
format: "Array of strings",
|
||||
tips: [
|
||||
"Use vocab_code from Vocab table for better tracking",
|
||||
"Include only words that appear in the story",
|
||||
"Order by frequency or importance",
|
||||
"Can reference vocab IDs or codes"
|
||||
],
|
||||
example: ["vocab-001-cat", "vocab-002-eat", "vocab-015-apple"]
|
||||
},
|
||||
|
||||
grade_levels: {
|
||||
description: "Supported grade levels",
|
||||
options: [
|
||||
"Pre-K",
|
||||
"Kindergarten",
|
||||
"Grade 1",
|
||||
"Grade 2",
|
||||
"Grade 3",
|
||||
"Grade 4",
|
||||
"Grade 5",
|
||||
"Grade 6"
|
||||
],
|
||||
tips: [
|
||||
"Can assign multiple grades",
|
||||
"Consider vocabulary difficulty",
|
||||
"Match with curriculum standards"
|
||||
]
|
||||
},
|
||||
|
||||
tag_categories: {
|
||||
description: "Common tag categories for story classification",
|
||||
categories: {
|
||||
themes: ["adventure", "friendship", "family", "courage", "honesty", "kindness"],
|
||||
subjects: ["animals", "nature", "food", "school", "home", "travel"],
|
||||
skills: ["reading", "listening", "vocabulary", "grammar", "phonics"],
|
||||
emotions: ["happy", "sad", "excited", "scared", "surprised"],
|
||||
genres: ["fiction", "non-fiction", "fantasy", "realistic", "fable"]
|
||||
},
|
||||
tips: [
|
||||
"Use 3-7 tags per story",
|
||||
"Mix different category types",
|
||||
"Keep tags consistent across stories",
|
||||
"Use lowercase for consistency"
|
||||
]
|
||||
},
|
||||
|
||||
examples: {
|
||||
complete_story: {
|
||||
name: "The Greedy Cat",
|
||||
logo: "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
|
||||
vocabulary: ["cat", "eat", "apple", "happy", "greedy"],
|
||||
context: [
|
||||
{
|
||||
image: "https://cdn.sena.tech/story/gc-scene1.jpg",
|
||||
text: "Once upon a time, there was a greedy cat.",
|
||||
audio: "https://cdn.sena.tech/audio/gc-scene1.mp3",
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
image: "https://cdn.sena.tech/story/gc-scene2.jpg",
|
||||
text: "The cat loved eating apples every day.",
|
||||
audio: "https://cdn.sena.tech/audio/gc-scene2.mp3",
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
image: "https://cdn.sena.tech/story/gc-scene3.jpg",
|
||||
text: "One day, the cat ate too many apples and felt sick.",
|
||||
audio: "https://cdn.sena.tech/audio/gc-scene3.mp3",
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
image: "https://cdn.sena.tech/story/gc-scene4.jpg",
|
||||
text: "The cat learned to eat just enough and was happy again.",
|
||||
audio: "https://cdn.sena.tech/audio/gc-scene4.mp3",
|
||||
order: 4
|
||||
}
|
||||
],
|
||||
grade: ["Grade 1", "Grade 2"],
|
||||
tag: ["animals", "food", "lesson", "health", "fiction"]
|
||||
},
|
||||
|
||||
minimal_story: {
|
||||
name: "My Pet Dog",
|
||||
context: [
|
||||
{
|
||||
text: "I have a pet dog.",
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
text: "My dog is brown.",
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
text: "I love my dog.",
|
||||
order: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
validation_checklist: [
|
||||
"✓ name is provided and descriptive",
|
||||
"✓ context array has at least 1 scene",
|
||||
"✓ Each context has text and order",
|
||||
"✓ order numbers are sequential (1, 2, 3...)",
|
||||
"✓ vocabulary words match story content",
|
||||
"✓ grade levels are appropriate for content",
|
||||
"✓ tags are relevant and descriptive",
|
||||
"✓ URLs are accessible (logo, images, audio)",
|
||||
"✓ Text is age-appropriate",
|
||||
"✓ Story has clear beginning, middle, end"
|
||||
],
|
||||
|
||||
common_mistakes: [
|
||||
{
|
||||
mistake: "Missing context order",
|
||||
example: { context: [{ text: "Some text" }] },
|
||||
fix: { context: [{ text: "Some text", order: 1 }] },
|
||||
explanation: "Every context must have an order number"
|
||||
},
|
||||
{
|
||||
mistake: "Non-sequential order numbers",
|
||||
example: { context: [{ order: 1 }, { order: 3 }, { order: 2 }] },
|
||||
fix: { context: [{ order: 1 }, { order: 2 }, { order: 3 }] },
|
||||
explanation: "Order should be sequential for proper story flow"
|
||||
},
|
||||
{
|
||||
mistake: "Invalid grade format",
|
||||
example: { grade: "Grade 1" },
|
||||
fix: { grade: ["Grade 1"] },
|
||||
explanation: "grade must be an array"
|
||||
},
|
||||
{
|
||||
mistake: "Mismatched vocabulary",
|
||||
example: { name: "The Cat", vocabulary: ["dog", "bird"] },
|
||||
fix: { name: "The Cat", vocabulary: ["cat"] },
|
||||
explanation: "Vocabulary should match words in the story"
|
||||
},
|
||||
{
|
||||
mistake: "Missing story name",
|
||||
example: { context: [/* context items */] },
|
||||
fix: { name: "My Story", context: [/* context items */] },
|
||||
explanation: "name is required"
|
||||
}
|
||||
],
|
||||
|
||||
ai_tips: {
|
||||
content_creation: "Generate age-appropriate, engaging stories with clear moral lessons",
|
||||
vocabulary_integration: "Reference existing vocabulary entries from Vocab table",
|
||||
multimedia: "Always provide audio URLs for better learning engagement",
|
||||
sequencing: "Ensure context order is logical and sequential",
|
||||
testing: "Test story flow by reading context in order",
|
||||
consistency: "Use consistent naming conventions for URLs and vocabulary"
|
||||
},
|
||||
|
||||
api_usage: {
|
||||
create: "POST /api/stories",
|
||||
get_all: "GET /api/stories?page=1&limit=20&grade_filter=Grade 1&tag_filter=animals",
|
||||
get_one: "GET /api/stories/:id",
|
||||
update: "PUT /api/stories/:id",
|
||||
delete: "DELETE /api/stories/:id",
|
||||
by_grade: "GET /api/stories/grade?grade=Grade 1",
|
||||
by_tag: "GET /api/stories/tag?tag=adventure",
|
||||
stats: "GET /api/stories/stats",
|
||||
guide: "GET /api/stories/guide"
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: guide
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating story guide:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to generate story guide',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user