diff --git a/server/app.ts b/server/app.ts index a616909..d7b2932 100644 --- a/server/app.ts +++ b/server/app.ts @@ -3,9 +3,9 @@ import auth, {BasicAuthCheckerCallback} from "http-auth" import bodyParser from "body-parser" import compress from "compression" import express, {Express, NextFunction, Request, Response} from "express" -import fs from "graceful-fs" +import {promises as fs} from "graceful-fs" -const config = require('./config').config +import {config} from "./config"; const app: Express = express(); @@ -37,26 +37,25 @@ const jsTemplateFiles = [ '/config.js' ]; -router.use(compress()); - -function serveTemplate (mimeType: string, req: Request, res: Response, next: NextFunction): void { - return fs.readFile(templateDir + '/' + req.path + '.template', 'utf8', function (err, body) { - if (err) { - return next(err); - } - - res.writeHead(200, { 'Content-Type': mimeType }); - res.end(_.template(body)({ config: config.client })); - - return null; // to suppress warning +function usePromise(f: (req: Request, res: Response) => Promise): void { + router.use((req: Request, res: Response, next: NextFunction): void => { + f(req, res).then(next).catch(next) }); } -router.use(function (req: Request, res: Response, next: NextFunction): void { +router.use(compress()); + +async function serveTemplate (mimeType: string, req: Request, res: Response): Promise { + const body = await fs.readFile(templateDir + '/' + req.path + '.template', 'utf8'); + + res.writeHead(200, { 'Content-Type': mimeType }); + res.end(_.template(body)({ config: config.client })); +} + +usePromise(async (req: Request, res: Response): Promise => { if (jsTemplateFiles.indexOf(req.path) >= 0) { - return serveTemplate('application/javascript', req, res, next); + await serveTemplate('application/javascript', req, res); } - return next(); }); router.use('/internal/admin', express.static(adminDir + '/')); diff --git a/server/jobs/scheduler.ts b/server/jobs/scheduler.ts index 40899e4..1c29370 100644 --- a/server/jobs/scheduler.ts +++ b/server/jobs/scheduler.ts @@ -34,7 +34,7 @@ export class Task { public lastRunStarted: moment.Moment | null, public lastRunDuration: number | null, public state: TaskState, - public enabled: true, + public enabled: boolean, ) {} run(): void { diff --git a/server/resources/frontendResource.js b/server/resources/frontendResource.js deleted file mode 100644 index 0a7d9b0..0000000 --- a/server/resources/frontendResource.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const fs = require('graceful-fs') - -const ErrorTypes = require('../utils/errorTypes') -const Logger = require('../logger') -const Resources = require('../utils/resources') - -const indexHtml = __dirname + '/../../client/index.html'; - -module.exports = { - render (req, res) { - const data = Resources.getData(req); - - fs.readFile(indexHtml, 'utf8', function (err, body) { - if (err) { - Logger.tag('frontend').error('Could not read file: ', indexHtml, err); - return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); - } - - return Resources.successHtml( - res, - body.replace( - /window.__nodeToken = \''+ data.token + '\'; + Resources.successHtml( + res, + body.replace( + /window.__nodeToken = \''+ data.token + '\'; { + Logger.tag('frontend').error('Could not read file: ', indexHtml, err); + return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); + }) +} diff --git a/server/resources/taskResource.js b/server/resources/taskResource.js deleted file mode 100644 index 178fe4b..0000000 --- a/server/resources/taskResource.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - -const _ = require('lodash') - -const Constraints = require('../validation/constraints') -const ErrorTypes = require('../utils/errorTypes') -const Resources = require('../utils/resources') -const Scheduler = require('../jobs/scheduler') -const Strings = require('../utils/strings') -const Validator = require('../validation/validator') - -const isValidId = Validator.forConstraint(Constraints.id); - -function toExternalTask(task) { - return { - id: task.id, - name: task.name, - description: task.description, - schedule: task.schedule, - runningSince: task.runningSince && task.runningSince.unix(), - lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), - lastRunDuration: task.lastRunDuration || undefined, - state: task.state, - enabled: task.enabled - }; -} - -function withValidTaskId(req, res, callback) { - const id = Strings.normalizeString(Resources.getData(req).id); - - if (!isValidId(id)) { - return callback({data: 'Invalid task id.', type: ErrorTypes.badRequest}); - } - - callback(null, id); -} - -function getTask(id, callback) { - const tasks = Scheduler.getTasks(); - const task = tasks[id]; - - if (!task) { - return callback({data: 'Task not found.', type: ErrorTypes.notFound}); - } - - callback(null, task); -} - -function withTask(req, res, callback) { - withValidTaskId(req, res, function (err, id) { - if (err) { - return callback(err); - } - - getTask(id, function (err, task) { - if (err) { - return callback(err); - } - - callback(null, task); - }); - }); -} - -function setTaskEnabled(req, res, enable) { - withTask(req, res, function (err, task) { - if (err) { - return Resources.error(res, err); - } - - task.enabled = !!enable; // ensure boolean - - return Resources.success(res, toExternalTask(task)); - }); -} - -module.exports = { - getAll (req, res) { - Resources.getValidRestParams('list', null, req, function (err, restParams) { - if (err) { - return Resources.error(res, err); - } - - const tasks = Resources.sort( - _.values(Scheduler.getTasks()), - ['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'], - restParams - ); - const filteredTasks = Resources.filter( - tasks, - ['id', 'name', 'schedule', 'state'], - restParams - ); - const total = filteredTasks.length; - - const pageTasks = Resources.getPageEntities(filteredTasks, restParams); - - res.set('X-Total-Count', total); - return Resources.success(res, _.map(pageTasks, toExternalTask)); - }); - }, - - run (req, res) { - withTask(req, res, function (err, task) { - if (err) { - return Resources.error(res, err); - } - - if (task.runningSince) { - return Resources.error(res, {data: 'Task already running.', type: ErrorTypes.conflict}); - } - - task.run(); - - return Resources.success(res, toExternalTask(task)); - }); - }, - - enable (req, res) { - setTaskEnabled(req, res, true); - }, - - disable (req, res) { - setTaskEnabled(req, res, false); - } -} diff --git a/server/resources/taskResource.ts b/server/resources/taskResource.ts new file mode 100644 index 0000000..d186cb4 --- /dev/null +++ b/server/resources/taskResource.ts @@ -0,0 +1,129 @@ +import _ from "lodash"; + +import CONSTRAINTS from "../validation/constraints"; +import ErrorTypes from "../utils/errorTypes"; +import * as Resources from "../utils/resources"; +import {getTasks, Task} from "../jobs/scheduler"; +import {normalizeString} from "../utils/strings"; +import {forConstraint} from "../validation/validator"; +import {Request, Response} from "express"; + +const isValidId = forConstraint(CONSTRAINTS.id, false); + +interface ExternalTask { + id: number, + name: string, + description: string, + schedule: string, + runningSince: number | null, + lastRunStarted: number | null, + lastRunDuration: number | null, + state: string, + enabled: boolean, +} + +function toExternalTask(task: Task): ExternalTask { + return { + id: task.id, + name: task.name, + description: task.description, + schedule: task.schedule, + runningSince: task.runningSince && task.runningSince.unix(), + lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), + lastRunDuration: task.lastRunDuration || null, + state: task.state, + enabled: task.enabled + }; +} + +async function withValidTaskId(req: Request): Promise { + const id = normalizeString(Resources.getData(req).id); + + if (!isValidId(id)) { + throw {data: 'Invalid task id.', type: ErrorTypes.badRequest}; + } + + return id; +} + +async function getTask(id: string): Promise { + const tasks = getTasks(); + const task = tasks[id]; + + if (!task) { + throw {data: 'Task not found.', type: ErrorTypes.notFound}; + } + + return task; +} + +async function withTask(req: Request): Promise { + const id = await withValidTaskId(req); + return await getTask(id); +} + +function setTaskEnabled(req: Request, res: Response, enable: boolean) { + withTask(req) + .then(task => { + task.enabled = enable; + Resources.success(res, toExternalTask(task)) + }) + .catch(err => Resources.error(res, err)) +} + +export function getAll (req: Request, res: Response): void { + Resources.getValidRestParams('list', null, req, function (err, restParams) { + if (err) { + return Resources.error(res, err); + } + + if (!restParams) { + return Resources.error( + res, + { + data: "Unexpected state: restParams is not set.", + type: ErrorTypes.internalError + } + ); + } + + const tasks = Resources.sort( + _.values(getTasks()), + ['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'], + restParams + ); + const filteredTasks = Resources.filter( + tasks, + ['id', 'name', 'schedule', 'state'], + restParams + ); + const total = filteredTasks.length; + + const pageTasks = Resources.getPageEntities(filteredTasks, restParams); + + res.set('X-Total-Count', total.toString(10)); + return Resources.success(res, _.map(pageTasks, toExternalTask)); + }); +} + +export function run (req: Request, res: Response): void { + withTask(req) + .then(task => { + if (task.runningSince) { + return Resources.error(res, {data: 'Task already running.', type: ErrorTypes.conflict}); + } + + task.run(); + + Resources.success(res, toExternalTask(task)); + }) + .catch(err => Resources.error(res, err)); +} + +export function enable (req: Request, res: Response): void { + setTaskEnabled(req, res, true); +} + +export function disable (req: Request, res: Response): void { + setTaskEnabled(req, res, false); +} diff --git a/server/resources/versionResource.js b/server/resources/versionResource.js deleted file mode 100644 index d323afb..0000000 --- a/server/resources/versionResource.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const Resources = require('../utils/resources') -const version = require('../config').version - -module.exports = { - get (req, res) { - return Resources.success( - res, - { - version - } - ); - } -} diff --git a/server/resources/versionResource.ts b/server/resources/versionResource.ts new file mode 100644 index 0000000..57d7091 --- /dev/null +++ b/server/resources/versionResource.ts @@ -0,0 +1,12 @@ +import {success} from "../utils/resources"; +import {version} from "../config"; +import {Request, Response} from "express"; + +export function get (req: Request, res: Response): void { + success( + res, + { + version + } + ); +} diff --git a/server/router.ts b/server/router.ts index b799630..d959732 100644 --- a/server/router.ts +++ b/server/router.ts @@ -3,12 +3,12 @@ import express from "express" import app from "./app" import {config} from "./config" -import VersionResource from "./resources/versionResource" +import * as VersionResource from "./resources/versionResource" import StatisticsResource from "./resources/statisticsResource" -import FrontendResource from "./resources/frontendResource" +import * as FrontendResource from "./resources/frontendResource" import NodeResource from "./resources/nodeResource" import MonitoringResource from "./resources/monitoringResource" -import TaskResource from "./resources/taskResource" +import * as TaskResource from "./resources/taskResource" import MailResource from "./resources/mailResource" export function init (): void { diff --git a/server/utils/resources.ts b/server/utils/resources.ts index a2faa9d..4cc275e 100644 --- a/server/utils/resources.ts +++ b/server/utils/resources.ts @@ -116,7 +116,7 @@ export function getData (req: Request): any { // TODO: Promisify. export function getValidRestParams( type: string, - subtype: string, + subtype: string | null, req: Request, callback: (err: {data: any, type: {code: number}} | null, restParams?: RestParams) => void ) { @@ -155,7 +155,7 @@ export function getValidRestParams( callback(null, restParams as RestParams); } -export function filter (entities: {[key: string]: Entity}, allowedFilterFields: string[], restParams: RestParams) { +export function filter (entities: ArrayLike, allowedFilterFields: string[], restParams: RestParams) { let query = restParams.q; if (query) { query = _.toLower(query.trim()); @@ -225,11 +225,11 @@ export function sort(entities: ArrayLike, allowedSortFields: string[], res } } -export function getPageEntities (entities: Entity[], restParams: RestParams) { +export function getPageEntities (entities: ArrayLike, restParams: RestParams) { const page = restParams._page; const perPage = restParams._perPage; - return entities.slice((page - 1) * perPage, page * perPage); + return _.slice(entities, (page - 1) * perPage, page * perPage); } export {filterCondition as whereCondition};