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.

269 lines (268 loc) 9.82 kB
"use strict"; /** * @fileoverview List Quizzes Query Handler - Clean Architecture Application Layer * @version 1.0.0 * @since 2025-07-30 * @module ListQuizzesQueryHandler * @description Handles quiz listing with business rules, permissions, and caching * * @architecture * Layer: Application (Query Handler) * Pattern: Query Handler Pattern + Repository Pattern * Dependencies: Domain repositories, permission service, cache * * @relationships * DEPENDS_ON: * - IQuizRepository (Domain) * - PermissionService (Infrastructure) * - CacheService (Infrastructure) * USED_BY: * - MCP list-quizzes tool (Infrastructure) * - HTTP list endpoints (Infrastructure) * * @contributors Claude Code Agent * @testCoverage Query handler tests, integration tests */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ListQuizzesQueryHandler = void 0; /** * List Quizzes Query Handler * * @description Processes quiz listing queries with proper business logic, * permission checking, caching, and pagination. * * @example * ```typescript * const handler = new ListQuizzesQueryHandler({ quizRepository, permissionService, cacheService }); * const result = await handler.handle(listQuery); * ``` * * @since 2025-07-30 * @author Claude Code Agent */ class ListQuizzesQueryHandler { constructor(dependencies) { this.quizRepository = dependencies.quizRepository; this.permissionService = dependencies.permissionService; this.cacheService = dependencies.cacheService; } /** * Handle List Quizzes Query * * @description Executes quiz listing with permission checking, business rules, * caching, and proper response formatting. * * @param query List quizzes query with filters and pagination * @returns Promise<ListQuizzesResult> Formatted result with metadata */ async handle(query) { console.log(`📋 Clean Architecture: Processing ${query.toString()}`); try { // 1. Check permissions await this.validatePermissions(query); // 2. Try cache first (for non-personalized queries) const cacheKey = this.getCacheKey(query); if (!query.userId && !query.includeStatistics) { const cachedResult = await this.getCachedResult(cacheKey); if (cachedResult) { console.log(`✅ Returning cached quiz list (${cachedResult.quizzes.length} quizzes)`); return cachedResult; } } // 3. Query repository const searchResult = await this.queryRepository(query); // 4. Apply business rules const filteredQuizzes = await this.applyBusinessRules(searchResult.quizzes, query); // 5. Format response const result = await this.formatResponse(query, { ...searchResult, quizzes: filteredQuizzes, }, false); // 6. Cache result (if appropriate) if (!query.userId && !query.includeStatistics) { await this.cacheResult(cacheKey, result); } console.log(`✅ Quiz list retrieved successfully: ${result.quizzes.length} quizzes`); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`❌ Quiz listing failed: ${errorMessage}`); throw error; } } /** * Validate user permissions for quiz listing */ async validatePermissions(query) { if (query.userId) { const hasPermission = await this.permissionService.hasPermission(query.userId, 'quiz:read'); if (!hasPermission) { throw new Error('Insufficient permissions to list quizzes'); } } // Anonymous users can only see active quizzes if (!query.userId && query.isActive === false) { throw new Error('Anonymous users can only view active quizzes'); } } /** * Generate cache key for the query */ getCacheKey(query) { const keyParts = [ 'quiz:list', `limit:${query.limit}`, `offset:${query.offset}`, `sort:${query.sortBy}:${query.sortOrder}`, `active:${query.isActive}`, ]; if (query.category) keyParts.push(`cat:${query.category}`); if (query.difficulty) keyParts.push(`diff:${query.difficulty}`); if (query.searchTerm) keyParts.push(`search:${query.searchTerm}`); if (query.includeQuestions) keyParts.push('withQuestions'); return keyParts.join(':'); } /** * Get cached result if available */ async getCachedResult(cacheKey) { try { const cached = await this.cacheService.get(cacheKey); if (cached && typeof cached === 'object') { return { ...cached, cached: true, timestamp: new Date().toISOString(), }; } } catch (error) { console.warn('Cache retrieval warning:', error); } return null; } /** * Query the repository */ async queryRepository(query) { const criteria = query.toSearchCriteria(); const options = query.toSearchOptions(); if (query.isSearchQuery()) { // Use search method for text search return await this.quizRepository.search(criteria, options); } else if (query.category) { // Use category-specific search return await this.quizRepository.findByCategory(query.category, options); } else if (query.difficulty) { // Use difficulty-specific search return await this.quizRepository.findByDifficulty(query.difficulty, options); } else { // Use general findAll return await this.quizRepository.findAll(options); } } /** * Apply business rules to filter results */ async applyBusinessRules(quizzes, query) { let filteredQuizzes = [...quizzes]; // Business Rule: Anonymous users only see public/active quizzes if (!query.userId) { filteredQuizzes = filteredQuizzes.filter(quiz => quiz.isActive !== false); } // Business Rule: Only include quizzes with questions (unless specifically requested otherwise) filteredQuizzes = filteredQuizzes.filter(quiz => quiz.questions && quiz.questions.length > 0); return filteredQuizzes; } /** * Format the response */ async formatResponse(query, searchResult, cached) { const totalPages = Math.ceil(searchResult.totalCount / query.limit); return { success: true, quizzes: searchResult.quizzes.map(quiz => this.mapQuizToView(quiz, query)), pagination: { total: searchResult.totalCount, limit: query.limit, offset: query.offset, page: Math.floor(query.offset / query.limit) + 1, totalPages, hasMore: searchResult.hasMore, }, filters: { category: query.category, difficulty: query.difficulty, searchTerm: query.searchTerm, isActive: query.isActive, }, sort: { sortBy: query.sortBy, sortOrder: query.sortOrder, }, cached, timestamp: new Date().toISOString(), }; } /** * Map domain quiz to view model */ mapQuizToView(quiz, query) { var _a; const view = { id: quiz.id, title: quiz.title, description: quiz.description || '', category: quiz.category || 'General', difficulty: quiz.difficulty || 'medium', questionCount: ((_a = quiz.questions) === null || _a === void 0 ? void 0 : _a.length) || 0, isActive: quiz.isActive !== false, createdAt: quiz.createdAt || new Date().toISOString(), updatedAt: quiz.updatedAt || new Date().toISOString(), }; // Include questions if requested if (query.includeQuestions && quiz.questions) { view.questions = quiz.questions.map((q, index) => ({ id: q.id, questionText: q.question || q.questionText, options: q.options || [], correctAnswer: q.answer || q.correctAnswer, explanation: q.explanation, order: q.order || index + 1, points: q.points || 10, })); } // Include statistics if requested if (query.includeStatistics) { view.statistics = { totalAttempts: 0, // Would be populated from statistics service averageScore: 0, completionRate: 0, }; } return view; } /** * Cache the result */ async cacheResult(cacheKey, result) { try { // Cache for 5 minutes (quiz lists change frequently) await this.cacheService.set(cacheKey, result, 300); console.log(`💾 Cached quiz list result: ${cacheKey}`); } catch (error) { console.warn('Cache storage warning:', error); // Don't fail the operation for cache issues } } } exports.ListQuizzesQueryHandler = ListQuizzesQueryHandler;