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
JavaScript
;
/**
* @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;