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.

305 lines (304 loc) • 11.1 kB
"use strict"; /** * @fileoverview Get Quiz Query Handler - Application Layer * @version 1.0.0 * @since 2025-07-29 * @lastUpdated 2025-07-29 * @module GetQuizQueryHandler Application Service * @description Handles quiz retrieval queries with data projection and permissions. * Orchestrates domain repositories and applies business rules for data access. * @contributors Claude Code Agent * @dependencies Domain repositories, entities * @requirements REQ-ARCH-001 (Clean Architecture Application Layer) * @testCoverage Unit tests for query handling and data projection */ Object.defineProperty(exports, "__esModule", { value: true }); exports.InsufficientPermissionsError = exports.QuizNotFoundError = exports.QuizQueryError = exports.GetQuizQueryHandler = void 0; /** * Get Quiz Query Handler * * @description Application service that handles quiz retrieval queries. * Orchestrates data access, applies business rules, and projects * data into appropriate view models for different use cases. * * @example * ```typescript * const handler = new GetQuizQueryHandler( * quizRepository, * questionRepository, * resultRepository, * permissionService * ); * * const quizView = await handler.handle(getQuizQuery); * ``` * * @since 2025-07-29 * @author Claude Code Agent * @requirements REQ-ARCH-001 (Clean Architecture Application Layer) */ class GetQuizQueryHandler { constructor(quizRepository, permissionService, cacheService) { this.quizRepository = quizRepository; this.permissionService = permissionService; this.cacheService = cacheService; } /** * Handle the get quiz query */ async handle(query, options = {}) { const { checkPermissions = true, includeInactive = false, maxCacheAge = 300, // 5 minutes default } = options; try { // 1. Check cache first const cachedResult = await this.checkCache(query, maxCacheAge); if (cachedResult) { return cachedResult; } // 2. Retrieve quiz entity const quiz = await this.retrieveQuiz(query, includeInactive); // 3. Check permissions if (checkPermissions) { await this.checkPermissions(quiz, query.userId); } // 4. Build base quiz view const quizView = await this.buildQuizView(quiz, query); // 5. Add optional inclusions const enrichedView = await this.enrichQuizView(quizView, quiz, query); // 6. Cache the result await this.cacheResult(query, enrichedView, maxCacheAge); return enrichedView; } catch (error) { // Log error with query context console.error('Quiz retrieval failed:', { queryId: query.queryId, quizId: query.data.quizId, error: error instanceof Error ? error.message : 'Unknown error', query: query.toSummary(), }); // Re-throw with additional context if (error instanceof QuizQueryError) { throw error; } throw new QuizQueryError(`Failed to retrieve quiz: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : new Error('Unknown error')); } } /** * Check cache for existing result */ async checkCache(query, maxCacheAge) { if (!this.cacheService) { return null; } try { const cacheKey = this.generateCacheKey(query); const cached = await this.cacheService.get(cacheKey); if (cached && this.isCacheValid(cached, maxCacheAge)) { return cached; } } catch (error) { // Cache errors shouldn't fail the query console.warn('Cache retrieval failed:', error); } return null; } /** * Retrieve quiz entity from repository */ async retrieveQuiz(query, includeInactive) { const quiz = query.shouldIncludeQuestions ? await this.quizRepository.findByIdWithQuestions(query.quizId) : await this.quizRepository.findById(query.quizId); if (!quiz) { throw new QuizNotFoundError(query.data.quizId); } // Check if quiz is active (unless includeInactive is true) if (!includeInactive && !quiz.isActive) { throw new QuizNotFoundError(query.data.quizId, 'Quiz is not active'); } return quiz; } /** * Check user permissions for quiz access */ async checkPermissions(quiz, userId) { if (!this.permissionService) { return; // No permission checking if service not provided } const hasAccess = await this.permissionService.canAccessQuiz(quiz.id.value, userId); if (!hasAccess) { throw new InsufficientPermissionsError(quiz.id.value, userId); } } /** * Build base quiz view from entity */ async buildQuizView(quiz, query) { return { id: quiz.id.value, title: quiz.title, description: quiz.description, category: quiz.category, difficulty: quiz.difficulty, timeLimit: quiz.timeLimit, isActive: quiz.isActive, questionCount: quiz.questionCount, createdAt: quiz.createdAt.toISOString(), updatedAt: quiz.updatedAt.toISOString(), }; } /** * Enrich quiz view with optional inclusions */ async enrichQuizView(baseView, quiz, query) { const enrichments = []; let questions; let statistics = null; let userResults = null; // Load questions if needed (simplified - use quiz.questions if available) if (query.shouldIncludeQuestions) { questions = quiz.questions || []; } // Load statistics if needed if (query.shouldIncludeStatistics) { enrichments.push(this.quizRepository.getStatistics(quiz.id).then(stats => { statistics = stats; })); } // Load user results if personalized query (simplified - skip for now) if (query.isPersonalized && query.userId) { // TODO: Implement user results when we have proper result repository userResults = { attemptCount: 0, bestScore: null, bestPercentage: null, lastAttemptDate: null, }; } // Wait for all enrichments to complete await Promise.all(enrichments); // Build enriched view return { ...baseView, questions: questions ? questions.map(q => this.mapQuestionToView(q)) : undefined, statistics: statistics ? this.mapStatisticsToView(statistics) : undefined, userResults: userResults ? this.mapUserResultsToView(userResults) : undefined, metadata: query.shouldIncludeMetadata ? quiz.metadata : undefined, }; } /** * Map question entity to view model */ mapQuestionToView(question) { return { id: question.id.value, questionText: question.questionText, options: question.options, order: question.order, type: question.type, points: question.points, explanation: question.explanation, hasExplanation: question.hasExplanation, difficulty: question.difficulty, }; } /** * Map statistics to view model */ mapStatisticsToView(statistics) { var _a; return { totalAttempts: statistics.totalAttempts, averageScore: statistics.averageScore, highestScore: statistics.highestScore, lowestScore: statistics.lowestScore, averageCompletionTime: statistics.averageCompletionTime, completionRate: statistics.completionRate, popularityRank: statistics.popularityRank, lastAttemptDate: (_a = statistics.lastAttemptDate) === null || _a === void 0 ? void 0 : _a.toISOString(), }; } /** * Map user results to view model */ mapUserResultsToView(userResults) { var _a; return { attemptCount: userResults.attemptCount, bestScore: userResults.bestScore, bestPercentage: userResults.bestPercentage, lastAttemptDate: (_a = userResults.lastAttemptDate) === null || _a === void 0 ? void 0 : _a.toISOString(), hasCompleted: userResults.attemptCount > 0, }; } /** * Cache the result */ async cacheResult(query, result, maxCacheAge) { if (!this.cacheService) { return; } try { const cacheKey = this.generateCacheKey(query); await this.cacheService.set(cacheKey, result, maxCacheAge); } catch (error) { // Cache errors shouldn't fail the query console.warn('Cache storage failed:', error); } } /** * Generate cache key for query */ generateCacheKey(query) { const keyParts = [ 'quiz', query.data.quizId, query.shouldIncludeQuestions ? 'q' : '', query.shouldIncludeStatistics ? 's' : '', query.shouldIncludeResults ? 'r' : '', query.shouldIncludeMetadata ? 'm' : '', query.userId || 'anon', ]; return keyParts.filter(Boolean).join(':'); } /** * Check if cached result is still valid */ isCacheValid(cached, maxCacheAge) { // Simple timestamp-based validation // In a real implementation, you might check ETag or version numbers return true; // Simplified for this example } } exports.GetQuizQueryHandler = GetQuizQueryHandler; // External service interfaces imported from infrastructure layer /** * Error Types */ class QuizQueryError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'QuizQueryError'; } } exports.QuizQueryError = QuizQueryError; class QuizNotFoundError extends QuizQueryError { constructor(quizId, reason) { const message = reason ? `Quiz ${quizId} not found: ${reason}` : `Quiz ${quizId} not found`; super(message); this.name = 'QuizNotFoundError'; } } exports.QuizNotFoundError = QuizNotFoundError; class InsufficientPermissionsError extends QuizQueryError { constructor(quizId, userId) { const userText = userId ? ` for user ${userId}` : ''; super(`Insufficient permissions to access quiz ${quizId}${userText}`); this.name = 'InsufficientPermissionsError'; } } exports.InsufficientPermissionsError = InsufficientPermissionsError;