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