This commit is contained in:
silverpro89
2026-01-26 20:23:08 +07:00
parent 53d97ba5db
commit 2c7b4675a7
49 changed files with 12668 additions and 1 deletions

84
models/Grammar.js Normal file
View File

@@ -0,0 +1,84 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Grammar = sequelize.define('Grammar', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
grammar_code: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
comment: 'Unique identifier for grammar rule (e.g., "gram-001-present-cont")'
},
title: {
type: DataTypes.STRING(200),
allowNull: false,
comment: 'Grammar rule name (e.g., "Present Continuous")'
},
translation: {
type: DataTypes.STRING(200),
allowNull: true,
comment: 'Vietnamese translation'
},
// GENERATIVE LOGIC
structure: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: {},
comment: 'Formula and pattern_logic for sentence generation. Structure: { formula: string, pattern_logic: array }'
},
// METADATA
instructions: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: {},
comment: 'Instructions and hints. Structure: { vi: string, hint: string }'
},
// ATTRIBUTES
difficulty_score: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1,
validate: {
min: 1,
max: 10
},
comment: 'Difficulty level from 1 (easiest) to 10 (hardest)'
},
category: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'Grammar category (e.g., "Tenses", "Modal Verbs", "Questions")'
},
tags: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of tags for categorization'
},
// STATUS
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Soft delete flag'
}
}, {
tableName: 'grammars',
timestamps: true,
indexes: [
{ fields: ['grammar_code'], unique: true },
{ fields: ['category'] },
{ fields: ['difficulty_score'] },
{ fields: ['is_active'] }
]
});
module.exports = Grammar;

56
models/GrammarMapping.js Normal file
View File

@@ -0,0 +1,56 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const GrammarMapping = sequelize.define('GrammarMapping', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
grammar_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'grammars',
key: 'id'
},
onDelete: 'CASCADE',
comment: 'Foreign key to grammars table'
},
book_id: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Book identifier (e.g., "global-success-2")'
},
grade: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Grade level'
},
unit: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Unit number in the book'
},
lesson: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Lesson number within the unit'
},
context_note: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Additional context about where this grammar appears'
}
}, {
tableName: 'grammar_mappings',
timestamps: true,
indexes: [
{ fields: ['grammar_id'] },
{ fields: ['book_id'] },
{ fields: ['grade'] },
{ fields: ['book_id', 'grade', 'unit', 'lesson'] }
]
});
module.exports = GrammarMapping;

View File

@@ -0,0 +1,71 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const GrammarMediaStory = sequelize.define('GrammarMediaStory', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
grammar_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'grammars',
key: 'id'
},
onDelete: 'CASCADE',
comment: 'Foreign key to grammars table'
},
story_id: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Unique story identifier (e.g., "st-01")'
},
title: {
type: DataTypes.STRING(200),
allowNull: false,
comment: 'Story title'
},
type: {
type: DataTypes.ENUM('story', 'video', 'animation', 'audio'),
allowNull: false,
defaultValue: 'story',
comment: 'Media type'
},
url: {
type: DataTypes.STRING(500),
allowNull: false,
comment: 'Single URL to the complete media file'
},
thumbnail: {
type: DataTypes.STRING(500),
allowNull: true,
comment: 'Thumbnail image URL'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Story description'
},
duration_seconds: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Media duration in seconds'
},
min_grade: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Minimum grade level for this story'
}
}, {
tableName: 'grammar_media_stories',
timestamps: true,
indexes: [
{ fields: ['grammar_id'] },
{ fields: ['story_id'] },
{ fields: ['type'] }
]
});
module.exports = GrammarMediaStory;

View File

@@ -37,9 +37,21 @@ const Lesson = sequelize.define('lessons', {
},
// Dạng 1: JSON Content - Nội dung học tập dạng JSON
// Cấu trúc content_json phụ thuộc vào lesson_content_type:
// - vocabulary: { type: "vocabulary", vocabulary_ids: [uuid1, uuid2, ...], exercises: [...] }
// - grammar: { type: "grammar", grammar_ids: [uuid1, uuid2, ...], examples: [...], exercises: [...] }
// - phonics: { type: "phonics", phonics_rules: [{ipa: "/æ/", words: [...]}], exercises: [...] }
// - review: { type: "review", sections: [{type: "vocabulary", ...}, {type: "grammar", ...}, {type: "phonics", ...}] }
content_json: {
type: DataTypes.JSON,
comment: 'Nội dung học tập dạng JSON: text, quiz, interactive, assignment, etc.'
comment: 'Nội dung học tập dạng JSON: vocabulary, grammar, phonics, review'
},
// Loại nội dung của bài học (để query dễ dàng)
lesson_content_type: {
type: DataTypes.ENUM('vocabulary', 'grammar', 'phonics', 'review', 'mixed'),
allowNull: true,
comment: 'Loại nội dung: vocabulary, grammar, phonics, review, mixed'
},
// Dạng 2: URL Content - Chứa link external

81
models/Story.js Normal file
View File

@@ -0,0 +1,81 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
/**
* Story Model
* Table: stories
* Stores story data with metadata, vocabulary, context, grades, and tags
*/
const Story = sequelize.define('stories', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
comment: 'Unique identifier for the story (UUID)'
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
comment: 'Story title/name'
},
logo: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'URL to story logo/thumbnail image'
},
vocabulary: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of vocabulary words used in the story'
},
context: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of story context objects with images, text, audio data'
},
grade: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of grade levels (e.g., ["Grade 1", "Grade 2"])'
},
tag: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of tags for categorization (e.g., ["adventure", "friendship"])'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: 'Record creation timestamp'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: 'Record last update timestamp'
}
}, {
tableName: 'stories',
timestamps: true,
underscored: true,
indexes: [
{
name: 'idx_name',
fields: ['name']
},
{
name: 'idx_created_at',
fields: ['created_at']
}
],
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci'
});
module.exports = Story;

90
models/Vocab.js Normal file
View File

@@ -0,0 +1,90 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Vocab = sequelize.define('Vocab', {
vocab_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for vocabulary entry'
},
vocab_code: {
type: DataTypes.STRING(50),
unique: true,
allowNull: false,
comment: 'Unique code for vocabulary (e.g., vocab-001-eat)'
},
base_word: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Base form of the word'
},
translation: {
type: DataTypes.STRING(200),
allowNull: false,
comment: 'Vietnamese translation'
},
difficulty_score: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Difficulty level (1-10)'
},
category: {
type: DataTypes.STRING(100),
comment: 'Category of the word (e.g., Action Verbs, Nouns)'
},
images: {
type: DataTypes.JSON,
comment: 'Array of image URLs'
},
tags: {
type: DataTypes.JSON,
comment: 'Array of tags for categorization'
},
syntax: {
type: DataTypes.JSON,
comment: 'Syntax roles for Grammar Engine (is_subject, is_verb, is_object, is_be, is_adj, is_adv, is_article, verb_type, article_type, adv_type, position, priority, etc.)'
},
semantics: {
type: DataTypes.JSON,
comment: 'Semantic constraints (can_be_subject_type, can_take_object_type, can_modify, cannot_modify, word_type, is_countable, person_type, etc.)'
},
constraints: {
type: DataTypes.JSON,
comment: 'Grammar constraints (followed_by, match_subject, match_with, phonetic_rules, etc.)'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether this vocab entry is active'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
name: 'idx_vocab_code',
fields: ['vocab_code']
},
{
name: 'idx_base_word',
fields: ['base_word']
},
{
name: 'idx_category',
fields: ['category']
}
]
});
module.exports = Vocab;

78
models/VocabForm.js Normal file
View File

@@ -0,0 +1,78 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const VocabForm = sequelize.define('VocabForm', {
form_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for word form'
},
vocab_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'CASCADE',
comment: 'Reference to vocabulary entry'
},
form_key: {
type: DataTypes.STRING(50),
allowNull: false,
comment: 'Form identifier (v1, v2, v3, v_s_es, v_ing, etc.)'
},
text: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'The actual word form (e.g., eat, eats, eating, ate)'
},
phonetic: {
type: DataTypes.STRING(100),
comment: 'IPA phonetic transcription (e.g., /iːt/)'
},
audio_url: {
type: DataTypes.STRING(500),
comment: 'URL to audio pronunciation file'
},
min_grade: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Minimum grade level to unlock this form'
},
description: {
type: DataTypes.TEXT,
comment: 'Description or usage note for this form'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab_form',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
name: 'idx_vocab_form_vocab',
fields: ['vocab_id']
},
{
name: 'idx_vocab_form_key',
fields: ['form_key']
},
{
name: 'idx_vocab_form_unique',
unique: true,
fields: ['vocab_id', 'form_key']
}
]
});
module.exports = VocabForm;

72
models/VocabMapping.js Normal file
View File

@@ -0,0 +1,72 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const VocabMapping = sequelize.define('VocabMapping', {
mapping_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for curriculum mapping'
},
vocab_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'CASCADE',
comment: 'Reference to vocabulary entry'
},
book_id: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Book/curriculum identifier (e.g., global-success-1)'
},
grade: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Grade level'
},
unit: {
type: DataTypes.INTEGER,
comment: 'Unit number in the book'
},
lesson: {
type: DataTypes.INTEGER,
comment: 'Lesson number in the unit'
},
form_key: {
type: DataTypes.STRING(50),
comment: 'Which form to use (v1, v_s_es, v_ing, v2, v3, etc.)'
},
context_note: {
type: DataTypes.TEXT,
comment: 'Additional context for this mapping'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab_mapping',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
name: 'idx_vocab_mapping_vocab',
fields: ['vocab_id']
},
{
name: 'idx_vocab_mapping_book_grade',
fields: ['book_id', 'grade']
},
{
name: 'idx_vocab_mapping_unit_lesson',
fields: ['unit', 'lesson']
}
]
});
module.exports = VocabMapping;

65
models/VocabRelation.js Normal file
View File

@@ -0,0 +1,65 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const VocabRelation = sequelize.define('VocabRelation', {
relation_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for vocabulary relation'
},
vocab_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'CASCADE',
comment: 'Reference to vocabulary entry'
},
relation_type: {
type: DataTypes.ENUM('synonym', 'antonym', 'related'),
allowNull: false,
comment: 'Type of relation (synonym, antonym, related)'
},
related_word: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'The related word (e.g., consume, dine, fast)'
},
related_vocab_id: {
type: DataTypes.UUID,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'SET NULL',
comment: 'Reference to related vocab entry if it exists in system'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab_relation',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
name: 'idx_vocab_relation_vocab',
fields: ['vocab_id']
},
{
name: 'idx_vocab_relation_type',
fields: ['relation_type']
},
{
name: 'idx_vocab_relation_related',
fields: ['related_vocab_id']
}
]
});
module.exports = VocabRelation;

View File

@@ -32,6 +32,20 @@ const Game = require('./Game');
const LessonComponentProgress = require('./LessonComponentProgress');
const LessonLeaderboard = require('./LessonLeaderboard');
// Group 3.2: Vocabulary System (NEW)
const Vocab = require('./Vocab');
const VocabMapping = require('./VocabMapping');
const VocabForm = require('./VocabForm');
const VocabRelation = require('./VocabRelation');
// Group 3.3: Grammar System (NEW)
const Grammar = require('./Grammar');
const GrammarMapping = require('./GrammarMapping');
const GrammarMediaStory = require('./GrammarMediaStory');
// Group 3.4: Story System (NEW)
const Story = require('./Story');
// Group 4: Attendance
const AttendanceLog = require('./AttendanceLog');
const AttendanceDaily = require('./AttendanceDaily');
@@ -148,6 +162,29 @@ const setupRelationships = () => {
LessonLeaderboard.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
Lesson.hasMany(LessonLeaderboard, { foreignKey: 'lesson_id', as: 'leaderboard' });
// Vocabulary relationships (NEW)
// Vocab -> VocabMapping (1:N)
Vocab.hasMany(VocabMapping, { foreignKey: 'vocab_id', as: 'mappings' });
VocabMapping.belongsTo(Vocab, { foreignKey: 'vocab_id', as: 'vocab' });
// Vocab -> VocabForm (1:N)
Vocab.hasMany(VocabForm, { foreignKey: 'vocab_id', as: 'forms' });
VocabForm.belongsTo(Vocab, { foreignKey: 'vocab_id', as: 'vocab' });
// Vocab -> VocabRelation (1:N)
Vocab.hasMany(VocabRelation, { foreignKey: 'vocab_id', as: 'relations' });
VocabRelation.belongsTo(Vocab, { foreignKey: 'vocab_id', as: 'vocab' });
VocabRelation.belongsTo(Vocab, { foreignKey: 'related_vocab_id', as: 'relatedVocab' });
// Grammar relationships (NEW)
// Grammar -> GrammarMapping (1:N)
Grammar.hasMany(GrammarMapping, { foreignKey: 'grammar_id', as: 'mappings' });
GrammarMapping.belongsTo(Grammar, { foreignKey: 'grammar_id', as: 'grammar' });
// Grammar -> GrammarMediaStory (1:N)
Grammar.hasMany(GrammarMediaStory, { foreignKey: 'grammar_id', as: 'mediaStories' });
GrammarMediaStory.belongsTo(Grammar, { foreignKey: 'grammar_id', as: 'grammar' });
// Attendance relationships
AttendanceLog.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
AttendanceLog.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
@@ -258,6 +295,20 @@ module.exports = {
LessonComponentProgress,
LessonLeaderboard,
// Group 3.2: Vocabulary System (NEW)
Vocab,
VocabMapping,
VocabForm,
VocabRelation,
// Group 3.3: Grammar System (NEW)
Grammar,
GrammarMapping,
GrammarMediaStory,
// Group 3.4: Story System (NEW)
Story,
// Group 4: Attendance
AttendanceLog,
AttendanceDaily,