mcp-quiz-server
Version:
🧠AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.
310 lines (309 loc) • 10.4 kB
JavaScript
"use strict";
/**
* @fileoverview Question Domain Entity - Pure Business Logic
* @version 1.0.0
* @since 2025-07-29
* @lastUpdated 2025-07-29
* @module Question Domain Entity
* @description Pure domain entity representing a Quiz Question in the business domain,
* free from framework dependencies and infrastructure concerns.
* Contains only business rules and domain logic.
* @contributors Claude Code Agent
* @dependencies None (framework-agnostic)
* @requirements REQ-ARCH-001 (Clean Architecture Domain Layer)
* @testCoverage Unit tests for business rules and validation logic
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Question = void 0;
const QuestionId_1 = require("../value-objects/QuestionId");
const QuestionAnsweredEvent_1 = require("../events/QuestionAnsweredEvent");
/**
* Question Domain Entity
*
* @description Represents a quiz question in the business domain with all business
* rules and invariants. This is a pure domain entity without any
* framework dependencies.
*
* @example
* ```typescript
* const question = Question.createMultipleChoice({
* questionText: "What is TypeScript?",
* options: ["A language", "A framework", "A tool"],
* correctAnswer: "A language",
* order: 1
* });
* ```
*
* @since 2025-07-29
* @author Claude Code Agent
* @requirements REQ-ARCH-001 (Clean Architecture Domain Layer)
*/
class Question {
constructor(props) {
this.props = props;
this._domainEvents = [];
this.validate();
}
/**
* Create a multiple choice question
*/
static createMultipleChoice(data) {
const now = new Date();
const questionId = QuestionId_1.QuestionId.generate();
return new Question({
id: questionId,
questionText: data.questionText,
options: data.options,
correctAnswer: data.correctAnswer,
explanation: data.explanation,
order: data.order,
type: 'multiple_choice',
points: data.points || 1,
metadata: data.metadata,
createdAt: now,
updatedAt: now,
});
}
/**
* Create a true/false question
*/
static createTrueFalse(data) {
const now = new Date();
const questionId = QuestionId_1.QuestionId.generate();
return new Question({
id: questionId,
questionText: data.questionText,
options: ['True', 'False'],
correctAnswer: data.correctAnswer ? 'True' : 'False',
explanation: data.explanation,
order: data.order,
type: 'true_false',
points: data.points || 1,
metadata: data.metadata,
createdAt: now,
updatedAt: now,
});
}
/**
* Reconstitute Question from persistence layer
*/
static fromPersistence(data) {
return new Question(data);
}
// Getters (read-only access to properties)
get id() {
return this.props.id;
}
get questionText() {
return this.props.questionText;
}
get options() {
return [...this.props.options];
}
get correctAnswer() {
return this.props.correctAnswer;
}
get explanation() {
return this.props.explanation;
}
get order() {
return this.props.order;
}
get type() {
return this.props.type;
}
get points() {
return this.props.points;
}
get metadata() {
return this.props.metadata;
}
get createdAt() {
return this.props.createdAt;
}
get updatedAt() {
return this.props.updatedAt;
}
/**
* Business Rules & Domain Logic
*/
get isMultipleChoice() {
return this.props.type === 'multiple_choice';
}
get isTrueFalse() {
return this.props.type === 'true_false';
}
get hasExplanation() {
var _a;
return !!((_a = this.props.explanation) === null || _a === void 0 ? void 0 : _a.trim());
}
get optionCount() {
return this.props.options.length;
}
get difficulty() {
// Business rule: Determine difficulty based on number of options
if (this.isTrueFalse)
return 'easy';
if (this.optionCount <= 3)
return 'medium';
return 'hard';
}
/**
* Business Operations
*/
/**
* Check if provided answer is correct
*/
isCorrectAnswer(userAnswer) {
if (!(userAnswer === null || userAnswer === void 0 ? void 0 : userAnswer.trim())) {
return false;
}
// Business rule: Case-insensitive comparison with trimmed whitespace
const normalizedUserAnswer = userAnswer.trim().toLowerCase();
const normalizedCorrectAnswer = this.props.correctAnswer.trim().toLowerCase();
return normalizedUserAnswer === normalizedCorrectAnswer;
}
/**
* Validate that the correct answer is among the options
*/
isValidConfiguration() {
// For multiple choice and true/false, correct answer must be in options
if (this.isMultipleChoice || this.isTrueFalse) {
return this.props.options.some(option => option.trim().toLowerCase() === this.props.correctAnswer.trim().toLowerCase());
}
// For other types, any non-empty answer is valid
return !!this.props.correctAnswer.trim();
}
/**
* Calculate points based on difficulty and custom rules
*/
calculatePoints() {
// Business rule: Base points with difficulty multiplier
const difficultyMultiplier = { easy: 1, medium: 1.5, hard: 2 };
return Math.round(this.props.points * difficultyMultiplier[this.difficulty]);
}
/**
* Record that this question was answered
*/
recordAnswer(userAnswer, isCorrect, userId) {
this.addDomainEvent(new QuestionAnsweredEvent_1.QuestionAnsweredEvent(this.id.value, userAnswer, this.props.correctAnswer, isCorrect, this.calculatePoints(), userId || 'anonymous'));
}
/**
* Update question with new properties
*/
withUpdates(updates) {
const updatedProps = {
...this.props,
...updates,
updatedAt: new Date(),
};
return new Question(updatedProps);
}
/**
* Change question order
*/
withNewOrder(newOrder) {
if (newOrder < 1) {
throw new Error('Question order must be a positive integer');
}
return new Question({
...this.props,
order: newOrder,
updatedAt: new Date(),
});
}
/**
* Domain Events Management
*/
addDomainEvent(event) {
this._domainEvents.push(event);
}
getDomainEvents() {
return [...this._domainEvents];
}
clearDomainEvents() {
this._domainEvents = [];
}
/**
* Business Validation Rules
*/
validate() {
var _a, _b;
if (!((_a = this.props.questionText) === null || _a === void 0 ? void 0 : _a.trim())) {
throw new Error('Question text is required and cannot be empty');
}
if (this.props.questionText.length > 1000) {
throw new Error('Question text cannot exceed 1000 characters');
}
if (!Array.isArray(this.props.options) || this.props.options.length < 1) {
throw new Error('Question must have at least one option');
}
if (this.props.options.length > 10) {
throw new Error('Question cannot have more than 10 options');
}
// Validate options are not empty
if (this.props.options.some(option => !(option === null || option === void 0 ? void 0 : option.trim()))) {
throw new Error('Question options cannot be empty');
}
// Check for duplicate options
const uniqueOptions = new Set(this.props.options.map(opt => opt.trim().toLowerCase()));
if (uniqueOptions.size !== this.props.options.length) {
throw new Error('Question cannot have duplicate options');
}
if (!((_b = this.props.correctAnswer) === null || _b === void 0 ? void 0 : _b.trim())) {
throw new Error('Question must have a correct answer');
}
if (this.props.correctAnswer.length > 500) {
throw new Error('Correct answer cannot exceed 500 characters');
}
if (this.props.order < 1) {
throw new Error('Question order must be a positive integer');
}
if (this.props.points < 0) {
throw new Error('Question points cannot be negative');
}
if (this.props.points > 100) {
throw new Error('Question points cannot exceed 100');
}
// Type-specific validation
if (this.isTrueFalse && this.props.options.length !== 2) {
throw new Error('True/false questions must have exactly 2 options');
}
if (this.isMultipleChoice && this.props.options.length < 2) {
throw new Error('Multiple choice questions must have at least 2 options');
}
// Validate explanation length if provided
if (this.props.explanation && this.props.explanation.length > 2000) {
throw new Error('Question explanation cannot exceed 2000 characters');
}
// Business rule validation: correct answer must be valid
if (!this.isValidConfiguration()) {
throw new Error('Correct answer must be one of the provided options for multiple choice questions');
}
}
/**
* Convert to plain object for serialization
*/
toSnapshot() {
return {
id: this.props.id,
questionText: this.props.questionText,
options: [...this.props.options],
correctAnswer: this.props.correctAnswer,
explanation: this.props.explanation,
order: this.props.order,
type: this.props.type,
points: this.props.points,
metadata: this.props.metadata,
createdAt: this.props.createdAt,
updatedAt: this.props.updatedAt,
};
}
/**
* Equality comparison based on ID
*/
equals(other) {
return this.id.equals(other.id);
}
}
exports.Question = Question;