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