feat: introduce Joi validation schemas and integrate them across various controllers for categories, lessons, courses, chapters, announcements, and admin course approvals.
This commit is contained in:
parent
c5aa195b13
commit
b56f604890
14 changed files with 553 additions and 28 deletions
30
Backend/src/validators/AdminCourseApproval.validator.ts
Normal file
30
Backend/src/validators/AdminCourseApproval.validator.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* Validator for approving a course
|
||||
* Comment is optional
|
||||
*/
|
||||
export const ApproveCourseValidator = Joi.object({
|
||||
comment: Joi.string()
|
||||
.max(1000)
|
||||
.optional()
|
||||
.messages({
|
||||
'string.max': 'Comment must not exceed 1000 characters'
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for rejecting a course
|
||||
* Comment is required when rejecting
|
||||
*/
|
||||
export const RejectCourseValidator = Joi.object({
|
||||
comment: Joi.string()
|
||||
.min(10)
|
||||
.max(1000)
|
||||
.required()
|
||||
.messages({
|
||||
'string.min': 'Comment must be at least 10 characters when rejecting a course',
|
||||
'string.max': 'Comment must not exceed 1000 characters',
|
||||
'any.required': 'Comment is required when rejecting a course'
|
||||
})
|
||||
});
|
||||
186
Backend/src/validators/ChaptersLesson.validator.ts
Normal file
186
Backend/src/validators/ChaptersLesson.validator.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
// Multi-language validation schema
|
||||
const multiLangSchema = Joi.object({
|
||||
th: Joi.string().required().messages({
|
||||
'any.required': 'Thai text is required'
|
||||
}),
|
||||
en: Joi.string().required().messages({
|
||||
'any.required': 'English text is required'
|
||||
})
|
||||
}).required();
|
||||
|
||||
const multiLangOptionalSchema = Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional()
|
||||
}).optional();
|
||||
|
||||
// ============================================
|
||||
// Chapter Validators
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Validator for creating a chapter
|
||||
*/
|
||||
export const CreateChapterValidator = Joi.object({
|
||||
title: multiLangSchema.messages({
|
||||
'any.required': 'Title is required'
|
||||
}),
|
||||
description: multiLangOptionalSchema,
|
||||
sort_order: Joi.number().integer().min(0).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for updating a chapter
|
||||
*/
|
||||
export const UpdateChapterValidator = Joi.object({
|
||||
title: multiLangOptionalSchema,
|
||||
description: multiLangOptionalSchema,
|
||||
sort_order: Joi.number().integer().min(0).optional(),
|
||||
is_published: Joi.boolean().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for reordering a chapter
|
||||
*/
|
||||
export const ReorderChapterValidator = Joi.object({
|
||||
sort_order: Joi.number().integer().min(0).required().messages({
|
||||
'any.required': 'Sort order is required',
|
||||
'number.min': 'Sort order must be at least 0'
|
||||
})
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Lesson Validators
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Validator for creating a lesson
|
||||
*/
|
||||
export const CreateLessonValidator = Joi.object({
|
||||
title: multiLangSchema.messages({
|
||||
'any.required': 'Title is required'
|
||||
}),
|
||||
content: multiLangOptionalSchema,
|
||||
type: Joi.string().valid('VIDEO', 'QUIZ').required().messages({
|
||||
'any.only': 'Type must be either VIDEO or QUIZ',
|
||||
'any.required': 'Type is required'
|
||||
}),
|
||||
sort_order: Joi.number().integer().min(0).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for updating a lesson
|
||||
*/
|
||||
export const UpdateLessonValidator = Joi.object({
|
||||
title: multiLangOptionalSchema,
|
||||
content: multiLangOptionalSchema,
|
||||
duration_minutes: Joi.number().min(0).optional().messages({
|
||||
'number.min': 'Duration must be at least 0'
|
||||
}),
|
||||
sort_order: Joi.number().integer().min(0).optional(),
|
||||
prerequisite_lesson_ids: Joi.array().items(Joi.number().integer().positive()).optional(),
|
||||
is_published: Joi.boolean().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for reordering lessons
|
||||
*/
|
||||
export const ReorderLessonsValidator = Joi.object({
|
||||
lesson_id: Joi.number().integer().positive().required().messages({
|
||||
'any.required': 'Lesson ID is required'
|
||||
}),
|
||||
sort_order: Joi.number().integer().min(0).required().messages({
|
||||
'any.required': 'Sort order is required'
|
||||
})
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Quiz Question Validators
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Validator for quiz choice
|
||||
*/
|
||||
const QuizChoiceValidator = Joi.object({
|
||||
text: multiLangSchema.messages({
|
||||
'any.required': 'Choice text is required'
|
||||
}),
|
||||
is_correct: Joi.boolean().required().messages({
|
||||
'any.required': 'is_correct is required'
|
||||
}),
|
||||
sort_order: Joi.number().integer().min(0).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for adding a question to a quiz
|
||||
*/
|
||||
export const AddQuestionValidator = Joi.object({
|
||||
question: multiLangSchema.messages({
|
||||
'any.required': 'Question is required'
|
||||
}),
|
||||
explanation: multiLangOptionalSchema,
|
||||
question_type: Joi.string()
|
||||
.valid('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER')
|
||||
.required()
|
||||
.messages({
|
||||
'any.only': 'Question type must be MULTIPLE_CHOICE, TRUE_FALSE, or SHORT_ANSWER',
|
||||
'any.required': 'Question type is required'
|
||||
}),
|
||||
sort_order: Joi.number().integer().min(0).optional(),
|
||||
choices: Joi.array().items(QuizChoiceValidator).min(1).optional().messages({
|
||||
'array.min': 'At least one choice is required for multiple choice questions'
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for updating a question
|
||||
*/
|
||||
export const UpdateQuestionValidator = Joi.object({
|
||||
question: multiLangOptionalSchema,
|
||||
explanation: multiLangOptionalSchema,
|
||||
question_type: Joi.string()
|
||||
.valid('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER')
|
||||
.optional()
|
||||
.messages({
|
||||
'any.only': 'Question type must be MULTIPLE_CHOICE, TRUE_FALSE, or SHORT_ANSWER'
|
||||
}),
|
||||
sort_order: Joi.number().integer().min(0).optional(),
|
||||
choices: Joi.array().items(QuizChoiceValidator).min(1).optional().messages({
|
||||
'array.min': 'At least one choice is required'
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for reordering a question
|
||||
*/
|
||||
export const ReorderQuestionValidator = Joi.object({
|
||||
sort_order: Joi.number().integer().min(0).required().messages({
|
||||
'any.required': 'Sort order is required',
|
||||
'number.min': 'Sort order must be at least 0'
|
||||
})
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Quiz Settings Validator
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Validator for updating quiz settings
|
||||
*/
|
||||
export const UpdateQuizValidator = Joi.object({
|
||||
title: multiLangOptionalSchema,
|
||||
description: multiLangOptionalSchema,
|
||||
passing_score: Joi.number().min(0).max(100).optional().messages({
|
||||
'number.min': 'Passing score must be at least 0',
|
||||
'number.max': 'Passing score must not exceed 100'
|
||||
}),
|
||||
time_limit: Joi.number().min(0).optional().messages({
|
||||
'number.min': 'Time limit must be at least 0'
|
||||
}),
|
||||
shuffle_questions: Joi.boolean().optional(),
|
||||
shuffle_choices: Joi.boolean().optional(),
|
||||
show_answers_after_completion: Joi.boolean().optional(),
|
||||
is_skippable: Joi.boolean().optional(),
|
||||
allow_multiple_attempts: Joi.boolean().optional()
|
||||
});
|
||||
|
|
@ -20,3 +20,38 @@ export const CreateCourseValidator = Joi.object({
|
|||
is_free: Joi.boolean().required(),
|
||||
have_certificate: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for updating a course
|
||||
*/
|
||||
export const UpdateCourseValidator = Joi.object({
|
||||
category_id: Joi.number().optional(),
|
||||
title: Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional(),
|
||||
}).optional(),
|
||||
slug: Joi.string().optional(),
|
||||
description: Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional(),
|
||||
}).optional(),
|
||||
price: Joi.number().optional(),
|
||||
is_free: Joi.boolean().optional(),
|
||||
have_certificate: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for cloning a course
|
||||
*/
|
||||
export const CloneCourseValidator = Joi.object({
|
||||
title: Joi.object({
|
||||
th: Joi.string().required().messages({
|
||||
'any.required': 'Thai title is required'
|
||||
}),
|
||||
en: Joi.string().required().messages({
|
||||
'any.required': 'English title is required'
|
||||
})
|
||||
}).required().messages({
|
||||
'any.required': 'Title is required'
|
||||
})
|
||||
});
|
||||
|
|
|
|||
38
Backend/src/validators/CoursesStudent.validator.ts
Normal file
38
Backend/src/validators/CoursesStudent.validator.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* Validator for saving video progress
|
||||
*/
|
||||
export const SaveVideoProgressValidator = Joi.object({
|
||||
video_progress_seconds: Joi.number().min(0).required().messages({
|
||||
'any.required': 'Video progress seconds is required',
|
||||
'number.min': 'Video progress must be at least 0'
|
||||
}),
|
||||
video_duration_seconds: Joi.number().min(0).optional().messages({
|
||||
'number.min': 'Video duration must be at least 0'
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for quiz answer
|
||||
*/
|
||||
const QuizAnswerValidator = Joi.object({
|
||||
question_id: Joi.number().integer().positive().required().messages({
|
||||
'any.required': 'Question ID is required',
|
||||
'number.positive': 'Question ID must be positive'
|
||||
}),
|
||||
choice_id: Joi.number().integer().positive().required().messages({
|
||||
'any.required': 'Choice ID is required',
|
||||
'number.positive': 'Choice ID must be positive'
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for submitting quiz answers
|
||||
*/
|
||||
export const SubmitQuizValidator = Joi.object({
|
||||
answers: Joi.array().items(QuizAnswerValidator).min(1).required().messages({
|
||||
'any.required': 'Answers are required',
|
||||
'array.min': 'At least one answer is required'
|
||||
})
|
||||
});
|
||||
15
Backend/src/validators/Lessons.validator.ts
Normal file
15
Backend/src/validators/Lessons.validator.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* Validator for setting YouTube video
|
||||
*/
|
||||
export const SetYouTubeVideoValidator = Joi.object({
|
||||
youtube_video_id: Joi.string().required().messages({
|
||||
'any.required': 'YouTube video ID is required',
|
||||
'string.empty': 'YouTube video ID cannot be empty'
|
||||
}),
|
||||
video_title: Joi.string().required().messages({
|
||||
'any.required': 'Video title is required',
|
||||
'string.empty': 'Video title cannot be empty'
|
||||
})
|
||||
});
|
||||
72
Backend/src/validators/announcements.validator.ts
Normal file
72
Backend/src/validators/announcements.validator.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* Validator for creating an announcement
|
||||
*/
|
||||
export const CreateAnnouncementValidator = Joi.object({
|
||||
title: Joi.object({
|
||||
th: Joi.string().required().messages({
|
||||
'any.required': 'Thai title is required'
|
||||
}),
|
||||
en: Joi.string().required().messages({
|
||||
'any.required': 'English title is required'
|
||||
})
|
||||
}).required().messages({
|
||||
'any.required': 'Title is required'
|
||||
}),
|
||||
content: Joi.object({
|
||||
th: Joi.string().required().messages({
|
||||
'any.required': 'Thai content is required'
|
||||
}),
|
||||
en: Joi.string().required().messages({
|
||||
'any.required': 'English content is required'
|
||||
})
|
||||
}).required().messages({
|
||||
'any.required': 'Content is required'
|
||||
}),
|
||||
status: Joi.string()
|
||||
.valid('DRAFT', 'PUBLISHED', 'ARCHIVED')
|
||||
.required()
|
||||
.messages({
|
||||
'any.only': 'Status must be one of: DRAFT, PUBLISHED, ARCHIVED',
|
||||
'any.required': 'Status is required'
|
||||
}),
|
||||
is_pinned: Joi.boolean()
|
||||
.required()
|
||||
.messages({
|
||||
'any.required': 'is_pinned is required'
|
||||
}),
|
||||
published_at: Joi.string()
|
||||
.isoDate()
|
||||
.optional()
|
||||
.messages({
|
||||
'string.isoDate': 'published_at must be a valid ISO date string'
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for updating an announcement
|
||||
*/
|
||||
export const UpdateAnnouncementValidator = Joi.object({
|
||||
title: Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional()
|
||||
}).optional(),
|
||||
content: Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional()
|
||||
}).optional(),
|
||||
status: Joi.string()
|
||||
.valid('DRAFT', 'PUBLISHED', 'ARCHIVED')
|
||||
.optional()
|
||||
.messages({
|
||||
'any.only': 'Status must be one of: DRAFT, PUBLISHED, ARCHIVED'
|
||||
}),
|
||||
is_pinned: Joi.boolean().optional(),
|
||||
published_at: Joi.string()
|
||||
.isoDate()
|
||||
.optional()
|
||||
.messages({
|
||||
'string.isoDate': 'published_at must be a valid ISO date string'
|
||||
})
|
||||
});
|
||||
58
Backend/src/validators/categories.validator.ts
Normal file
58
Backend/src/validators/categories.validator.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* Validator for creating a category
|
||||
*/
|
||||
export const CreateCategoryValidator = Joi.object({
|
||||
name: Joi.object({
|
||||
th: Joi.string().required().messages({
|
||||
'any.required': 'Thai name is required'
|
||||
}),
|
||||
en: Joi.string().required().messages({
|
||||
'any.required': 'English name is required'
|
||||
})
|
||||
}).required().messages({
|
||||
'any.required': 'Name is required'
|
||||
}),
|
||||
slug: Joi.string()
|
||||
.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
||||
.required()
|
||||
.messages({
|
||||
'string.pattern.base': 'Slug must be lowercase with hyphens (e.g., web-development)',
|
||||
'any.required': 'Slug is required'
|
||||
}),
|
||||
description: Joi.object({
|
||||
th: Joi.string().required().messages({
|
||||
'any.required': 'Thai description is required'
|
||||
}),
|
||||
en: Joi.string().required().messages({
|
||||
'any.required': 'English description is required'
|
||||
})
|
||||
}).required().messages({
|
||||
'any.required': 'Description is required'
|
||||
}),
|
||||
created_by: Joi.number().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for updating a category
|
||||
*/
|
||||
export const UpdateCategoryValidator = Joi.object({
|
||||
id: Joi.number().required().messages({
|
||||
'any.required': 'Category ID is required'
|
||||
}),
|
||||
name: Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional()
|
||||
}).optional(),
|
||||
slug: Joi.string()
|
||||
.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
||||
.optional()
|
||||
.messages({
|
||||
'string.pattern.base': 'Slug must be lowercase with hyphens (e.g., web-development)'
|
||||
}),
|
||||
description: Joi.object({
|
||||
th: Joi.string().optional(),
|
||||
en: Joi.string().optional()
|
||||
}).optional()
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue