UNPKG

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.

296 lines (295 loc) • 9.32 kB
"use strict"; /** * @fileoverview Quiz Domain Entity - Pure Business Logic * @version 1.0.0 * @since 2025-07-29 * @lastUpdated 2025-07-29 * @module Quiz Domain Entity * @description Pure domain entity representing a Quiz 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.Quiz = void 0; const QuizId_1 = require("../value-objects/QuizId"); const QuizCreatedEvent_1 = require("../events/QuizCreatedEvent"); const QuizCompletedEvent_1 = require("../events/QuizCompletedEvent"); /** * Quiz Domain Entity * * @description Represents a quiz in the business domain with all business rules * and invariants. This is a pure domain entity without any * framework dependencies. * * @example * ```typescript * const quiz = Quiz.create({ * title: "TypeScript Fundamentals", * category: "programming", * difficulty: "medium", * questions: [question1, question2] * }); * ``` * * @since 2025-07-29 * @author Claude Code Agent * @requirements REQ-ARCH-001 (Clean Architecture Domain Layer) */ class Quiz { constructor(props) { this.props = props; this._domainEvents = []; this.validate(); } /** * Create a new Quiz instance with business validation */ static create(data) { const now = new Date(); const quizId = QuizId_1.QuizId.generate(); const quiz = new Quiz({ id: quizId, title: data.title, description: data.description, category: data.category, difficulty: data.difficulty, timeLimit: data.timeLimit, isActive: true, questions: data.questions, createdAt: now, updatedAt: now, metadata: data.metadata, }); // Raise domain event quiz.addDomainEvent(new QuizCreatedEvent_1.QuizCreatedEvent(quizId.value, data.title, data.category, data.questions.length)); return quiz; } /** * Reconstitute Quiz from persistence layer */ static fromPersistence(data) { return new Quiz(data); } // Getters (read-only access to properties) get id() { return this.props.id; } get title() { return this.props.title; } get description() { return this.props.description; } get category() { return this.props.category; } get difficulty() { return this.props.difficulty; } get timeLimit() { return this.props.timeLimit; } get isActive() { return this.props.isActive; } get questions() { return [...this.props.questions]; } get createdAt() { return this.props.createdAt; } get updatedAt() { return this.props.updatedAt; } get metadata() { return this.props.metadata; } /** * Business Rules & Domain Logic */ get questionCount() { return this.props.questions.length; } get isTimeLimited() { return this.props.timeLimit !== undefined && this.props.timeLimit > 0; } get estimatedDuration() { // Business rule: Estimate 1 minute per question + time limit consideration const baseTime = this.questionCount * 1; // 1 minute per question return this.props.timeLimit || baseTime; } get difficultyScore() { // Business rule: Convert difficulty to numeric score for analytics const scores = { easy: 1, medium: 2, hard: 3, expert: 4 }; return scores[this.props.difficulty] || 2; } /** * Business Operations */ /** * Check if quiz can be started based on business rules */ canBeStarted() { if (!this.props.isActive) { return { allowed: false, reason: 'Quiz is not active' }; } if (this.questionCount === 0) { return { allowed: false, reason: 'Quiz has no questions' }; } if (this.questionCount > 100) { return { allowed: false, reason: 'Quiz exceeds maximum question limit (100)' }; } return { allowed: true }; } /** * Calculate scoring based on answers */ calculateScore(answers) { let correctAnswers = 0; const totalQuestions = this.questionCount; for (const question of this.props.questions) { const userAnswer = answers[question.id.value]; if (userAnswer && question.isCorrectAnswer(userAnswer)) { correctAnswers++; } } const percentage = totalQuestions > 0 ? (correctAnswers / totalQuestions) * 100 : 0; return { score: correctAnswers, percentage: Math.round(percentage * 100) / 100, correctAnswers, totalQuestions, }; } /** * Complete quiz and raise domain event */ complete(answers, userId) { const result = this.calculateScore(answers); this.addDomainEvent(new QuizCompletedEvent_1.QuizCompletedEvent(this.id.value, userId || 'anonymous', result.score, result.totalQuestions, result.percentage, answers)); } /** * Update quiz metadata */ updateMetadata(metadata) { this.props = { ...this.props, metadata: { ...this.props.metadata, ...metadata }, updatedAt: new Date(), }; } /** * Deactivate quiz */ deactivate() { this.props = { ...this.props, isActive: false, updatedAt: new Date(), }; } /** * Reactivate quiz */ reactivate() { const validation = this.canBeStarted(); if (!validation.allowed && validation.reason !== 'Quiz is not active') { throw new Error(`Cannot reactivate quiz: ${validation.reason}`); } this.props = { ...this.props, isActive: true, 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.title) === null || _a === void 0 ? void 0 : _a.trim())) { throw new Error('Quiz title is required and cannot be empty'); } if (this.props.title.length > 255) { throw new Error('Quiz title cannot exceed 255 characters'); } if (!((_b = this.props.category) === null || _b === void 0 ? void 0 : _b.trim())) { throw new Error('Quiz category is required'); } if (!['easy', 'medium', 'hard', 'expert'].includes(this.props.difficulty)) { throw new Error('Quiz difficulty must be easy, medium, hard, or expert'); } if (this.props.timeLimit !== undefined && this.props.timeLimit < 0) { throw new Error('Quiz time limit cannot be negative'); } if (this.props.timeLimit !== undefined && this.props.timeLimit > 1440) { // 24 hours throw new Error('Quiz time limit cannot exceed 24 hours (1440 minutes)'); } // Validate questions if (this.props.questions.length === 0) { console.warn('Quiz created with no questions - consider adding questions'); } if (this.props.questions.length > 100) { throw new Error('Quiz cannot have more than 100 questions'); } // Validate question uniqueness const questionTexts = this.props.questions.map(q => q.questionText); const uniqueTexts = new Set(questionTexts); if (questionTexts.length !== uniqueTexts.size) { throw new Error('Quiz cannot have duplicate questions'); } } /** * Convert to plain object for serialization */ toSnapshot() { return { id: this.props.id, title: this.props.title, description: this.props.description, category: this.props.category, difficulty: this.props.difficulty, timeLimit: this.props.timeLimit, isActive: this.props.isActive, questions: this.props.questions, createdAt: this.props.createdAt, updatedAt: this.props.updatedAt, metadata: this.props.metadata, }; } /** * Create a copy with updated properties */ withUpdates(updates) { const updatedProps = { ...this.props, ...updates, updatedAt: new Date(), }; return new Quiz(updatedProps); } /** * Equality comparison based on ID */ equals(other) { return this.id.equals(other.id); } } exports.Quiz = Quiz;