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.
278 lines (277 loc) • 10.9 kB
JavaScript
;
/**
* @fileoverview Create Quiz Command Handler - Application Layer
* @version 1.0.0
* @since 2025-07-29
* @lastUpdated 2025-07-29
* @module CreateQuizCommandHandler Application Service
* @description Handles the creation of new quizzes with validation and business rules.
* Orchestrates domain entities, services, and repositories.
* @contributors Claude Code Agent
* @dependencies Domain entities, repositories, services
* @requirements REQ-ARCH-001 (Clean Architecture Application Layer)
* @testCoverage Unit tests for command handling and business logic
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvalidCommandError = exports.DuplicateQuizTitleError = exports.QuizValidationFailedError = exports.QuizCreationError = exports.CreateQuizCommandHandler = void 0;
const Quiz_1 = require("../../domain/entities/Quiz");
const Question_1 = require("../../domain/entities/Question");
/**
* Create Quiz Command Handler
*
* @description Application service that handles quiz creation commands.
* Orchestrates domain logic, validation, and persistence.
* Follows Clean Architecture principles with dependency inversion.
*
* @example
* ```typescript
* const handler = new CreateQuizCommandHandler(
* quizRepository,
* questionRepository,
* validationService,
* eventBus
* );
*
* const result = await handler.handle(createQuizCommand);
* ```
*
* @since 2025-07-29
* @author Claude Code Agent
* @requirements REQ-ARCH-001 (Clean Architecture Application Layer)
*/
class CreateQuizCommandHandler {
constructor(quizRepository, questionRepository, validationService, eventBus) {
this.quizRepository = quizRepository;
this.questionRepository = questionRepository;
this.validationService = validationService;
this.eventBus = eventBus;
}
/**
* Handle the create quiz command
*/
async handle(command, options = {}) {
const { validateStrict = false, checkDuplicateTitle = true, publishImmediately = true, notifyAuthor = false, } = options;
try {
// 1. Pre-validation checks
await this.performPreValidation(command, checkDuplicateTitle);
// 2. Create domain entities
const { quiz, questions } = await this.createDomainEntities(command);
// 3. Validate quiz using domain service
const validationResult = this.validationService.validateQuiz(quiz, {
strictMode: validateStrict,
educationalStandards: true,
accessibilityCheck: true,
contentQualityCheck: true,
});
// 4. Handle validation results
if (!validationResult.isValid) {
throw new QuizValidationFailedError('Quiz validation failed', validationResult.errors, validationResult.warnings);
}
// 5. Persist entities within transaction
await this.persistQuizWithQuestions(quiz, questions);
// 6. Publish domain events
await this.publishDomainEvents(quiz, questions);
// 7. Handle post-creation tasks
await this.handlePostCreationTasks(quiz, options);
// 8. Return result
return {
quizId: quiz.id.value,
title: quiz.title,
questionsCount: questions.length,
validationScore: validationResult.score,
warnings: validationResult.warnings.map(w => w.message),
createdAt: quiz.createdAt,
};
}
catch (error) {
// Log error with command context
console.error('Quiz creation failed:', {
commandId: command.commandId,
error: error instanceof Error ? error.message : 'Unknown error',
command: command.toSummary(),
});
// Re-throw with additional context
if (error instanceof QuizCreationError) {
throw error;
}
throw new QuizCreationError(`Failed to create quiz: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : new Error('Unknown error'));
}
}
/**
* Perform pre-validation checks
*/
async performPreValidation(command, checkDuplicateTitle) {
// Check for duplicate title if requested
if (checkDuplicateTitle) {
const titleExists = await this.quizRepository.isTitleUnique(command.data.title);
if (!titleExists) {
throw new DuplicateQuizTitleError(command.data.title);
}
}
// Additional business rule validations can go here
// For example: author permissions, rate limiting, etc.
}
/**
* Create domain entities from command data
*/
async createDomainEntities(command) {
// Create questions first
const questions = command.data.questions.map((questionData, index) => {
if (questionData.options.length === 2 &&
(questionData.options.includes('True') ||
questionData.options.includes('False') ||
questionData.options.includes('true') ||
questionData.options.includes('false'))) {
// Create true/false question
const isTrue = questionData.correctAnswer.toLowerCase() === 'true';
return Question_1.Question.createTrueFalse({
questionText: questionData.question,
correctAnswer: isTrue,
explanation: questionData.explanation,
order: index + 1,
points: questionData.points || 1,
});
}
else {
// Create multiple choice question
return Question_1.Question.createMultipleChoice({
questionText: questionData.question,
options: questionData.options,
correctAnswer: questionData.correctAnswer,
explanation: questionData.explanation,
order: index + 1,
points: questionData.points || 1,
});
}
});
// Create quiz with questions
const quiz = Quiz_1.Quiz.create({
title: command.data.title,
description: command.data.description,
category: command.data.category,
difficulty: command.data.difficulty,
timeLimit: command.data.timeLimit,
questions: questions,
metadata: {
...command.data.metadata,
createdByCommand: command.commandId,
authorId: command.data.authorId,
},
});
return { quiz, questions };
}
/**
* Persist quiz and questions within a transaction
*/
async persistQuizWithQuestions(quiz, questions) {
// Use repository transaction to ensure atomicity
await this.quizRepository.transaction(async (transactionalQuizRepo) => {
// Save quiz first
await transactionalQuizRepo.save(quiz);
// Save questions within the same transaction
await this.questionRepository.transaction(async (transactionalQuestionRepo) => {
await transactionalQuestionRepo.saveMany(questions);
});
});
}
/**
* Publish domain events from entities
*/
async publishDomainEvents(quiz, questions) {
if (!this.eventBus) {
return; // Event bus is optional
}
// Publish quiz events
const quizEvents = quiz.getDomainEvents();
for (const event of quizEvents) {
await this.eventBus.publish(event);
}
quiz.clearDomainEvents();
// Publish question events
for (const question of questions) {
const questionEvents = question.getDomainEvents();
for (const event of questionEvents) {
await this.eventBus.publish(event);
}
question.clearDomainEvents();
}
}
/**
* Handle post-creation tasks
*/
async handlePostCreationTasks(quiz, options) {
var _a;
const tasks = [];
// Notification task
if (options.notifyAuthor && ((_a = quiz.metadata) === null || _a === void 0 ? void 0 : _a.authorId)) {
tasks.push(this.notifyAuthor(quiz.metadata.authorId, quiz));
}
// Additional post-creation tasks can be added here
// For example: analytics tracking, cache warming, etc.
// Execute all tasks in parallel
await Promise.allSettled(tasks);
}
/**
* Notify author of successful quiz creation
*/
async notifyAuthor(authorId, quiz) {
try {
// This would integrate with a notification service
console.log(`Notifying author ${authorId} of quiz creation: ${quiz.title}`);
// In a real implementation, this would send an email, push notification, etc.
// await this.notificationService.send({
// userId: authorId,
// type: 'quiz_created',
// data: { quizId: quiz.id.value, title: quiz.title }
// });
}
catch (error) {
// Log but don't fail the command for notification errors
console.warn('Failed to notify author:', error);
}
}
/**
* Validate command before processing
*/
validateCommand(command) {
if (!command.isValid()) {
throw new InvalidCommandError('Command validation failed');
}
// Additional command-level validations can go here
}
}
exports.CreateQuizCommandHandler = CreateQuizCommandHandler;
/**
* Error Types
*/
class QuizCreationError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'QuizCreationError';
}
}
exports.QuizCreationError = QuizCreationError;
class QuizValidationFailedError extends QuizCreationError {
constructor(message, errors, warnings) {
super(message);
this.errors = errors;
this.warnings = warnings;
this.name = 'QuizValidationFailedError';
}
}
exports.QuizValidationFailedError = QuizValidationFailedError;
class DuplicateQuizTitleError extends QuizCreationError {
constructor(title) {
super(`Quiz with title "${title}" already exists`);
this.name = 'DuplicateQuizTitleError';
}
}
exports.DuplicateQuizTitleError = DuplicateQuizTitleError;
class InvalidCommandError extends QuizCreationError {
constructor(message) {
super(message);
this.name = 'InvalidCommandError';
}
}
exports.InvalidCommandError = InvalidCommandError;