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.

470 lines (469 loc) โ€ข 19 kB
"use strict"; /** * @moduleName: Database Initialization Script (MVP Local) * @version: 1.0.0 * @since: 2025-07-24 * @lastUpdated: 2025-07-24 * @projectSummary: Local SQLite database setup for quiz management MVP - no authentication required * @techStack: TypeORM, SQLite, Migration Support, Sample Data * @dependency: typeorm, fs, path, ../database/config, ../database/entities * @interModuleDependency: Database entities and repositories for quiz functionality only * @requirementsTraceability: * {@link Requirements.REQ_DATA_001} (TypeORM Quiz Schema) * {@link Requirements.REQ_DATA_003} (Automated Data Validation) * @briefDescription: Simple local database initialization with sample quiz data for MVP deployment * @methods: initialize, seedSampleData, ensureDataDirectory, importLegacyData, reset * @contributors: GitHub Copilot, Open Source Community * @examples: const init = new DatabaseInitializer(); await init.initialize(); * @vulnerabilitiesAssessment: Local SQLite only, no network access, TypeORM prevents SQL injection */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.DatabaseInitializer = void 0; exports.main = main; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const config_1 = require("./config"); const Question_1 = require("./entities/Question"); const Quiz_1 = require("./entities/Quiz"); /** * @description Simple database initialization for local MVP deployment * Handles initialization, migration, seeding, and management of the local and test databases. * * @remarks * - Supports SQLite and enables foreign key constraints. * - Seeds sample quiz and question data if the database is empty. * - Provides utilities for legacy data migration, test data seeding, and database reset. * - Ensures required data directories exist for SQLite storage. * * @example * ```typescript * const dbInit = new DatabaseInitializer(); * await dbInit.initialize(); * ``` */ class DatabaseInitializer { constructor() { this.dataSource = null; } /** * @description Initializes local SQLite database with sample data * @returns {Promise<DataSource>} Initialized database connection * @throws {Error} If database initialization fails */ async initialize() { try { console.log('๐Ÿ”„ Starting local database initialization...'); // Ensure data directory exists this.ensureDataDirectory(); // Initialize database connection this.dataSource = await (0, config_1.initializeDatabase)(); // CRITICAL FIX: Enable foreign key constraints in SQLite if (this.dataSource.driver.options.type === 'sqlite') { await this.dataSource.query('PRAGMA foreign_keys = ON'); console.log('โœ… Foreign key constraints enabled'); } // Ensure database schema is created (especially important in Docker) if (process.env.NODE_ENV !== 'production') { console.log('๐Ÿ”„ Synchronizing database schema...'); // Synchronize schema without transaction for SQLite if (this.dataSource.driver.options.type === 'sqlite') { await this.dataSource.synchronize(false); // dropBeforeSync = false } else { await this.dataSource.synchronize(); } console.log('โœ… Database schema synchronized'); } // Run any pending migrations await this.runMigrations(); // Seed sample data if database is empty await this.seedSampleData(); console.log('โœ… Local database initialized successfully'); return this.dataSource; } catch (error) { console.error('โŒ Database initialization failed:', error); throw error; } } /** * @description Initialize test database for development * @returns {Promise<DataSource>} Test database connection */ async initializeTest() { try { this.dataSource = await (0, config_1.createTestDatabase)(); // CRITICAL FIX: Enable foreign key constraints in SQLite for tests if (this.dataSource.driver.options.type === 'sqlite') { await this.dataSource.query('PRAGMA foreign_keys = ON'); console.log('โœ… Foreign key constraints enabled for tests'); } await this.seedTestData(); console.log('โœ… Test database initialized successfully'); return this.dataSource; } catch (error) { console.error('โŒ Test database initialization failed:', error); throw error; } } /** * @description Ensures the data directory exists for SQLite database */ ensureDataDirectory() { const dataDir = path.resolve('./data'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); console.info('๐Ÿ“ Created data directory'); } } /** * @description Runs database migrations if available */ async runMigrations() { if (!this.dataSource) throw new Error('Database not initialized'); try { const pendingMigrations = await this.dataSource.showMigrations(); if (pendingMigrations) { console.log('๐Ÿ”„ Running database migrations...'); await this.dataSource.runMigrations(); console.log('โœ… Migrations completed'); } } catch (error) { console.info('โ„น๏ธ No migrations to run or migrations not configured'); } } /** * @description Seeds sample quiz data if database is empty */ async seedSampleData() { if (!this.dataSource) throw new Error('Database not initialized'); const quizRepository = this.dataSource.getRepository(Quiz_1.Quiz); try { const existingQuizzes = await quizRepository.count(); if (existingQuizzes === 0) { console.log('๐ŸŒฑ Seeding sample quiz data...'); await this.createSampleQuizzes(); console.log('โœ… Sample data seeded'); } } catch (error) { // If table doesn't exist yet, create sample data if (error.code === 'SQLITE_ERROR' && error.message.includes('no such table')) { console.log('๐ŸŒฑ Creating sample quiz data (table not found)...'); await this.createSampleQuizzes(); console.log('โœ… Sample data seeded'); } else { throw error; } } } /** * @description Creates sample quizzes for demonstration */ async createSampleQuizzes() { if (!this.dataSource) throw new Error('Database not initialized'); const quizRepository = this.dataSource.getRepository(Quiz_1.Quiz); const questionRepository = this.dataSource.getRepository(Question_1.Question); // TypeScript Fundamentals Quiz const tsQuiz = quizRepository.create({ title: 'TypeScript Fundamentals', description: 'Test your knowledge of TypeScript basics', category: 'programming', difficulty: 'medium', timeLimit: 15, isActive: true, }); const savedTsQuiz = await quizRepository.save(tsQuiz); const tsQuestions = [ { question: 'What is TypeScript?', options: [ 'A JavaScript framework', 'A superset of JavaScript that adds static typing', 'A database query language', 'A CSS preprocessor', ], answer: 'A superset of JavaScript that adds static typing', explanation: 'TypeScript is a programming language developed by Microsoft that builds on JavaScript by adding static type definitions.', order: 1, type: 'multiple_choice', points: 1, }, { question: 'Which keyword is used with type annotations in TypeScript?', options: [ 'var only', 'let only', 'const only', 'All of the above can be used with type annotations', ], answer: 'All of the above can be used with type annotations', explanation: 'TypeScript allows type annotations with var, let, and const declarations.', order: 2, type: 'multiple_choice', points: 1, }, { question: 'What is the purpose of interfaces in TypeScript?', options: [ 'To define the structure of objects', 'To create classes', 'To import modules', 'To handle errors', ], answer: 'To define the structure of objects', explanation: 'Interfaces in TypeScript are used to define contracts for the shape of objects.', order: 3, type: 'multiple_choice', points: 1, }, ]; const tsQuestionEntities = tsQuestions.map(q => questionRepository.create({ ...q, quizId: savedTsQuiz.id, })); await questionRepository.save(tsQuestionEntities); // JavaScript Basics Quiz const jsQuiz = quizRepository.create({ title: 'JavaScript Essentials', description: 'Essential JavaScript concepts every developer should know', category: 'programming', difficulty: 'beginner', timeLimit: 10, isActive: true, }); const savedJsQuiz = await quizRepository.save(jsQuiz); const jsQuestions = [ { question: 'Which of the following is not a JavaScript data type?', options: ['string', 'boolean', 'integer', 'object'], answer: 'integer', explanation: 'JavaScript has number, not integer as a specific type.', order: 1, type: 'multiple_choice', points: 1, }, { question: 'What does "this" refer to in JavaScript?', options: [ 'The current function', 'The global object', 'The object that owns the method', 'It depends on the context', ], answer: 'It depends on the context', explanation: 'The value of "this" in JavaScript depends on how a function is called.', order: 2, type: 'multiple_choice', points: 1, }, ]; const jsQuestionEntities = jsQuestions.map(q => questionRepository.create({ ...q, quizId: savedJsQuiz.id, })); await questionRepository.save(jsQuestionEntities); } /** * @description Seeds basic test data for development */ async seedTestData() { if (!this.dataSource) throw new Error('Database not initialized'); const quizRepository = this.dataSource.getRepository(Quiz_1.Quiz); const questionRepository = this.dataSource.getRepository(Question_1.Question); const testQuiz = quizRepository.create({ title: 'Test Quiz', description: 'A simple test quiz for development', category: 'test', difficulty: 'easy', isActive: true, }); const savedQuiz = await quizRepository.save(testQuiz); const testQuestion = questionRepository.create({ quizId: savedQuiz.id, question: 'What is 2 + 2?', options: ['3', '4', '5', '6'], answer: '4', explanation: 'Basic arithmetic: 2 + 2 = 4', order: 1, type: 'multiple_choice', points: 1, }); await questionRepository.save(testQuestion); } /** * @description Imports data from legacy file-based storage * @param {string} legacyDataPath Path to legacy data files */ async migrateLegacyData(legacyDataPath) { if (!this.dataSource) throw new Error('Database not initialized'); try { const currentQuizPath = path.join(legacyDataPath, 'current.json'); const resultsLogPath = path.join(legacyDataPath, 'results.jsonl'); if (fs.existsSync(currentQuizPath)) { console.log('๐Ÿ”„ Migrating legacy quiz data...'); const legacyQuiz = JSON.parse(fs.readFileSync(currentQuizPath, 'utf8')); await this.importLegacyQuiz(legacyQuiz); console.log('โœ… Legacy quiz data migrated'); } if (fs.existsSync(resultsLogPath)) { console.log('๐Ÿ”„ Legacy results found but migration not implemented'); console.log('โ„น๏ธ Results migration can be added if needed'); } } catch (error) { console.error('โŒ Legacy data migration failed:', error); throw error; } } /** * @description Imports a single quiz from legacy format * @param {any} legacyQuiz Legacy quiz object */ async importLegacyQuiz(legacyQuiz) { if (!this.dataSource) throw new Error('Database not initialized'); const quizRepository = this.dataSource.getRepository(Quiz_1.Quiz); const questionRepository = this.dataSource.getRepository(Question_1.Question); const quiz = quizRepository.create({ title: legacyQuiz.title || 'Migrated Quiz', description: legacyQuiz.description || 'Migrated from legacy system', category: 'migrated', difficulty: 'medium', isActive: true, }); const savedQuiz = await quizRepository.save(quiz); if (legacyQuiz.questions && Array.isArray(legacyQuiz.questions)) { const questions = legacyQuiz.questions.map((q, index) => questionRepository.create({ quizId: savedQuiz.id, question: q.question || '', options: q.options || [], answer: q.answer || '', explanation: q.explanation || 'No explanation provided', order: index + 1, type: 'multiple_choice', points: 1, })); await questionRepository.save(questions); } } /** * @description Resets the database by dropping and recreating all tables */ async reset() { if (!this.dataSource) throw new Error('Database not initialized'); console.warn('โš ๏ธ Resetting database...'); await this.dataSource.dropDatabase(); await this.dataSource.synchronize(); await this.seedSampleData(); console.log('โœ… Database reset completed'); } /** * @description Closes database connection */ async cleanup() { if (this.dataSource && this.dataSource.isInitialized) { await this.dataSource.destroy(); console.log('๐Ÿงน Database connection closed'); } } /** * @description Gets the current data source connection * @returns {DataSource | null} Current database connection */ getDataSource() { return this.dataSource; } } exports.DatabaseInitializer = DatabaseInitializer; /** * @description CLI interface for database management */ async function main() { const command = process.argv[2]; const initializer = new DatabaseInitializer(); try { switch (command) { case 'init': await initializer.initialize(); break; case 'reset': await initializer.initialize(); await initializer.reset(); break; case 'migrate': await initializer.initialize(); if (process.argv[3]) { await initializer.migrateLegacyData(process.argv[3]); } else { console.log('Please provide legacy data path: npm run db migrate <path>'); } break; case 'test': await initializer.initializeTest(); break; default: console.log('Usage: npm run db [init|reset|migrate|test] [legacy-data-path]'); console.log(''); console.log('Commands:'); console.log(' init - Initialize database with sample data'); console.log(' reset - Reset database (WARNING: deletes all data)'); console.log(' migrate - Import data from legacy file-based storage'); console.log(' test - Initialize test database'); } } catch (error) { console.error('Database operation failed:', error); process.exit(1); } finally { await initializer.cleanup(); } } // Run if called directly if (require.main === module) { main(); }