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
JavaScript
"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;