server/controller.ts
import console from 'console';
import {escape} from 'entities';
import type {Request, Response} from 'express';
import {baseDatabasePath, databaseVersionErrorCode, memoryDatabasePath} from './const';
import DatabaseCoordinator from './model/database/databaseCoordinator';
import DatabaseMigrator from './model/database/databaseMigrator';
import type {IdeaForAdding} from './model/ideas/ideaForAdding';
import type {Language} from './model/languages/language';
import {type Manager} from './model/manager';
import type {Settings} from './model/settings/settings';
import {databasePath} from './options';
import {normalizeIdea} from './utils/expressionStringUtils';
// This is the contact point for the front-end and the back-end
// Controller as in C in MVC
// It must validate arguments before calling methods of the managers
async function initDatabase(databasePath: string) {
let dbCoordinator = new DatabaseCoordinator(databasePath);
await dbCoordinator.init();
if (!dbCoordinator.isValid) {
if (dbCoordinator.isValidVersion) {
console.error(`Invalid database path ('${databasePath}'). Defaulting to '${memoryDatabasePath}'.`);
} else {
console.error(`Old database version. Defaulting to '${memoryDatabasePath}'. You can migrate the database through the API or UI.`);
}
dbCoordinator = new DatabaseCoordinator(memoryDatabasePath);
await dbCoordinator.init();
}
return dbCoordinator;
}
let dbCoordinator = await initDatabase(databasePath);
let {dataServiceProvider} = dbCoordinator;
export async function getStats(_: Request, res: Response): Promise<void> {
const stats = await dataServiceProvider.statsCounter.getStats();
res.send(JSON.stringify(stats));
}
export async function getNextPracticeIdea(_: Request, res: Response): Promise<void> {
if ((await dataServiceProvider.ideaManager.countIdeas()) === 0) {
res.status(404);
res.end();
}
const idea = await dataServiceProvider.practiceManager.getNextIdea();
if (idea === undefined) {
// There are no practiceable ideas
res.status(404).end();
}
res.send(JSON.stringify(idea));
}
export async function getLanguageById(req: Request, res: Response): Promise<void> {
if (!(await validateLanguageIdInRequest(req, res))) {
return;
}
const languageId = parseInt(req.params.id, 10);
const language = await dataServiceProvider.languageManager.getLanguage(languageId);
res.send(language);
}
export async function deleteLanguage(req: Request, res: Response): Promise<void> {
if (!(await validateLanguageIdInRequest(req, res))) {
return;
}
const languageId = parseInt(req.params.id, 10);
await dataServiceProvider.languageManager.deleteLanguage(languageId);
res.end();
}
export async function addLanguage(req: Request, res: Response): Promise<void> {
if (!(await dataServiceProvider.inputValidator.validateLanguageForAdding(req.body))) {
res.status(400);
res.end();
return;
}
const l: Language = await dataServiceProvider.languageManager.addLanguage(req.body.name as string);
res.status(201);
res.send(JSON.stringify(l));
}
export async function editLanguages(req: Request, res: Response): Promise<void> {
if (!(await dataServiceProvider.inputValidator.validateLanguagesForEditing(req.body))) {
res.status(400);
res.end();
return;
}
const ll = await dataServiceProvider.languageManager.editLanguages(req.body as Language[]);
// Reset practice manager because practiceable ideas may change after editing languages
dataServiceProvider.practiceManager.clear();
res.send(JSON.stringify(ll));
}
export async function getLanguages(_: Request, res: Response): Promise<void> {
res.send(JSON.stringify(await dataServiceProvider.languageManager.getLanguages()));
}
export async function addIdea(req: Request, res: Response): Promise<void> {
if (!(await dataServiceProvider.inputValidator.validateIdeaForAdding(req.body as IdeaForAdding))) {
res.status(400);
res.end();
return;
}
const ideaForAdding = req.body as IdeaForAdding;
normalizeIdea(ideaForAdding);
const returnIdea = await dataServiceProvider.ideaManager.addIdea(ideaForAdding);
res.status(201);
res.send(JSON.stringify(returnIdea));
}
export async function getIdeaById(req: Request, res: Response): Promise<void> {
if (!(await validateIdeaIdInRequest(req, res))) {
return;
}
const idea = await dataServiceProvider.ideaManager.getIdea(parseInt(req.params.id, 10));
res.send(idea);
}
export async function getExpressions(_: Request, res: Response): Promise<void> {
const expressions = await dataServiceProvider.ideaManager.getExpressionsForSearch();
res.send(expressions);
}
export async function deleteIdea(req: Request, res: Response): Promise<void> {
if (!(await validateIdeaIdInRequest(req, res))) {
return;
}
await dataServiceProvider.ideaManager.deleteIdea(parseInt(req.params.id, 10));
res.end();
}
export async function editIdea(req: Request, res: Response): Promise<void> {
if (!(await validateIdeaIdInRequest(req, res))) {
return;
}
if (!(await dataServiceProvider.inputValidator.validateIdeaForAdding(req.body as IdeaForAdding))) {
res.status(400);
res.end();
return;
}
const idea = req.body as IdeaForAdding;
normalizeIdea(idea);
await dataServiceProvider.ideaManager.editIdea(idea, parseInt(req.params.id, 10));
res.send(await dataServiceProvider.ideaManager.getIdea(parseInt(req.params.id, 10)));
}
export async function setSettings(req: Request, res: Response): Promise<void> {
if (!dataServiceProvider.inputValidator.validateSettings(req.body as Settings)) {
res.status(400);
res.end();
return;
}
const settings = req.body as Settings;
if ((await dataServiceProvider.settingsManager.isPracticeOnlyNotKnown()) !== settings.practiceOnlyNotKnown) {
// Reset practice manager because practiceable ideas may change after changing this setting
dataServiceProvider.practiceManager.clear();
}
await dataServiceProvider.settingsManager.setSettings(settings);
res.status(200);
res.end();
}
export async function getSettings(_: Request, res: Response): Promise<void> {
res.send(JSON.stringify(await dataServiceProvider.settingsManager.getSettings()));
}
export async function getDatabasePath(_: Request, res: Response): Promise<void> {
res.send(JSON.stringify(escape(dbCoordinator.inputPath)));
}
export async function changeDatabase(req: Request, res: Response): Promise<void> {
if (!dataServiceProvider.inputValidator.validateChangeDatabase(req.body)) {
res.status(400).end();
return;
}
const newDbCoordinator = await changeDatabaseToPath((req.body as {path: string}).path);
if (!newDbCoordinator.isValidVersion) {
res.status(400).send(JSON.stringify({error: databaseVersionErrorCode}));
return;
}
if (!newDbCoordinator.isValid) {
res.status(400).send(JSON.stringify({error: 'INVALID_REQUEST'}));
return;
}
res.end();
}
async function changeDatabaseToPath(path: string) {
const newDbCoordinator = new DatabaseCoordinator(path);
await newDbCoordinator.init();
if (newDbCoordinator.isValid && newDbCoordinator.isValidVersion) {
dataServiceProvider = newDbCoordinator.dataServiceProvider;
dbCoordinator = newDbCoordinator;
}
return newDbCoordinator;
}
export async function migrateDatabase(req: Request, res: Response): Promise<void> {
if (!dataServiceProvider.inputValidator.validateMigrateDatabase(req.body)) {
res.status(400).end();
return;
}
const {path} = (req.body as {path: string});
const {noContentUpdate} = (req.body as {noContentUpdate: boolean});
// Database to migrate
const dbCoordinatorForToMigrate = new DatabaseCoordinator(path);
await dbCoordinatorForToMigrate.init();
if (!dbCoordinatorForToMigrate.isValidPath) {
res.status(400).send(JSON.stringify({error: 'INVALID_REQUEST'}));
return;
}
// Base database
const dbCoordinatorForBaseDb = new DatabaseCoordinator(baseDatabasePath);
await dbCoordinatorForBaseDb.init();
try {
const databaseMigrator = new DatabaseMigrator(dbCoordinatorForToMigrate.databaseHandler.db,
dbCoordinatorForBaseDb.dataServiceProvider);
await databaseMigrator.migrate(noContentUpdate);
dbCoordinator = await initDatabase((req.body as {path: string}).path);
({dataServiceProvider} = dbCoordinator);
res.status(200).end();
} catch (error) {
console.error('Migration error:', error);
res.status(400).send(JSON.stringify({error: 'MIGRATION_ERROR'}));
}
}
export async function deleteAllData(_: Request, res: Response): Promise<void> {
await dataServiceProvider.reset();
res.end();
}
async function validateIdInRequest(req: Request, res: Response, manager: Manager): Promise<boolean> {
if (!validateNumberInRequest(req.params.id, res)) {
return false;
}
const id = parseInt(req.params.id, 10);
if (!(await manager.idExists(id))) {
res.status(404);
res.end();
return false;
}
return true;
}
async function validateLanguageIdInRequest(req: Request, res: Response): Promise<boolean> {
return validateIdInRequest(req, res, dataServiceProvider.languageManager);
}
async function validateIdeaIdInRequest(req: Request, res: Response): Promise<boolean> {
return validateIdInRequest(req, res, dataServiceProvider.ideaManager);
}
function validateNumberInRequest(expectedNumber: string, res: Response): boolean {
if (Number.isNaN(Number(expectedNumber))) {
res.status(400);
res.end();
return false;
}
return true;
}