diff --git a/.eslintrc b/.eslintrc index 74de9c8..ad0902a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,18 +1,25 @@ { "extends": [ - "codex" + "codex/ts", + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], "plugins": [ - "chai-friendly" + "chai-friendly", + "@typescript-eslint" ], "env": { "mocha": true }, "rules": { - "no-unused-expressions": 0, - "chai-friendly/no-unused-expressions": 2 + "no-unused-expressions": 1, + "chai-friendly/no-unused-expressions": 2, + "@typescript-eslint/ban-types": 1, + "@typescript-eslint/no-magic-numbers": 0, + "@typescript-eslint/no-explicit-any": 1 }, - "parser": "babel-eslint", + "parser": "@typescript-eslint/parser", "globals": { "fetch": true, "alert": true diff --git a/.gitignore b/.gitignore index d9bc62b..fcc7825 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ typings/ # Uploads /public/uploads /public/uploads_test + +# Compiled files +/dist/* \ No newline at end of file diff --git a/README.md b/README.md index 18bfd6c..96164d5 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ $ yarn install --frozen-lockfile ``` ### Available scripts +#### Compile to Javascript +``` +$ yarn compile +``` #### Start the server diff --git a/config/development.json b/config/development.json index c91a562..fc7ab53 100644 --- a/config/development.json +++ b/config/development.json @@ -1,6 +1,7 @@ { "port": 3000, "database": ".db", + "rcFile": "./.codexdocsrc", "uploads": "public/uploads", "secret": "iamasecretstring" } diff --git a/config/index.js b/config/index.js deleted file mode 100644 index 3e50721..0000000 --- a/config/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * This module reads configuration file depending on NODE_ENV - * - * @type {module} - */ - -const fs = require('fs'); -const path = require('path'); -const NODE_ENV = process.env.NODE_ENV || 'development'; -const configPath = `./${NODE_ENV}.json`; -let config; - -if (fs.existsSync(path.resolve(__dirname, configPath))) { - config = require(configPath); -} else { - config = { - database: '.db', - port: 3000, - uploads: 'public/uploads', - secret: 'secret' - }; -} - -module.exports = config; diff --git a/config/production.json b/config/production.json index c91a562..fc7ab53 100644 --- a/config/production.json +++ b/config/production.json @@ -1,6 +1,7 @@ { "port": 3000, "database": ".db", + "rcFile": "./.codexdocsrc", "uploads": "public/uploads", "secret": "iamasecretstring" } diff --git a/config/testing.json b/config/testing.json index 0c1ec88..25371d0 100644 --- a/config/testing.json +++ b/config/testing.json @@ -1,7 +1,7 @@ { "port": 3001, "database": ".testdb", - "rcFile": "./test/.codexdocsrc", + "rcFile": "./src/test/.codexdocsrc", "uploads": "public/uploads_test", "secret": "iamasecretstring" } diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod index 581144f..0a95cea 100644 --- a/docker/Dockerfile.prod +++ b/docker/Dockerfile.prod @@ -9,4 +9,6 @@ RUN yarn install --prod COPY . . +RUN yarn compile + CMD ["yarn", "start"] diff --git a/nodemon.json b/nodemon.json index 2b0661c..6a2a5ac 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,11 +1,15 @@ { - "ignore": [ - "node_modules", - "src/frontend", - "public/dist" - ], - "events": { - "restart": "echo \"App restarted due to: '$FILENAME'\"" - }, - "ext": "js,twig" -} + "ignore": [ + "node_modules", + "src/frontend", + "public/dist" + ], + "events": { + "restart": "echo \"App restarted due to: '$FILENAME'\"" + }, + "watch": ["src/"], + "execMap": { + "ts": "node -r ts-node/register" + }, + "ext": "js,json,ts,twig" +} \ No newline at end of file diff --git a/package.json b/package.json index 55d05ca..834c017 100644 --- a/package.json +++ b/package.json @@ -9,40 +9,46 @@ "> 1%" ], "scripts": { - "start": "cross-env NODE_ENV=production nodemon ./bin/www", - "start:dev": "cross-env NODE_ENV=development nodemon ./bin/www", - "test": "cross-env NODE_ENV=testing mocha --recursive ./test", - "lint": "eslint --fix --cache ./src/**/*.js", + "start": "cross-env NODE_ENV=production nodemon --config nodemon.json ./dist/bin/server.js", + "start:ts": "cross-env NODE_ENV=production nodemon --config nodemon.json ./src/bin/server.ts", + "start:dev": "cross-env NODE_ENV=development nodemon --config nodemon.json ./src/bin/server.ts", + "test": "cross-env NODE_ENV=testing mocha --recursive ./dist/test --exit", + "test:ts": "cross-env NODE_ENV=testing ts-mocha ./src/test/*.ts ./src/test/**/*.ts --exit", + "lint": "eslint --fix --cache --ext .ts ./src/backend", "build": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --mode=production", "build:dev": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --mode=development --watch", - "precommit": "yarn lint && yarn test --exit", - "generatePassword": "node ./generatePassword.js", - "editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest" + "precommit": "yarn lint && yarn test:ts", + "generatePassword:ts": "ts-node ./src/generatePassword.ts", + "generatePassword": "node ./dist/generatePassword.js", + "editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest", + "compile": "npx tsc" }, "dependencies": { "@editorjs/embed": "^2.5.0", "bcrypt": "^5.0.1", - "commander": "^2.19.0", - "cookie-parser": "~1.4.3", - "cross-env": "^5.2.0", - "csurf": "^1.9.0", - "debug": "~4.1.0", - "dotenv": "^6.2.0", - "express": "~4.16.0", - "file-type": "^10.7.1", - "http-errors": "~1.7.1", - "jsonwebtoken": "^8.4.0", - "mime": "^2.4.0", - "mkdirp": "^0.5.1", - "morgan": "~1.9.0", - "multer": "^1.3.1", + "commander": "^8.1.0", + "config": "^3.3.6", + "cookie-parser": "^1.4.5", + "cross-env": "^7.0.3", + "csurf": "^1.11.0", + "debug": "^4.3.2", + "dotenv": "^10.0.0", + "express": "^4.17.1", + "file-type": "^16.5.2", + "http-errors": "^1.8.0", + "jsonwebtoken": "^8.5.1", + "mime": "^2.5.2", + "mkdirp": "^1.0.4", + "morgan": "^1.10.0", + "multer": "^1.4.2", "nedb": "^1.8.0", "node-fetch": "^2.6.1", - "nodemon": "^1.18.3", - "open-graph-scraper": "^4.5.0", - "twig": "~1.12.0", + "nodemon": "^2.0.12", + "open-graph-scraper": "^4.9.0", + "ts-node": "^10.1.0", + "twig": "^1.15.4", "typescript-eslint": "^0.0.1-alpha.0", - "uuid4": "^1.0.0" + "uuid4": "^2.0.2" }, "devDependencies": { "@babel/core": "^7.0.0", @@ -63,6 +69,31 @@ "@editorjs/raw": "^2.3.0", "@editorjs/table": "^2.0.1", "@editorjs/warning": "^1.2.0", + "@types/bcrypt": "^5.0.0", + "@types/chai": "^4.2.21", + "@types/commander": "^2.12.2", + "@types/config": "^0.0.39", + "@types/cookie-parser": "^1.4.2", + "@types/csurf": "^1.11.2", + "@types/debug": "^4.1.7", + "@types/eslint": "^7.28.0", + "@types/express": "^4.17.13", + "@types/file-type": "^10.9.1", + "@types/jsonwebtoken": "^8.5.4", + "@types/mime": "^2.0.3", + "@types/mkdirp": "^1.0.2", + "@types/mocha": "^9.0.0", + "@types/morgan": "^1.9.3", + "@types/multer": "^1.4.7", + "@types/nedb": "^1.8.12", + "@types/node": "^16.4.1", + "@types/node-fetch": "^2.5.12", + "@types/open-graph-scraper": "^4.8.1", + "@types/rimraf": "^3.0.1", + "@types/sinon": "^10.0.2", + "@types/twig": "^1.12.6", + "@typescript-eslint/eslint-plugin": "^4.28.5", + "@typescript-eslint/parser": "^4.28.5", "autoprefixer": "^9.1.3", "babel": "^6.23.0", "babel-eslint": "^10.0.1", @@ -71,17 +102,16 @@ "chai-http": "^4.0.0", "css-loader": "^1.0.0", "cssnano": "^4.1.0", - "eslint": "^6.8.0", - "eslint-config-codex": "^1.3.4", + "eslint": "^7.31.0", + "eslint-config-codex": "^1.6.2", "eslint-plugin-chai-friendly": "^0.4.1", "eslint-plugin-import": "^2.14.0", "eslint-plugin-node": "^8.0.1", - "eslint-plugin-standard": "^4.0.0", "highlight.js": "^11.1.0", "husky": "^1.1.2", "mini-css-extract-plugin": "^0.4.3", "mocha": "^5.2.0", - "mocha-sinon": "^2.1.0", + "mocha-sinon": "^2.1.2", "module-dispatcher": "^2.0.0", "normalize.css": "^8.0.1", "nyc": "^13.1.0", @@ -99,8 +129,10 @@ "postcss-nested-ancestors": "^2.0.0", "postcss-nesting": "^7.0.0", "postcss-smart-import": "^0.7.6", - "rimraf": "^2.6.3", - "sinon": "^7.0.0", + "rimraf": "^3.0.2", + "sinon": "^11.1.2", + "ts-mocha": "^8.0.0", + "typescript": "^4.3.5", "webpack": "^4.17.1", "webpack-cli": "^3.1.0" } diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 8ef3210..0000000 --- a/src/app.js +++ /dev/null @@ -1,42 +0,0 @@ -const createError = require('http-errors'); -const express = require('express'); -const path = require('path'); -const cookieParser = require('cookie-parser'); -const logger = require('morgan'); -const rcParser = require('./utils/rcparser'); -const routes = require('./routes'); - -const app = express(); -const config = rcParser.getConfiguration(); - -app.locals.config = config; - -// view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'twig'); -require('./utils/twig'); - -app.use(logger('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, '../public'))); - -app.use('/', routes); -// catch 404 and forward to error handler -app.use(function (req, res, next) { - next(createError(404)); -}); - -// error handler -app.use(function (err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; - - // render the error page - res.status(err.status || 500); - res.render('error'); -}); - -module.exports = app; diff --git a/src/backend/app.ts b/src/backend/app.ts new file mode 100644 index 0000000..b9f1c1c --- /dev/null +++ b/src/backend/app.ts @@ -0,0 +1,38 @@ +import express, { Request, Response } from 'express'; +import path from 'path'; +import cookieParser from 'cookie-parser'; +import morgan from 'morgan'; +import rcParser from './utils/rcparser'; +import routes from './routes'; +import HttpException from './exceptions/httpException'; + +const app = express(); +const config = rcParser.getConfiguration(); + +app.locals.config = config; + +// view engine setup +app.set('views', path.join(__dirname, '../../src/backend/', 'views')); +app.set('view engine', 'twig'); +require('./utils/twig'); + +app.use(morgan('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, '../../public'))); + +app.use('/', routes); + +// error handler +app.use(function (err: HttpException, req: Request, res: Response) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +export default app; \ No newline at end of file diff --git a/src/controllers/aliases.js b/src/backend/controllers/aliases.ts similarity index 75% rename from src/controllers/aliases.js rename to src/backend/controllers/aliases.ts index bdda40a..0628302 100644 --- a/src/controllers/aliases.js +++ b/src/backend/controllers/aliases.ts @@ -1,4 +1,4 @@ -const Alias = require('../models/alias'); +import Alias from '../models/alias'; /** * @class Aliases @@ -11,7 +11,7 @@ class Aliases { * @param {string} aliasName - alias name of entity * @returns {Promise} */ - static async get(aliasName) { + public static async get(aliasName: string): Promise { const alias = await Alias.get(aliasName); if (!alias.id) { @@ -22,4 +22,4 @@ class Aliases { } } -module.exports = Aliases; +export default Aliases; diff --git a/src/controllers/pages.js b/src/backend/controllers/pages.ts similarity index 68% rename from src/controllers/pages.js rename to src/backend/controllers/pages.ts index c37cf6e..6a4bd07 100644 --- a/src/controllers/pages.js +++ b/src/backend/controllers/pages.ts @@ -1,5 +1,7 @@ -const Model = require('../models/page'); -const Alias = require('../models/alias'); +import Page, { PageData } from '../models/page'; +import Alias from '../models/alias'; + +type PageDataFields = keyof PageData; /** * @class Pages @@ -11,7 +13,7 @@ class Pages { * * @returns {['title', 'body']} */ - static get REQUIRED_FIELDS() { + public static get REQUIRED_FIELDS(): Array { return [ 'body' ]; } @@ -21,8 +23,8 @@ class Pages { * @param {string} id - page id * @returns {Promise} */ - static async get(id) { - const page = await Model.get(id); + public static async get(id: string): Promise { + const page = await Page.get(id); if (!page._id) { throw new Error('Page with given id does not exist'); @@ -36,8 +38,8 @@ class Pages { * * @returns {Promise} */ - static async getAll() { - return Model.getAll(); + public static async getAll(): Promise { + return Page.getAll(); } /** @@ -46,20 +48,28 @@ class Pages { * @param {string} parent - id of current page * @returns {Promise} */ - static async getAllExceptChildren(parent) { + public static async getAllExceptChildren(parent: string): Promise { const pagesAvailable = this.removeChildren(await Pages.getAll(), parent); - return pagesAvailable.filter((item) => item !== null); + const nullFilteredPages: Page[] = []; + + pagesAvailable.forEach(async item => { + if (item instanceof Page) { + nullFilteredPages.push(item); + } + }); + + return nullFilteredPages; } /** * Set all children elements to null * - * @param {Page[]} [pagesAvailable] - Array of all pages + * @param {Array} [pagesAvailable] - Array of all pages * @param {string} parent - id of parent page * @returns {Array} */ - static removeChildren(pagesAvailable, parent) { + public static removeChildren(pagesAvailable: Array, parent: string | undefined): Array { pagesAvailable.forEach(async (item, index) => { if (item === null || item._parent !== parent) { return; @@ -74,14 +84,14 @@ class Pages { /** * Create new page model and save it in the database * - * @param {PageData} data + * @param {PageData} data - info about page * @returns {Promise} */ - static async insert(data) { + public static async insert(data: PageData): Promise { try { Pages.validate(data); - const page = new Model(data); + const page = new Page(data); const insertedPage = await page.save(); @@ -95,18 +105,80 @@ class Pages { } return insertedPage; - } catch (validationError) { - throw new Error(validationError); + } catch (e) { + throw new Error('validationError'); } } + /** + * Update page with given id in the database + * + * @param {string} id - page id + * @param {PageData} data - info about page + * @returns {Promise} + */ + public static async update(id: string, data: PageData): Promise { + const page = await Page.get(id); + const previousUri = page.uri; + + if (!page._id) { + throw new Error('Page with given id does not exist'); + } + + if (data.uri && !data.uri.match(/^[a-z0-9'-]+$/i)) { + throw new Error('Uri has unexpected characters'); + } + + page.data = data; + const updatedPage = await page.save(); + + if (updatedPage.uri !== previousUri) { + if (updatedPage.uri) { + const alias = new Alias({ + id: updatedPage._id, + type: Alias.types.PAGE, + }, updatedPage.uri); + + alias.save(); + } + + if (previousUri) { + Alias.markAsDeprecated(previousUri); + } + } + + return updatedPage; + } + + /** + * Remove page with given id from the database + * + * @param {string} id - page id + * @returns {Promise} + */ + public static async remove(id: string): Promise { + const page = await Page.get(id); + + if (!page._id) { + throw new Error('Page with given id does not exist'); + } + + if (page.uri) { + const alias = await Alias.get(page.uri); + + await alias.destroy(); + } + + return page.destroy(); + } + /** * Check PageData object for required fields * - * @param {PageData} data + * @param {PageData} data - info about page * @throws {Error} - validation error */ - static validate(data) { + private static validate(data: PageData): void { const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined'); if (!allRequiredFields) { @@ -131,64 +203,6 @@ class Pages { throw new Error('Please, fill page Header'); } } - - /** - * Update page with given id in the database - * - * @param {string} id - page id - * @param {PageData} data - * @returns {Promise} - */ - static async update(id, data) { - const page = await Model.get(id); - const previousUri = page.uri; - - if (!page._id) { - throw new Error('Page with given id does not exist'); - } - - if (data.uri && !data.uri.match(/^[a-z0-9'-]+$/i)) { - throw new Error('Uri has unexpected characters'); - } - - page.data = data; - const updatedPage = await page.save(); - - if (updatedPage.uri !== previousUri) { - if (updatedPage.uri) { - const alias = new Alias({ - id: updatedPage._id, - type: Alias.types.PAGE, - }, updatedPage.uri); - - alias.save(); - } - - Alias.markAsDeprecated(previousUri); - } - - return updatedPage; - } - - /** - * Remove page with given id from the database - * - * @param {string} id - page id - * @returns {Promise} - */ - static async remove(id) { - const page = await Model.get(id); - - if (!page._id) { - throw new Error('Page with given id does not exist'); - } - - const alias = await Alias.get(page.uri); - - await alias.destroy(); - - return page.destroy(); - } } -module.exports = Pages; +export default Pages; diff --git a/src/controllers/pagesOrder.js b/src/backend/controllers/pagesOrder.ts similarity index 62% rename from src/controllers/pagesOrder.js rename to src/backend/controllers/pagesOrder.ts index eeb88a5..f084af3 100644 --- a/src/controllers/pagesOrder.js +++ b/src/backend/controllers/pagesOrder.ts @@ -1,4 +1,5 @@ -const Model = require('../models/pageOrder'); +import PageOrder from '../models/pageOrder'; +import Page from '../models/page'; /** * @class PagesOrder @@ -13,8 +14,8 @@ class PagesOrder { * @param {string} parentId - of which page we want to get children order * @returns {Promise} */ - static async get(parentId) { - const order = await Model.get(parentId); + public static async get(parentId: string): Promise { + const order = await PageOrder.get(parentId); if (!order._id) { throw new Error('Page with given id does not contain order'); @@ -26,10 +27,10 @@ class PagesOrder { /** * Returns all records about page's order * - * @returns {Promise} + * @returns {Promise} */ - static async getAll() { - return Model.getAll(); + public static async getAll(): Promise { + return PageOrder.getAll(); } /** @@ -38,8 +39,8 @@ class PagesOrder { * @param {string} parentId - parent page's id * @param {string} childId - new page pushed to the order */ - static async push(parentId, childId) { - const order = await Model.get(parentId); + public static async push(parentId: string, childId: string): Promise { + const order = await PageOrder.get(parentId); order.push(childId); await order.save(); @@ -52,13 +53,13 @@ class PagesOrder { * @param {string} newParentId - new parent page's id * @param {string} targetPageId - page's id which is changing the parent page */ - static async move(oldParentId, newParentId, targetPageId) { - const oldParentOrder = await Model.get(oldParentId); + public static async move(oldParentId: string, newParentId: string, targetPageId: string): Promise { + const oldParentOrder = await PageOrder.get(oldParentId); oldParentOrder.remove(targetPageId); await oldParentOrder.save(); - const newParentOrder = await Model.get(newParentId); + const newParentOrder = await PageOrder.get(newParentId); newParentOrder.push(targetPageId); await newParentOrder.save(); @@ -73,14 +74,14 @@ class PagesOrder { * @param {boolean} ignoreSelf - should we ignore current page in list or not * @returns {Page[]} */ - static async getOrderedChildren(pages, currentPageId, parentPageId, ignoreSelf = false) { - const children = await Model.get(parentPageId); + public static async getOrderedChildren(pages: Page[], currentPageId: string, parentPageId: string, ignoreSelf = false): Promise { + const children = await PageOrder.get(parentPageId); const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id); // Create unique array with ordered and unordered pages id - const ordered = [ ...new Set([...children.order, ...unordered]) ]; + const ordered = Array.from(new Set([...children.order, ...unordered])); - const result = []; + const result: Page[] = []; ordered.forEach(pageId => { pages.forEach(page => { @@ -94,26 +95,26 @@ class PagesOrder { } /** - * @param {string[]} unordered + * @param {string[]} unordered - list of pages * @param {string} currentPageId - page's id that changes the order * @param {string} parentPageId - parent page's id that contains both two pages * @param {string} putAbovePageId - page's id above which we put the target page */ - static async update(unordered, currentPageId, parentPageId, putAbovePageId) { - const pageOrder = await Model.get(parentPageId); + public static async update(unordered: string[], currentPageId: string, parentPageId: string, putAbovePageId: string): Promise { + const pageOrder = await PageOrder.get(parentPageId); // Create unique array with ordered and unordered pages id - pageOrder.order = [ ...new Set([...pageOrder.order, ...unordered]) ]; + pageOrder.order = Array.from(new Set([...pageOrder.order, ...unordered])); pageOrder.putAbove(currentPageId, putAbovePageId); await pageOrder.save(); } /** - * @param parentId + * @param {string} parentId - identity of parent page * @returns {Promise} */ - static async remove(parentId) { - const order = await Model.get(parentId); + public static async remove(parentId: string): Promise { + const order = await PageOrder.get(parentId); if (!order._id) { throw new Error('Page with given id does not contain order'); @@ -123,4 +124,4 @@ class PagesOrder { } } -module.exports = PagesOrder; +export default PagesOrder; diff --git a/src/controllers/transport.js b/src/backend/controllers/transport.ts similarity index 64% rename from src/controllers/transport.js rename to src/backend/controllers/transport.ts index a35a1aa..b8b773e 100644 --- a/src/controllers/transport.js +++ b/src/backend/controllers/transport.ts @@ -1,12 +1,17 @@ -const fileType = require('file-type'); -const fetch = require('node-fetch'); -const fs = require('fs'); -const nodePath = require('path'); +import fileType from 'file-type'; +import fetch from 'node-fetch'; +import fs from 'fs'; +import nodePath from 'path'; +import config from 'config'; +import File, { FileData } from '../models/file'; +import crypto from '../utils/crypto'; +import deepMerge from '../utils/objects'; -const Model = require('../models/file'); -const { random16 } = require('../utils/crypto'); -const { deepMerge } = require('../utils/objects'); -const config = require('../../config'); +const random16 = crypto.random16; + +interface Dict { + [key: string]: any; +} /** * @class Transport @@ -28,10 +33,10 @@ class Transport { * @param {object} map - object that represents how should fields of File object should be mapped to response * @returns {Promise} */ - static async save(multerData, map) { + public static async save(multerData: Dict, map: Dict): Promise { const { originalname: name, path, filename, size, mimetype } = multerData; - const file = new Model({ + const file = new File({ name, filename, path, @@ -57,22 +62,33 @@ class Transport { * @param {object} map - object that represents how should fields of File object should be mapped to response * @returns {Promise} */ - static async fetch(url, map) { + public static async fetch(url: string, map: Dict): Promise { const fetchedFile = await fetch(url); const buffer = await fetchedFile.buffer(); const filename = await random16(); - const type = fileType(buffer); + const type = await fileType.fromBuffer(buffer); const ext = type ? type.ext : nodePath.extname(url).slice(1); - fs.writeFileSync(`${config.uploads}/${filename}.${ext}`, buffer); + fs.writeFileSync(`${config.get('uploads')}/${filename}.${ext}`, buffer); - const file = new Model({ + const fetchedContentType: string | null = fetchedFile.headers.get('content-type'); + let fetchedMimeType: string|undefined; + + if (fetchedContentType === null) { + fetchedMimeType = undefined; + } else { + fetchedMimeType = fetchedContentType; + } + + const mimeType = type ? type.mime : fetchedMimeType; + + const file = new File({ name: url, filename: `${filename}.${ext}`, - path: `${config.uploads}/${filename}.${ext}`, + path: `${config.get('uploads')}/${filename}.${ext}`, size: buffer.length, - mimetype: type ? type.mime : fetchedFile.headers.get('content-type'), + mimetype: mimeType, }); await file.save(); @@ -89,19 +105,19 @@ class Transport { /** * Map fields of File object to response by provided map object * - * @param {File} file + * @param {File} file - file object * @param {object} map - object that represents how should fields of File object should be mapped to response * */ - static composeResponse(file, map) { - const response = {}; + public static composeResponse(file: File, map: Dict): Dict { + const response: Dict = {}; const { data } = file; Object.entries(map).forEach(([name, path]) => { - const fields = path.split(':'); + const fields: string[] = path.split(':'); if (fields.length > 1) { - let object = {}; + let object: Dict = {}; const result = object; fields.forEach((field, i) => { @@ -125,4 +141,4 @@ class Transport { } } -module.exports = Transport; +export default Transport; diff --git a/src/backend/controllers/users.ts b/src/backend/controllers/users.ts new file mode 100644 index 0000000..549cad5 --- /dev/null +++ b/src/backend/controllers/users.ts @@ -0,0 +1,20 @@ +import User from '../models/user'; + +/** + * @class Users + * @classdesc Users controller + */ +class Users { + /** + * Find and return user model. + * + * @returns {Promise} + */ + public static async get(): Promise { + const userData: User = await User.get(); + + return userData; + } +} + +export default Users; diff --git a/src/backend/exceptions/httpException.ts b/src/backend/exceptions/httpException.ts new file mode 100644 index 0000000..f13e7a2 --- /dev/null +++ b/src/backend/exceptions/httpException.ts @@ -0,0 +1,21 @@ +/** + * HttpException class for middleware + * + * @property {number} status - exception status code + * @property {string} message - detail about the exception + */ +class HttpException extends Error { + public status: number; + public message: string; + /** + * @param status - status of the exception + * @param message - message about the exception + */ + constructor(status: number, message: string) { + super(message); + this.status = status; + this.message = message; + } +} + +export default HttpException; \ No newline at end of file diff --git a/src/models/alias.js b/src/backend/models/alias.ts similarity index 70% rename from src/models/alias.js rename to src/backend/models/alias.ts index 5090d08..58d8142 100644 --- a/src/models/alias.js +++ b/src/backend/models/alias.ts @@ -1,5 +1,8 @@ -const { aliases: aliasesDb } = require('../utils/database/index'); -const { binaryMD5 } = require('../utils/crypto'); +import crypto from '../utils/crypto'; +import database from '../utils/database/index'; + +const binaryMD5 = crypto.binaryMD5; +const aliasesDb = database['aliases']; /** * @typedef {object} AliasData @@ -10,6 +13,13 @@ const { binaryMD5 } = require('../utils/crypto'); * @property {string} id - entity id * */ +export interface AliasData { + _id?: string; + hash?: string; + type?: string; + deprecated?: boolean; + id?: string; +} /** * @class Alias @@ -22,16 +32,40 @@ const { binaryMD5 } = require('../utils/crypto'); * @property {string} id - entity title */ class Alias { + public _id?: string; + public hash?: string; + public type?: string; + public deprecated?: boolean; + public id?: string; + + /** + * @class + * + * @param {AliasData} data - info about alias + * @param {string} aliasName - alias of entity + */ + constructor(data: AliasData = {}, aliasName = '') { + if (data === null) { + data = {}; + } + if (data._id) { + this._id = data._id; + } + if (aliasName) { + this.hash = binaryMD5(aliasName); + } + this.data = data; + } /** * Return Alias types * * @returns {object} */ - static get types() { + public static get types(): { PAGE: string } { return { PAGE: 'page', }; - }; + } /** * Find and return alias with given alias @@ -39,7 +73,7 @@ class Alias { * @param {string} aliasName - alias of entity * @returns {Promise} */ - static async get(aliasName) { + public static async get(aliasName: string): Promise { const hash = binaryMD5(aliasName); let data = await aliasesDb.findOne({ hash: hash, @@ -54,22 +88,17 @@ class Alias { } /** - * @class + * Mark alias as deprecated * - * @param {AliasData} data * @param {string} aliasName - alias of entity + * @returns {Promise} */ - constructor(data = {}, aliasName = '') { - if (data === null) { - data = {}; - } - if (data._id) { - this._id = data._id; - } - if (aliasName) { - this.hash = binaryMD5(aliasName); - } - this.data = data; + public static async markAsDeprecated(aliasName: string): Promise { + const alias = await Alias.get(aliasName); + + alias.deprecated = true; + + return alias.save(); } /** @@ -77,9 +106,9 @@ class Alias { * * @returns {Promise} */ - async save() { + public async save(): Promise { if (!this._id) { - const insertedRow = await aliasesDb.insert(this.data); + const insertedRow = await aliasesDb.insert(this.data) as { _id: string }; this._id = insertedRow._id; } else { @@ -92,9 +121,9 @@ class Alias { /** * Set AliasData object fields to internal model fields * - * @param {AliasData} aliasData + * @param {AliasData} aliasData - info about alias */ - set data(aliasData) { + public set data(aliasData: AliasData) { const { id, type, hash, deprecated } = aliasData; this.id = id || this.id; @@ -108,7 +137,7 @@ class Alias { * * @returns {AliasData} */ - get data() { + public get data(): AliasData { return { _id: this._id, id: this.id, @@ -119,23 +148,9 @@ class Alias { } /** - * Mark alias as deprecated - * - * @param {string} aliasName - alias of entity * @returns {Promise} */ - static async markAsDeprecated(aliasName) { - const alias = await Alias.get(aliasName); - - alias.deprecated = true; - - return alias.save(); - } - - /** - * @returns {Promise} - */ - async destroy() { + public async destroy(): Promise { await aliasesDb.remove({ _id: this._id }); delete this._id; @@ -144,4 +159,4 @@ class Alias { } } -module.exports = Alias; +export default Alias; diff --git a/src/models/file.js b/src/backend/models/file.ts similarity index 68% rename from src/models/file.js rename to src/backend/models/file.ts index af2a928..3ed44ee 100644 --- a/src/models/file.js +++ b/src/backend/models/file.ts @@ -1,4 +1,6 @@ -const { files: filesDb } = require('../utils/database/index'); +import database from '../utils/database/index'; + +const filesDb = database['files']; /** * @typedef {object} FileData @@ -10,6 +12,15 @@ const { files: filesDb } = require('../utils/database/index'); * @property {string} mimetype - file MIME type * @property {number} size - size of the file in */ +export interface FileData { + _id?: string; + name?: string; + filename?: string; + path?: string; + mimetype?: string; + size?: number; + [key: string]: string | number | undefined; +} /** * @class File @@ -23,48 +34,19 @@ const { files: filesDb } = require('../utils/database/index'); * @property {number} size - size of the file in */ class File { - /** - * Find and return model of file with given id - * - * @param {string} _id - file id - * @returns {Promise} - */ - static async get(_id) { - const data = await filesDb.findOne({ _id }); - - return new File(data); - } - - /** - * Find and return model of file with given id - * - * @param {string} filename - uploaded filename - * @returns {Promise} - */ - static async getByFilename(filename) { - const data = await filesDb.findOne({ filename }); - - return new File(data); - } - - /** - * Find all files which match passed query object - * - * @param {object} query - * @returns {Promise} - */ - static async getAll(query = {}) { - const docs = await filesDb.find(query); - - return Promise.all(docs.map(doc => new File(doc))); - } + public _id?: string; + public name?: string; + public filename?: string; + public path?: string; + public mimetype?: string; + public size?: number; /** * @class * - * @param {FileData} data + * @param {FileData} data - info about file */ - constructor(data = {}) { + constructor(data: FileData = {}) { if (data === null) { data = {}; } @@ -75,13 +57,48 @@ class File { this.data = data; } + /** + * Find and return model of file with given id + * + * @param {string} _id - file id + * @returns {Promise} + */ + public static async get(_id: string): Promise { + const data: FileData = await filesDb.findOne({ _id }); + + return new File(data); + } + + /** + * Find and return model of file with given id + * + * @param {string} filename - uploaded filename + * @returns {Promise} + */ + public static async getByFilename(filename: string): Promise { + const data = await filesDb.findOne({ filename }); + + return new File(data); + } + + /** + * Find all files which match passed query object + * + * @param {object} query - input query + * @returns {Promise} + */ + public static async getAll(query: Record = {}): Promise { + const docs = await filesDb.find(query); + + return Promise.all(docs.map(doc => new File(doc))); + } /** * Set FileData object fields to internal model fields * - * @param {FileData} fileData + * @param {FileData} fileData - info about file */ - set data(fileData) { + public set data(fileData: FileData) { const { name, filename, path, mimetype, size } = fileData; this.name = name || this.name; @@ -96,7 +113,7 @@ class File { * * @returns {FileData} */ - get data() { + public get data(): FileData { return { _id: this._id, name: this.name, @@ -112,9 +129,9 @@ class File { * * @returns {Promise} */ - async save() { + public async save(): Promise { if (!this._id) { - const insertedRow = await filesDb.insert(this.data); + const insertedRow = await filesDb.insert(this.data) as { _id: string }; this._id = insertedRow._id; } else { @@ -129,7 +146,7 @@ class File { * * @returns {Promise} */ - async destroy() { + public async destroy(): Promise { await filesDb.remove({ _id: this._id }); delete this._id; @@ -137,24 +154,24 @@ class File { return this; } - /** - * Removes unnecessary public folder prefix - * - * @param {string} path - * @returns {string} - */ - processPath(path) { - return path.replace(/^public/, ''); - } - /** * Return readable file data * * @returns {FileData} */ - toJSON() { + public toJSON(): FileData { return this.data; } + + /** + * Removes unnecessary public folder prefix + * + * @param {string} path - input path to be processed + * @returns {string} + */ + private processPath(path: string): string { + return path.replace(/^public/, ''); + } } -module.exports = File; +export default File; diff --git a/src/models/page.js b/src/backend/models/page.ts similarity index 65% rename from src/models/page.js rename to src/backend/models/page.ts index 87e2f8d..d90b186 100644 --- a/src/models/page.js +++ b/src/backend/models/page.ts @@ -1,5 +1,7 @@ -const urlify = require('../utils/urlify'); -const { pages: pagesDb } = require('../utils/database/index'); +import urlify from '../utils/urlify'; +import database from '../utils/database/index'; + +const pagesDb = database['pages']; /** * @typedef {object} PageData @@ -9,6 +11,13 @@ const { pages: pagesDb } = require('../utils/database/index'); * @property {*} body - page body * @property {string} parent - id of parent page */ +export interface PageData { + _id?: string; + title?: string; + uri?: string; + body?: any; + parent?: string; +} /** * @class Page @@ -21,48 +30,18 @@ const { pages: pagesDb } = require('../utils/database/index'); * @property {string} _parent - id of parent page */ class Page { - /** - * Find and return model of page with given id - * - * @param {string} _id - page id - * @returns {Promise} - */ - static async get(_id) { - const data = await pagesDb.findOne({ _id }); - - return new Page(data); - } - - /** - * Find and return model of page with given uri - * - * @param {string} uri - page uri - * @returns {Promise} - */ - static async getByUri(uri) { - const data = await pagesDb.findOne({ uri }); - - return new Page(data); - } - - /** - * Find all pages which match passed query object - * - * @param {object} query - * @returns {Promise} - */ - static async getAll(query = {}) { - const docs = await pagesDb.find(query); - - return Promise.all(docs.map(doc => new Page(doc))); - } + public _id?: string; + public body?: any; + public title?: string; + public uri?: string; + public _parent?: string; /** * @class * - * @param {PageData} data + * @param {PageData} data - page's data */ - constructor(data = {}) { + constructor(data: PageData = {}) { if (data === null) { data = {}; } @@ -74,12 +53,48 @@ class Page { this.data = data; } + /** + * Find and return model of page with given id + * + * @param {string} _id - page id + * @returns {Promise} + */ + public static async get(_id: string): Promise { + const data = await pagesDb.findOne({ _id }); + + return new Page(data); + } + + /** + * Find and return model of page with given uri + * + * @param {string} uri - page uri + * @returns {Promise} + */ + public static async getByUri(uri: string): Promise { + const data = await pagesDb.findOne({ uri }); + + return new Page(data); + } + + /** + * Find all pages which match passed query object + * + * @param {object} query - input query + * @returns {Promise} + */ + public static async getAll(query: Record = {}): Promise { + const docs = await pagesDb.find(query); + + return Promise.all(docs.map(doc => new Page(doc))); + } + /** * Set PageData object fields to internal model fields * - * @param {PageData} pageData + * @param {PageData} pageData - page's data */ - set data(pageData) { + public set data(pageData: PageData) { const { body, parent, uri } = pageData; this.body = body || this.body; @@ -93,7 +108,7 @@ class Page { * * @returns {PageData} */ - get data() { + public get data(): PageData { return { _id: this._id, title: this.title, @@ -103,32 +118,12 @@ class Page { }; } - /** - * Extract first header from editor data - * - * @returns {string} - */ - extractTitleFromBody() { - const headerBlock = this.body ? this.body.blocks.find(block => block.type === 'header') : ''; - - return headerBlock ? headerBlock.data.text : ''; - } - - /** - * Transform title for uri - * - * @returns {string} - */ - transformTitleToUri() { - return urlify(this.title); - } - /** * Link given page as parent * - * @param {Page} parentPage + * @param {Page} parentPage - the page to be set as parent */ - set parent(parentPage) { + public set parent(parentPage: Page) { this._parent = parentPage._id; } @@ -137,9 +132,10 @@ class Page { * * @returns {Promise} */ - get parent() { - return pagesDb.findOne({ _id: this._parent }) - .then(data => new Page(data)); + public async getParent(): Promise { + const data = await pagesDb.findOne({ _id: this._parent }); + + return new Page(data); } /** @@ -147,9 +143,11 @@ class Page { * * @returns {Promise} */ - get children() { + public get children(): Promise { return pagesDb.find({ parent: this._id }) - .then(data => data.map(page => new Page(page))); + .then(data => { + return data.map(page => new Page(page)); + }); } /** @@ -157,11 +155,13 @@ class Page { * * @returns {Promise} */ - async save() { - this.uri = await this.composeUri(this.uri); + public async save(): Promise { + if (this.uri !== undefined) { + this.uri = await this.composeUri(this.uri); + } if (!this._id) { - const insertedRow = await pagesDb.insert(this.data); + const insertedRow = await pagesDb.insert(this.data) as { _id: string }; this._id = insertedRow._id; } else { @@ -176,7 +176,7 @@ class Page { * * @returns {Promise} */ - async destroy() { + public async destroy(): Promise { await pagesDb.remove({ _id: this._id }); delete this._id; @@ -184,13 +184,22 @@ class Page { return this; } + /** + * Return readable page data + * + * @returns {PageData} + */ + public toJSON(): PageData { + return this.data; + } + /** * Find and return available uri * * @returns {Promise} - * @param uri + * @param uri - input uri to be composed */ - async composeUri(uri) { + private async composeUri(uri: string): Promise { let pageWithSameUriCount = 0; if (!this._id) { @@ -210,13 +219,28 @@ class Page { } /** - * Return readable page data + * Extract first header from editor data * - * @returns {PageData} + * @returns {string} */ - toJSON() { - return this.data; + private extractTitleFromBody(): string { + const headerBlock = this.body ? this.body.blocks.find((block: Record) => block.type === 'header') : ''; + + return headerBlock ? headerBlock.data.text : ''; + } + + /** + * Transform title for uri + * + * @returns {string} + */ + private transformTitleToUri(): string { + if (this.title === undefined) { + return ''; + } + + return urlify(this.title); } } -module.exports = Page; +export default Page; diff --git a/src/models/pageOrder.js b/src/backend/models/pageOrder.ts similarity index 58% rename from src/models/pageOrder.js rename to src/backend/models/pageOrder.ts index dd8f7c8..761fc6a 100644 --- a/src/models/pageOrder.js +++ b/src/backend/models/pageOrder.ts @@ -1,4 +1,6 @@ -const { pagesOrder: db } = require('../utils/database/index'); +import database from '../utils/database/index'; + +const db = database['pagesOrder']; /** * @typedef {object} PageOrderData @@ -6,6 +8,11 @@ const { pagesOrder: db } = require('../utils/database/index'); * @property {string} page - page id * @property {Array} order - list of ordered pages */ +export interface PageOrderData { + _id?: string; + page?: string; + order?: string[]; +} /** * @class PageOrder @@ -14,44 +21,17 @@ const { pagesOrder: db } = require('../utils/database/index'); * Creates order for Pages with children */ class PageOrder { - /** - * Returns current Page's children order - * - * @param {string} pageId - page's id - * @returns {PageOrder} - */ - static async get(pageId) { - const order = await db.findOne({ page: pageId }); + public _id?: string; + public page?: string; + private _order?: string[]; - let data = {}; - - if (!order) { - data.page = pageId; - } else { - data = order; - } - - return new PageOrder(data); - } - - /** - * Find all pages which match passed query object - * - * @param {object} query - * @returns {Promise} - */ - static async getAll(query = {}) { - const docs = await db.find(query); - - return Promise.all(docs.map(doc => new PageOrder(doc))); - } /** * @class * - * @param {PageOrderData} data + * @param {PageOrderData} data - info about pageOrder */ - constructor(data = {}) { + constructor(data: PageOrderData = {}) { if (data === null) { data = {}; } @@ -63,14 +43,46 @@ class PageOrder { this.data = data; } + /** + * Returns current Page's children order + * + * @param {string} pageId - page's id + * @returns {Promise} + */ + public static async get(pageId: string): Promise { + const order = await db.findOne({ page: pageId }); + + let data: PageOrderData = {}; + + if (order === null) { + data.page = pageId; + } else { + data = order; + } + + return new PageOrder(data); + } + + /** + * Find all pages which match passed query object + * + * @param {object} query - input query + * @returns {Promise} + */ + public static async getAll(query: Record = {}): Promise { + const docs = await db.find(query); + + return Promise.all(docs.map(doc => new PageOrder(doc))); + } + /** * constructor data setter * - * @param {PageOrderData} pageOrderData + * @param {PageOrderData} pageOrderData - info about pageOrder */ - set data(pageOrderData) { - this._page = pageOrderData.page || 0; - this._order = pageOrderData.order || []; + public set data(pageOrderData: PageOrderData) { + this.page = pageOrderData.page || '0'; + this.order = pageOrderData.order || []; } /** @@ -78,11 +90,11 @@ class PageOrder { * * @returns {PageOrderData} */ - get data() { + public get data(): PageOrderData { return { _id: this._id, - page: '' + this._page, - order: this._order, + page: '' + this.page, + order: this.order, }; } @@ -91,9 +103,12 @@ class PageOrder { * * @param {string} pageId - page's id */ - push(pageId) { + public push(pageId: string | number): void { if (typeof pageId === 'string') { - this._order.push(pageId); + if (this.order === undefined) { + this.order = []; + } + this.order.push(pageId); } else { throw new Error('given id is not string'); } @@ -104,11 +119,15 @@ class PageOrder { * * @param {string} pageId - page's id */ - remove(pageId) { - const found = this._order.indexOf(pageId); + public remove(pageId: string): void { + if (this.order === undefined) { + return; + } + + const found = this.order.indexOf(pageId); if (found >= 0) { - this._order.splice(found, 1); + this.order.splice(found, 1); } } @@ -116,9 +135,13 @@ class PageOrder { * @param {string} currentPageId - page's id that changes the order * @param {string} putAbovePageId - page's id above which we put the target page * - * @returns void + * @returns {void} */ - putAbove(currentPageId, putAbovePageId) { + public putAbove(currentPageId: string, putAbovePageId: string): void { + if (this.order === undefined) { + return; + } + const found1 = this.order.indexOf(putAbovePageId); const found2 = this.order.indexOf(currentPageId); @@ -135,16 +158,20 @@ class PageOrder { /** * Returns page before passed page with id * - * @param {string} pageId + * @param {string} pageId - identity of page */ - getPageBefore(pageId) { + public getPageBefore(pageId: string): string | null { + if (this.order === undefined) { + return null; + } + const currentPageInOrder = this.order.indexOf(pageId); /** * If page not found or first return nothing */ if (currentPageInOrder <= 0) { - return; + return null; } return this.order[currentPageInOrder - 1]; @@ -153,16 +180,20 @@ class PageOrder { /** * Returns page before passed page with id * - * @param pageId + * @param pageId - identity of page */ - getPageAfter(pageId) { + public getPageAfter(pageId: string): string | null { + if (this.order === undefined) { + return null; + } + const currentPageInOrder = this.order.indexOf(pageId); /** * If page not found or is last */ if (currentPageInOrder === -1 || currentPageInOrder === this.order.length - 1) { - return; + return null; } return this.order[currentPageInOrder + 1]; @@ -171,7 +202,7 @@ class PageOrder { /** * @param {string[]} order - define new order */ - set order(order) { + public set order(order: string[]) { this._order = order; } @@ -180,16 +211,18 @@ class PageOrder { * * @returns {string[]} */ - get order() { - return this._order; + public get order(): string[] { + return this._order || []; } /** * Save or update page data in the database + * + * @returns {Promise} */ - async save() { + public async save(): Promise { if (!this._id) { - const insertedRow = await db.insert(this.data); + const insertedRow = await db.insert(this.data) as { _id: string}; this._id = insertedRow._id; } else { @@ -201,14 +234,14 @@ class PageOrder { /** * Remove page data from the database + * + * @returns {Promise} */ - async destroy() { + public async destroy(): Promise { await db.remove({ _id: this._id }); delete this._id; - - return this; } } -module.exports = PageOrder; +export default PageOrder; diff --git a/src/backend/models/user.ts b/src/backend/models/user.ts new file mode 100644 index 0000000..0d382bb --- /dev/null +++ b/src/backend/models/user.ts @@ -0,0 +1,40 @@ +import database from '../utils/database/index'; + +const db = database['password']; + +export interface UserData { + passHash?: string; +} + +/** + * @class User + * @class User model + * + * @property {string} passHash - hashed password + */ +class User { + public passHash?: string; + + /** + * @class + * + * @param {UserData} userData - user data for construct new object + */ + constructor(userData: UserData) { + this.passHash = userData.passHash; + } + + /** + * Find and return model of user. + * User is only one. + * + * @returns {Promise} + */ + public static async get(): Promise { + const userData: UserData = await db.findOne({}); + + return new User(userData); + } +} + +export default User; diff --git a/src/routes/aliases.js b/src/backend/routes/aliases.ts similarity index 59% rename from src/routes/aliases.js rename to src/backend/routes/aliases.ts index d7f467f..1dc2195 100644 --- a/src/routes/aliases.js +++ b/src/backend/routes/aliases.ts @@ -1,16 +1,17 @@ -const express = require('express'); +import express, { Request, Response } from 'express'; +import Aliases from '../controllers/aliases'; +import Pages from '../controllers/pages'; +import Alias from '../models/alias'; +import verifyToken from './middlewares/token'; + const router = express.Router(); -const Aliases = require('../controllers/aliases'); -const Pages = require('../controllers/pages'); -const Alias = require('../models/alias'); -const verifyToken = require('./middlewares/token'); /** * GET /* * * Return document with given alias */ -router.get('*', verifyToken, async (req, res) => { +router.get('*', verifyToken, async (req: Request, res: Response) => { try { let url = req.originalUrl.slice(1); // Cuts first '/' character const queryParamsIndex = url.indexOf('?'); @@ -21,11 +22,15 @@ router.get('*', verifyToken, async (req, res) => { const alias = await Aliases.get(url); + if (alias.id === undefined) { + throw new Error('Alias not found'); + } + switch (alias.type) { case Alias.types.PAGE: { const page = await Pages.get(alias.id); - const pageParent = await page.parent; + const pageParent = await page.getParent(); res.render('pages/page', { page, @@ -37,9 +42,9 @@ router.get('*', verifyToken, async (req, res) => { } catch (err) { res.status(400).json({ success: false, - error: err.message, + error: err, }); } }); -module.exports = router; +export default router; diff --git a/src/backend/routes/api/index.ts b/src/backend/routes/api/index.ts new file mode 100644 index 0000000..be225d8 --- /dev/null +++ b/src/backend/routes/api/index.ts @@ -0,0 +1,12 @@ +import express from 'express'; +import pagesAPI from './pages'; +import transportAPI from './transport'; +import linksAPI from './links'; + +const router = express.Router(); + +router.use('/', pagesAPI); +router.use('/', transportAPI); +router.use('/', linksAPI); + +export default router; diff --git a/src/backend/routes/api/links.ts b/src/backend/routes/api/links.ts new file mode 100644 index 0000000..63fb06e --- /dev/null +++ b/src/backend/routes/api/links.ts @@ -0,0 +1,62 @@ +import express, { Request, Response } from 'express'; +import ogs from 'open-graph-scraper'; + +const router = express.Router(); + +interface ResponseData { + success: number; + meta?: { + title: string | undefined; + description: string | undefined; + siteName: string | undefined; + image: { url: string | undefined } + } +} + +/** + * Accept file url to fetch + */ +router.get('/fetchUrl', async (req: Request, res: Response) => { + const response: ResponseData = { + success: 0, + }; + + if (!req.query.url) { + res.status(400).json(response); + + return; + } + + if (typeof req.query.url !== 'string') { + return; + } + + try { + const linkData = (await ogs({ url: req.query.url })).result; + + if (!linkData.success) { + return; + } + + response.success = 1; + response.meta = { + title: linkData.ogTitle, + description: linkData.ogDescription, + siteName: linkData.ogSiteName, + image: { + url: undefined, + }, + }; + + if (linkData.ogImage !== undefined) { + response.meta.image = { url: linkData.ogImage.toString() }; + } + + res.status(200).json(response); + } catch (e) { + console.log(e); + res.status(500).json(response); + } +}); + +export default router; diff --git a/src/routes/api/pages.js b/src/backend/routes/api/pages.ts similarity index 59% rename from src/routes/api/pages.js rename to src/backend/routes/api/pages.ts index 8934ca9..ba240b7 100644 --- a/src/routes/api/pages.js +++ b/src/backend/routes/api/pages.ts @@ -1,8 +1,10 @@ -const express = require('express'); +import express, { Request, Response } from 'express'; +import multerFunc from 'multer'; +import Pages from '../../controllers/pages'; +import PagesOrder from '../../controllers/pagesOrder'; + const router = express.Router(); -const multer = require('multer')(); -const Pages = require('../../controllers/pages'); -const PagesOrder = require('../../controllers/pagesOrder'); +const multer = multerFunc(); /** * GET /page/:id @@ -10,18 +12,18 @@ const PagesOrder = require('../../controllers/pagesOrder'); * Return PageData of page with given id */ -router.get('/page/:id', async (req, res) => { +router.get('/page/:id', async (req: Request, res: Response) => { try { const page = await Pages.get(req.params.id); res.json({ success: true, - result: page.data + result: page.data, }); } catch (err) { res.status(400).json({ success: false, - error: err.message + error: (err as Error).message, }); } }); @@ -31,18 +33,18 @@ router.get('/page/:id', async (req, res) => { * * Return PageData for all pages */ -router.get('/pages', async (req, res) => { +router.get('/pages', async (req: Request, res: Response) => { try { const pages = await Pages.getAll(); res.json({ success: true, - result: pages + result: pages, }); } catch (err) { res.status(400).json({ success: false, - error: err.message + error: (err as Error).message, }); } }); @@ -52,22 +54,30 @@ router.get('/pages', async (req, res) => { * * Create new page in the database */ -router.put('/page', multer.none(), async (req, res) => { +router.put('/page', multer.none(), async (req: Request, res: Response) => { try { const { title, body, parent } = req.body; - const page = await Pages.insert({ title, body, parent }); + const page = await Pages.insert({ + title, + body, + parent, + }); + + if (page._id === undefined) { + throw new Error('Page not found'); + } /** push to the orders array */ await PagesOrder.push(parent, page._id); res.json({ success: true, - result: page + result: page, }); } catch (err) { res.status(400).json({ success: false, - error: err.message + error: (err as Error).message, }); } }); @@ -77,7 +87,7 @@ router.put('/page', multer.none(), async (req, res) => { * * Update page data in the database */ -router.post('/page/:id', multer.none(), async (req, res) => { +router.post('/page/:id', multer.none(), async (req: Request, res: Response) => { const { id } = req.params; try { @@ -85,25 +95,46 @@ router.post('/page/:id', multer.none(), async (req, res) => { const pages = await Pages.getAll(); let page = await Pages.get(id); + if (page._id === undefined) { + throw new Error('Page not found'); + } + + if (!page._parent) { + throw new Error('Parent not found'); + } + if (page._parent !== parent) { await PagesOrder.move(page._parent, parent, id); } else { if (putAbovePageId && putAbovePageId !== '0') { const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id); - await PagesOrder.update(unordered, page._id, page._parent, putAbovePageId); + const unOrdered: string[] = []; + + unordered.forEach(item => { + if (typeof item === 'string') { + unOrdered.push(item); + } + }); + + await PagesOrder.update(unOrdered, page._id, page._parent, putAbovePageId); } } - page = await Pages.update(id, { title, body, parent, uri }); + page = await Pages.update(id, { + title, + body, + parent, + uri, + }); res.json({ success: true, - result: page + result: page, }); } catch (err) { res.status(400).json({ success: false, - error: err.message + error: (err as Error).message, }); } }); @@ -113,10 +144,19 @@ router.post('/page/:id', multer.none(), async (req, res) => { * * Remove page from the database */ -router.delete('/page/:id', async (req, res) => { +router.delete('/page/:id', async (req: Request, res: Response) => { try { const pageId = req.params.id; const page = await Pages.get(pageId); + + if (page._id === undefined) { + throw new Error('Page not found'); + } + + if (!page._parent) { + throw new Error('Parent not found'); + } + const parentPageOrder = await PagesOrder.get(page._parent); const pageBeforeId = parentPageOrder.getPageBefore(page._id); const pageAfterId = parentPageOrder.getPageAfter(page._id); @@ -134,17 +174,19 @@ router.delete('/page/:id', async (req, res) => { /** * remove current page and go deeper to remove children with orders * - * @param startFrom + * @param {string} startFrom - start point to delete * @returns {Promise} */ - const deleteRecursively = async (startFrom) => { - let order = []; + const deleteRecursively = async (startFrom: string): Promise => { + let order: string[] = []; try { const children = await PagesOrder.get(startFrom); order = children.order; - } catch (e) {} + } catch (e) { + order = []; + } order.forEach(async id => { await deleteRecursively(id); @@ -153,7 +195,9 @@ router.delete('/page/:id', async (req, res) => { await Pages.remove(startFrom); try { await PagesOrder.remove(startFrom); - } catch (e) {} + } catch (e) { + order = []; + } }; await deleteRecursively(req.params.id); @@ -164,14 +208,14 @@ router.delete('/page/:id', async (req, res) => { res.json({ success: true, - result: pageToRedirect + result: pageToRedirect, }); } catch (err) { res.status(400).json({ success: false, - error: err.message + error: (err as Error).message, }); } }); -module.exports = router; +export default router; diff --git a/src/routes/api/transport.js b/src/backend/routes/api/transport.ts similarity index 56% rename from src/routes/api/transport.js rename to src/backend/routes/api/transport.ts index 16139c9..15219e0 100644 --- a/src/routes/api/transport.js +++ b/src/backend/routes/api/transport.ts @@ -1,59 +1,79 @@ -const express = require('express'); -const router = express.Router(); -const multer = require('multer'); -const mime = require('mime'); -const mkdirp = require('mkdirp'); -const Transport = require('../../controllers/transport'); -const { random16 } = require('../../utils/crypto'); -const config = require('../../../config'); +import { Request, Response, Router } from 'express'; +import multer, { StorageEngine } from 'multer'; +import mime from 'mime'; +import mkdirp from 'mkdirp'; +import config from 'config'; +import Transport from '../../controllers/transport'; +import { random16 } from '../../utils/crypto'; + +const router = Router(); /** * Multer storage for uploaded files and images - * @type {DiskStorage|DiskStorage} + * + * @type {StorageEngine} */ -const storage = multer.diskStorage({ +const storage: StorageEngine = multer.diskStorage({ destination: (req, file, cb) => { - const dir = config.uploads || 'public/uploads'; + const dir: string = config.get('uploads') || 'public/uploads'; - mkdirp(dir, err => cb(err, dir)); + mkdirp(dir); + cb(null, dir); }, filename: async (req, file, cb) => { const filename = await random16(); cb(null, `${filename}.${mime.getExtension(file.mimetype)}`); - } + }, }); /** * Multer middleware for image uploading */ const imageUploader = multer({ - storage, + storage: storage, fileFilter: (req, file, cb) => { if (!/image/.test(file.mimetype) && !/video\/mp4/.test(file.mimetype)) { cb(null, false); + return; } cb(null, true); - } -}).fields([ { name: 'image', maxCount: 1 } ]); + }, +}).fields([ { + name: 'image', + maxCount: 1, +} ]); /** * Multer middleware for file uploading */ const fileUploader = multer({ - storage -}).fields([ { name: 'file', maxCount: 1 } ]); + storage: storage, +}).fields([ { + name: 'file', + maxCount: 1, +} ]); /** * Accepts images to upload */ -router.post('/transport/image', imageUploader, async (req, res) => { - let response = { success: 0 }; +router.post('/transport/image', imageUploader, async (req: Request, res: Response) => { + const response = { + success: 0, + message: '', + }; - if (!req.files || !req.files.image) { + if (req.files === undefined) { + response.message = 'No files found'; res.status(400).json(response); + + return; + } + if (!('image' in req.files)) { + res.status(400).json(response); + return; } @@ -73,11 +93,17 @@ router.post('/transport/image', imageUploader, async (req, res) => { /** * Accepts files to upload */ -router.post('/transport/file', fileUploader, async (req, res) => { - let response = { success: 0 }; +router.post('/transport/file', fileUploader, async (req: Request, res: Response) => { + const response = { success: 0 }; - if (!req.files || !req.files.file) { + if (req.files === undefined) { res.status(400).json(response); + + return; + } + if (!('file' in req.files)) { + res.status(400).json(response); + return; } @@ -97,11 +123,12 @@ router.post('/transport/file', fileUploader, async (req, res) => { /** * Accept file url to fetch */ -router.post('/transport/fetch', multer().none(), async (req, res) => { - let response = { success: 0 }; +router.post('/transport/fetch', multer().none(), async (req: Request, res: Response) => { + const response = { success: 0 }; if (!req.body.url) { res.status(400).json(response); + return; } @@ -116,4 +143,4 @@ router.post('/transport/fetch', multer().none(), async (req, res) => { } }); -module.exports = router; +export default router; diff --git a/src/backend/routes/auth.ts b/src/backend/routes/auth.ts new file mode 100644 index 0000000..0f0065e --- /dev/null +++ b/src/backend/routes/auth.ts @@ -0,0 +1,78 @@ +import express, { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import config from 'config'; +import bcrypt from 'bcrypt'; +import csrf from 'csurf'; +import * as dotenv from 'dotenv'; +import Users from '../controllers/users'; + +dotenv.config(); + +const router = express.Router(); +const csrfProtection = csrf({ cookie: true }); +const parseForm = express.urlencoded({ extended: false }); + +/** + * Authorization page + */ +router.get('/auth', csrfProtection, function (req: Request, res: Response) { + res.render('auth', { + title: 'Login page', + csrfToken: req.csrfToken(), + }); +}); + +/** + * Process given password + */ +router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => { + try { + const userDoc = await Users.get(); + const passHash = userDoc.passHash; + + if (!passHash) { + res.render('auth', { + title: 'Login page', + header: 'Password not set', + csrfToken: req.csrfToken(), + }); + + return; + } + + bcrypt.compare(req.body.password, passHash, async (err, result) => { + if (err || result === false) { + res.render('auth', { + title: 'Login page', + header: 'Wrong password', + csrfToken: req.csrfToken(), + }); + + return; + } + + const token = jwt.sign({ + iss: 'Codex Team', + sub: 'auth', + iat: Date.now(), + }, passHash + config.get('secret')); + + res.cookie('authToken', token, { + httpOnly: true, + expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year + }); + + res.redirect('/'); + }); + } catch (err) { + res.render('auth', { + title: 'Login page', + header: 'Password not set', + csrfToken: req.csrfToken(), + }); + + return; + } +}); + +export default router; diff --git a/src/routes/home.js b/src/backend/routes/home.ts similarity index 55% rename from src/routes/home.js rename to src/backend/routes/home.ts index b1963de..ffe0148 100644 --- a/src/routes/home.js +++ b/src/backend/routes/home.ts @@ -1,9 +1,10 @@ -const express = require('express'); -const verifyToken = require('./middlewares/token'); +import express, { Request, Response } from 'express'; +import verifyToken from './middlewares/token'; + const router = express.Router(); /* GET home page. */ -router.get('/', verifyToken, async (req, res) => { +router.get('/', verifyToken, async (req: Request, res: Response) => { const config = req.app.locals.config; if (config.startPage) { @@ -12,4 +13,4 @@ router.get('/', verifyToken, async (req, res) => { res.render('pages/index', { isAuthorized: res.locals.isAuthorized }); }); -module.exports = router; +export default router; diff --git a/src/backend/routes/index.ts b/src/backend/routes/index.ts new file mode 100644 index 0000000..2057c44 --- /dev/null +++ b/src/backend/routes/index.ts @@ -0,0 +1,17 @@ +import express from 'express'; +import home from './home'; +import pages from './pages'; +import auth from './auth'; +import aliases from './aliases'; +import api from './api'; +import pagesMiddleware from './middlewares/pages'; + +const router = express.Router(); + +router.use('/', pagesMiddleware, home); +router.use('/', pagesMiddleware, pages); +router.use('/', pagesMiddleware, auth); +router.use('/api', api); +router.use('/', aliases); + +export default router; diff --git a/src/backend/routes/middlewares/locals.ts b/src/backend/routes/middlewares/locals.ts new file mode 100644 index 0000000..91a119e --- /dev/null +++ b/src/backend/routes/middlewares/locals.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from 'express'; + +/** + * Middleware for checking locals.isAuthorized property, which allows to edit/create pages + * + * @param req - request object + * @param res - response object + * @param next - next function + */ +export default function allowEdit(req: Request, res: Response, next: NextFunction): void { + if (res.locals.isAuthorized) { + next(); + } else { + res.redirect('/auth'); + } +} diff --git a/src/routes/middlewares/pages.js b/src/backend/routes/middlewares/pages.ts similarity index 61% rename from src/routes/middlewares/pages.js rename to src/backend/routes/middlewares/pages.ts index 21386bb..523b7cc 100644 --- a/src/routes/middlewares/pages.js +++ b/src/backend/routes/middlewares/pages.ts @@ -1,6 +1,9 @@ -const Pages = require('../../controllers/pages'); -const PagesOrder = require('../../controllers/pagesOrder'); -const asyncMiddleware = require('../../utils/asyncMiddleware'); +import { NextFunction, Request, Response } from 'express'; +import Pages from '../../controllers/pages'; +import PagesOrder from '../../controllers/pagesOrder'; +import Page from '../../models/page'; +import asyncMiddleware from '../../utils/asyncMiddleware'; +import PageOrder from '../../models/pageOrder'; /** * Process one-level pages list to parent-children list @@ -8,12 +11,12 @@ const asyncMiddleware = require('../../utils/asyncMiddleware'); * @param {string} parentPageId - parent page id * @param {Page[]} pages - list of all available pages * @param {PagesOrder[]} pagesOrder - list of pages order - * @param {number} level - * @param {number} currentLevel + * @param {number} level - max level recursion + * @param {number} currentLevel - current level of element * - * @return {Page[]} + * @returns {Page[]} */ -function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel = 1) { +function createMenuTree(parentPageId: string, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] { const childrenOrder = pagesOrder.find(order => order.data.page === parentPageId); /** @@ -21,16 +24,16 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel * if we got some children order on parents tree, then we push found pages in order sequence * otherwise just find all pages includes parent tree */ - let ordered = []; + let ordered: any[] = []; if (childrenOrder) { - ordered = childrenOrder.order.map(pageId => { + ordered = childrenOrder.order.map((pageId: string) => { return pages.find(page => page._id === pageId); }); } const unordered = pages.filter(page => page._parent === parentPageId); - const branch = [ ...new Set([...ordered, ...unordered]) ]; + const branch = Array.from(new Set([...ordered, ...unordered])); /** * stop recursion when we got the passed max level @@ -44,20 +47,22 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel */ return branch.filter(page => page && page._id).map(page => { return Object.assign({ - children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1) + children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1), }, page.data); }); } /** * Middleware for all /page/... routes - * @param req - * @param res - * @param next + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ -module.exports = asyncMiddleware(async (req, res, next) => { +export default asyncMiddleware(async (req: Request, res: Response, next: NextFunction) => { /** * Pages without parent + * * @type {string} */ const parentIdOfRootPages = '0'; diff --git a/src/backend/routes/middlewares/token.ts b/src/backend/routes/middlewares/token.ts new file mode 100644 index 0000000..2289be1 --- /dev/null +++ b/src/backend/routes/middlewares/token.ts @@ -0,0 +1,38 @@ +import * as dotenv from 'dotenv'; +import config from 'config'; +import { NextFunction, Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import Users from '../../controllers/users'; + +dotenv.config(); + +/** + * Middleware for checking jwt token + * + * @param req - request object + * @param res - response object + * @param next - next function + */ +export default async function verifyToken(req: Request, res: Response, next: NextFunction): Promise { + const token = req.cookies.authToken; + + try { + const userDoc = await Users.get(); + + if (!userDoc.passHash) { + res.locals.isAuthorized = false; + next(); + + return; + } + + const decodedToken = jwt.verify(token, userDoc.passHash + config.get('secret')); + + res.locals.isAuthorized = !!decodedToken; + + next(); + } catch (err) { + res.locals.isAuthorized = false; + next(); + } +} diff --git a/src/backend/routes/pages.ts b/src/backend/routes/pages.ts new file mode 100644 index 0000000..82c8107 --- /dev/null +++ b/src/backend/routes/pages.ts @@ -0,0 +1,75 @@ +import express, { NextFunction, Request, Response } from 'express'; +import Pages from '../controllers/pages'; +import PagesOrder from '../controllers/pagesOrder'; +import verifyToken from './middlewares/token'; +import allowEdit from './middlewares/locals'; + +const router = express.Router(); + +/** + * Create new page form + */ +router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => { + try { + const pagesAvailable = await Pages.getAll(); + + res.render('pages/form', { + pagesAvailable, + page: null, + }); + } catch (error) { + res.status(404); + next(error); + } +}); + +/** + * Edit page form + */ +router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => { + const pageId = req.params.id; + + try { + const page = await Pages.get(pageId); + const pagesAvailable = await Pages.getAllExceptChildren(pageId); + + if (!page._parent) { + throw new Error('Parent not found'); + } + + const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true); + + res.render('pages/form', { + page, + parentsChildrenOrdered, + pagesAvailable, + }); + } catch (error) { + res.status(404); + next(error); + } +}); + +/** + * View page + */ +router.get('/page/:id', verifyToken, async (req: Request, res: Response, next: NextFunction) => { + const pageId = req.params.id; + + try { + const page = await Pages.get(pageId); + + const pageParent = await page.parent; + + res.render('pages/page', { + page, + pageParent, + config: req.app.locals.config, + }); + } catch (error) { + res.status(404); + next(error); + } +}); + +export default router; diff --git a/src/backend/utils/asyncMiddleware.ts b/src/backend/utils/asyncMiddleware.ts new file mode 100644 index 0000000..65fdda4 --- /dev/null +++ b/src/backend/utils/asyncMiddleware.ts @@ -0,0 +1,18 @@ +import { NextFunction, Request, Response } from 'express'; + +interface InputFunction { + (req: Request, res: Response, next: NextFunction): void; +} + +/** + * Helper for making async middlewares for express router + * + * @param {Function} fn - input function + * @returns {function(*=, *=, *=)} + */ +export default function asyncMiddleware(fn: InputFunction): (req: Request, res: Response, next: NextFunction) => void { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)) + .catch(next); + }; +} diff --git a/src/utils/crypto.js b/src/backend/utils/crypto.ts similarity index 50% rename from src/utils/crypto.js rename to src/backend/utils/crypto.ts index 0d23ccc..39b60c6 100644 --- a/src/utils/crypto.js +++ b/src/backend/utils/crypto.ts @@ -1,4 +1,14 @@ -const crypto = require('crypto'); +import crypto from 'crypto'; + +/** + * + * @param {string} hexStr - input hex string + * @returns {string} - output binary string + */ +function hexToBinary(hexStr: string): string { + return (parseInt(hexStr, 16).toString(2)) + .padStart(8, '0'); +} /** * Create binary md5 @@ -6,10 +16,10 @@ const crypto = require('crypto'); * @param stringToHash - string to hash * @returns {string} - binary hash of argument */ -function binaryMD5(stringToHash) { - return crypto.createHash('md5') +export function binaryMD5(stringToHash: string): string { + return hexToBinary(crypto.createHash('md5') .update(stringToHash) - .digest('binary'); + .digest('hex')); } /** @@ -17,7 +27,7 @@ function binaryMD5(stringToHash) { * * @returns {Promise} */ -function random16() { +export function random16(): Promise { return new Promise((resolve, reject) => { crypto.randomBytes(16, (err, raw) => { if (err) { @@ -29,7 +39,7 @@ function random16() { }); } -module.exports = { +export default { binaryMD5, random16, }; diff --git a/src/utils/database/index.js b/src/backend/utils/database/index.ts similarity index 58% rename from src/utils/database/index.js rename to src/backend/utils/database/index.ts index 43913c4..f742172 100644 --- a/src/utils/database/index.js +++ b/src/backend/utils/database/index.ts @@ -1,33 +1,58 @@ -const pages = require('./pages'); -const files = require('./files'); -const password = require('./password'); -const aliases = require('./aliases'); -const pagesOrder = require('./pagesOrder'); +import Datastore from 'nedb'; +import { AliasData } from '../../models/alias'; +import { FileData } from '../../models/file'; +import { PageData } from '../../models/page'; +import { PageOrderData } from '../../models/pageOrder'; +import { UserData } from '../../models/user'; +import initDb from './initDb'; + +/** + * @typedef Options - optional params + * @param {boolean} multi - (false) allows to take action to several documents + * @param {boolean} upsert - (false) if true, upsert document with update fields. + * Method will return inserted doc or number of affected docs if doc hasn't been inserted + * @param {boolean} returnUpdatedDocs - (false) if true, returns affected docs + */ +interface Options { + multi?: boolean; + upsert?: boolean; + returnUpdatedDocs?: boolean; +} + +interface ResolveFunction { + (value: any): void; +} + +interface RejectFunction { + (reason?: unknown): void; +} /** * @class Database * @classdesc Simple decorator class to work with nedb datastore * - * @property db - nedb Datastore object + * @property {Datastore} db - nedb Datastore object */ -class Database { +export class Database { + private db: Datastore; /** - * @constructor + * @class * * @param {Object} nedbInstance - nedb Datastore object */ - constructor(nedbInstance) { + constructor(nedbInstance: Datastore) { this.db = nedbInstance; } /** * Insert new document into the database + * * @see https://github.com/louischatriot/nedb#inserting-documents * * @param {Object} doc - object to insert * @returns {Promise} - inserted doc or Error object */ - async insert(doc) { + public async insert(doc: DocType): Promise { return new Promise((resolve, reject) => this.db.insert(doc, (err, newDoc) => { if (err) { reject(err); @@ -39,14 +64,15 @@ class Database { /** * Find documents that match passed query + * * @see https://github.com/louischatriot/nedb#finding-documents * * @param {Object} query - query object * @param {Object} projection - projection object * @returns {Promise|Error>} - found docs or Error object */ - async find(query, projection) { - const cbk = (resolve, reject) => (err, docs) => { + public async find(query: Record, projection?: DocType): Promise> { + const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, docs: DocType[]) => { if (err) { reject(err); } @@ -65,14 +91,15 @@ class Database { /** * Find one document matches passed query + * * @see https://github.com/louischatriot/nedb#finding-documents * * @param {Object} query - query object * @param {Object} projection - projection object * @returns {Promise} - found doc or Error object */ - async findOne(query, projection) { - const cbk = (resolve, reject) => (err, doc) => { + public async findOne(query: Record, projection?: DocType): Promise { + const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, doc: DocType) => { if (err) { reject(err); } @@ -91,18 +118,15 @@ class Database { /** * Update document matches query + * * @see https://github.com/louischatriot/nedb#updating-documents * * @param {Object} query - query object * @param {Object} update - fields to update - * @param {Object} options - * @param {Boolean} options.multi - (false) allows update several documents - * @param {Boolean} options.upsert - (false) if true, upsert document with update fields. - * Method will return inserted doc or number of affected docs if doc hasn't been inserted - * @param {Boolean} options.returnUpdatedDocs - (false) if true, returns affected docs + * @param {Options} options - optional params * @returns {Promise} - number of updated rows or affected docs or Error object */ - async update(query, update, options = {}) { + public async update(query: Record, update: DocType, options: Options = {}): Promise> { return new Promise((resolve, reject) => this.db.update(query, update, options, (err, result, affectedDocs) => { if (err) { reject(err); @@ -126,14 +150,14 @@ class Database { /** * Remove document matches passed query + * * @see https://github.com/louischatriot/nedb#removing-documents * * @param {Object} query - query object - * @param {Object} options - * @param {Boolean} options.multi - (false) if true, remove several docs + * @param {Options} options - optional params * @returns {Promise} - number of removed rows or Error object */ - async remove(query, options = {}) { + public async remove(query: Record, options: Options = {}): Promise { return new Promise((resolve, reject) => this.db.remove(query, options, (err, result) => { if (err) { reject(err); @@ -144,11 +168,10 @@ class Database { } } -module.exports = { - class: Database, - pages: new Database(pages), - password: new Database(password), - aliases: new Database(aliases), - pagesOrder: new Database(pagesOrder), - files: new Database(files) +export default { + pages: new Database(initDb('pages')), + password: new Database(initDb('password')), + aliases: new Database(initDb('aliases')), + pagesOrder: new Database(initDb('pagesOrder')), + files: new Database(initDb('files')), }; diff --git a/src/backend/utils/database/initDb.ts b/src/backend/utils/database/initDb.ts new file mode 100644 index 0000000..7475369 --- /dev/null +++ b/src/backend/utils/database/initDb.ts @@ -0,0 +1,16 @@ +import Datastore from 'nedb'; +import config from 'config'; +import path from 'path'; + +/** + * Init function for nedb instance + * + * @param {string} name - name of the data file + * @returns {Datastore} db - nedb instance + */ +export default function initDb(name: string): Datastore { + return new Datastore({ + filename: path.resolve(`./${config.get('database')}/${name}.db`), + autoload: true, + }); +} \ No newline at end of file diff --git a/src/utils/objects.js b/src/backend/utils/objects.ts similarity index 62% rename from src/utils/objects.js rename to src/backend/utils/objects.ts index c297a37..7eb5d06 100644 --- a/src/utils/objects.js +++ b/src/backend/utils/objects.ts @@ -5,8 +5,13 @@ * @param {object[]} sources * @returns {object} */ -function deepMerge(target, ...sources) { - const isObject = item => item && typeof item === 'object' && !Array.isArray(item); + +/** + * @param {Record} target - target to merge into + * @param {...any[]} sources - sources to merge from + */ +function deepMerge(target: Record, ...sources: any[]): Record { + const isObject = (item: unknown): boolean => !!item && typeof item === 'object' && !Array.isArray(item); if (!sources.length) { return target; @@ -30,6 +35,4 @@ function deepMerge(target, ...sources) { return deepMerge(target, ...sources); } -module.exports = { - deepMerge, -}; +export default deepMerge; diff --git a/src/utils/rcparser.js b/src/backend/utils/rcparser.ts similarity index 71% rename from src/utils/rcparser.js rename to src/backend/utils/rcparser.ts index d995075..85a274d 100644 --- a/src/utils/rcparser.js +++ b/src/backend/utils/rcparser.ts @@ -1,28 +1,43 @@ -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const rcPath = path.resolve(__dirname, '../../', config.rcFile || './.codexdocsrc'); +import fs from 'fs'; +import path from 'path'; +import config from 'config'; + +const rcPath = path.resolve(__dirname, '../../../', config.get('rcFile') || './.codexdocsrc'); + +/** + * @typedef {object} menu + * @property {string} title - menu option title + * @property {string} uri - menu option href + */ +interface Menu { + title: string; + uri: string; + [key: string]: string; +} /** * @typedef {object} RCData * @property {string} title - website title - * @property {object[]} menu - options for website menu - * @property {string} menu[].title - menu option title - * @property {string} menu[].uri - menu option href + * @property {Menu[]} menu - options for website menu */ +interface RCData { + title: string; + menu: Menu[]; + [key: string]: string | Menu[]; +} /** * @class RCParser * @classdesc Class to parse runtime configuration file for CodeX Docs engine */ -module.exports = class RCParser { +export default class RCParser { /** * Default CodeX Docs configuration * * @static * @returns {{title: string, menu: Array}} */ - static get DEFAULTS() { + public static get DEFAULTS():RCData { return { title: 'CodeX Docs', menu: [], @@ -35,12 +50,12 @@ module.exports = class RCParser { * @static * @returns {{title: string, menu: []}} */ - static getConfiguration() { + public static getConfiguration(): RCData { if (!fs.existsSync(rcPath)) { return RCParser.DEFAULTS; } - const file = fs.readFileSync(rcPath, { encoding: 'UTF-8' }); + const file = fs.readFileSync(rcPath, 'utf-8'); const rConfig = RCParser.DEFAULTS; let userConfig; @@ -63,7 +78,7 @@ module.exports = class RCParser { rConfig.menu = RCParser.DEFAULTS.menu; } - rConfig.menu = rConfig.menu.filter((option, i) => { + rConfig.menu = rConfig.menu.filter((option: string | Menu, i:number) => { i = i + 1; if (typeof option === 'string') { return true; @@ -92,7 +107,7 @@ module.exports = class RCParser { return true; }); - rConfig.menu = rConfig.menu.map(option => { + rConfig.menu = rConfig.menu.map((option: string | Menu) => { if (typeof option === 'string') { return { title: option, @@ -106,4 +121,4 @@ module.exports = class RCParser { return rConfig; } -}; +} diff --git a/src/utils/translation.js b/src/backend/utils/translation.ts similarity index 78% rename from src/utils/translation.js rename to src/backend/utils/translation.ts index f1682a3..f7baee4 100644 --- a/src/utils/translation.js +++ b/src/backend/utils/translation.ts @@ -1,4 +1,7 @@ -const translationTable = { +interface TransTable { + [key: string]: string; +} +const translationTable: TransTable = { а: 'a', б: 'b', в: 'v', @@ -73,6 +76,10 @@ const translationTable = { * @returns {string} - translated string */ -module.exports = function translateString(string) { +/** + * @param {string} string - input text to be translated + * @returns {string} text - translated text + */ +export default function translateString(string: string): string { return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char); -}; +} diff --git a/src/utils/twig.js b/src/backend/utils/twig.ts similarity index 53% rename from src/utils/twig.js rename to src/backend/utils/twig.ts index 2195d59..fa3ee6d 100644 --- a/src/utils/twig.js +++ b/src/backend/utils/twig.ts @@ -1,22 +1,22 @@ /** * Twig extensions */ -const twig = require('twig'); -const fs = require('fs'); -const urlify = require('./urlify'); +import twig from 'twig'; +import fs from 'fs'; +import urlify from './urlify'; -module.exports = (function () { +export default (function () { 'use strict'; /** * Function for include svg on page * * @example svg('path/from/root/dir') - * @param filename - name of icon + * @param {string} filename - name of icon * @returns {string} - svg code */ - twig.extendFunction('svg', function (filename) { - return fs.readFileSync(`${__dirname}/../frontend/svg/${filename}.svg`, 'utf-8'); + twig.extendFunction('svg', function (filename: string) { + return fs.readFileSync(`./src/frontend/svg/${filename}.svg`, 'utf-8'); }); /** @@ -26,7 +26,7 @@ module.exports = (function () { * @param {string} string - source string with HTML * @returns {string} alias-like string */ - twig.extendFilter('urlify', function (string) { + twig.extendFilter('urlify', function (string: string) { return urlify(string); }); @@ -34,13 +34,15 @@ module.exports = (function () { * Parse link as URL object * * @param {string} linkUrl - link to be processed - * @returns {UrlWithStringQuery} — url data + * @returns {string} url — url data */ - twig.extendFunction('parseLink', function (linkUrl) { + twig.extendFunction('parseLink', function (linkUrl: string): string { try { - return new URL(linkUrl); + return new URL(linkUrl).toString(); } catch (e) { console.log(e); + + return ''; } }); }()); diff --git a/src/utils/urlify.js b/src/backend/utils/urlify.ts similarity index 87% rename from src/utils/urlify.js rename to src/backend/utils/urlify.ts index bb08e20..9716a10 100644 --- a/src/utils/urlify.js +++ b/src/backend/utils/urlify.ts @@ -1,4 +1,4 @@ -const translateString = require('./translation'); +import translateString from './translation'; /** * Convert text to URL-like string @@ -7,7 +7,7 @@ const translateString = require('./translation'); * @param {string} string - source string with HTML * @returns {string} alias-like string */ -module.exports = function urlify(string) { +export default function urlify(string: string): string { // strip tags string = string.replace(/(<([^>]+)>)/ig, ''); @@ -30,4 +30,4 @@ module.exports = function urlify(string) { string = translateString(string); return string; -}; +} diff --git a/src/views/auth.twig b/src/backend/views/auth.twig similarity index 100% rename from src/views/auth.twig rename to src/backend/views/auth.twig diff --git a/src/views/components/aside.twig b/src/backend/views/components/aside.twig similarity index 100% rename from src/views/components/aside.twig rename to src/backend/views/components/aside.twig diff --git a/src/views/components/header.twig b/src/backend/views/components/header.twig similarity index 100% rename from src/views/components/header.twig rename to src/backend/views/components/header.twig diff --git a/src/views/error.twig b/src/backend/views/error.twig similarity index 100% rename from src/views/error.twig rename to src/backend/views/error.twig diff --git a/src/views/layout.twig b/src/backend/views/layout.twig similarity index 100% rename from src/views/layout.twig rename to src/backend/views/layout.twig diff --git a/src/views/pages/blocks/checklist.twig b/src/backend/views/pages/blocks/checklist.twig similarity index 100% rename from src/views/pages/blocks/checklist.twig rename to src/backend/views/pages/blocks/checklist.twig diff --git a/src/views/pages/blocks/code.twig b/src/backend/views/pages/blocks/code.twig similarity index 100% rename from src/views/pages/blocks/code.twig rename to src/backend/views/pages/blocks/code.twig diff --git a/src/views/pages/blocks/delimiter.twig b/src/backend/views/pages/blocks/delimiter.twig similarity index 100% rename from src/views/pages/blocks/delimiter.twig rename to src/backend/views/pages/blocks/delimiter.twig diff --git a/src/views/pages/blocks/embed.twig b/src/backend/views/pages/blocks/embed.twig similarity index 100% rename from src/views/pages/blocks/embed.twig rename to src/backend/views/pages/blocks/embed.twig diff --git a/src/views/pages/blocks/header.twig b/src/backend/views/pages/blocks/header.twig similarity index 100% rename from src/views/pages/blocks/header.twig rename to src/backend/views/pages/blocks/header.twig diff --git a/src/views/pages/blocks/image.twig b/src/backend/views/pages/blocks/image.twig similarity index 100% rename from src/views/pages/blocks/image.twig rename to src/backend/views/pages/blocks/image.twig diff --git a/src/views/pages/blocks/linkTool.twig b/src/backend/views/pages/blocks/linkTool.twig similarity index 100% rename from src/views/pages/blocks/linkTool.twig rename to src/backend/views/pages/blocks/linkTool.twig diff --git a/src/views/pages/blocks/list.twig b/src/backend/views/pages/blocks/list.twig similarity index 100% rename from src/views/pages/blocks/list.twig rename to src/backend/views/pages/blocks/list.twig diff --git a/src/views/pages/blocks/paragraph.twig b/src/backend/views/pages/blocks/paragraph.twig similarity index 100% rename from src/views/pages/blocks/paragraph.twig rename to src/backend/views/pages/blocks/paragraph.twig diff --git a/src/views/pages/blocks/raw.twig b/src/backend/views/pages/blocks/raw.twig similarity index 100% rename from src/views/pages/blocks/raw.twig rename to src/backend/views/pages/blocks/raw.twig diff --git a/src/views/pages/blocks/table.twig b/src/backend/views/pages/blocks/table.twig similarity index 100% rename from src/views/pages/blocks/table.twig rename to src/backend/views/pages/blocks/table.twig diff --git a/src/views/pages/blocks/warning.twig b/src/backend/views/pages/blocks/warning.twig similarity index 100% rename from src/views/pages/blocks/warning.twig rename to src/backend/views/pages/blocks/warning.twig diff --git a/src/views/pages/form.twig b/src/backend/views/pages/form.twig similarity index 100% rename from src/views/pages/form.twig rename to src/backend/views/pages/form.twig diff --git a/src/views/pages/index.twig b/src/backend/views/pages/index.twig similarity index 100% rename from src/views/pages/index.twig rename to src/backend/views/pages/index.twig diff --git a/src/views/pages/page.twig b/src/backend/views/pages/page.twig similarity index 100% rename from src/views/pages/page.twig rename to src/backend/views/pages/page.twig diff --git a/bin/nvm.sh b/src/bin/nvm.sh similarity index 100% rename from bin/nvm.sh rename to src/bin/nvm.sh diff --git a/bin/www b/src/bin/server.ts old mode 100755 new mode 100644 similarity index 65% rename from bin/www rename to src/bin/server.ts index ab2a467..5ea477b --- a/bin/www +++ b/src/bin/server.ts @@ -1,17 +1,17 @@ -#!/usr/bin/env node - /** * Module dependencies. */ -const app = require('../src/app'); -const debug = require('debug')('codex.editor.docs:server'); -const http = require('http'); -const config = require('../config'); +import app from '../backend/app'; +import http from 'http'; +import config from 'config'; +import Debug from 'debug'; + +const debug = Debug.debug('codex.editor.docs:server'); /** * Get port from environment and store in Express. */ -const port = normalizePort(config.port || '3000'); +const port = normalizePort(config.get('port') || '3000'); app.set('port', port); @@ -29,8 +29,9 @@ server.on('listening', onListening); /** * Normalize a port into a number, string, or false. + * @param val */ -function normalizePort(val) { +function normalizePort(val: string): number | string | false { const value = parseInt(val, 10); if (isNaN(value)) { @@ -47,9 +48,10 @@ function normalizePort(val) { } /** - * Event listener for HTTP server "error" event. + * Event listener for HTTP server 'error' event. + * @param error */ -function onError(error) { +function onError(error: NodeJS.ErrnoException): void { if (error.syscall !== 'listen') { throw error; } @@ -63,19 +65,27 @@ function onError(error) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); + break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); + break; default: throw error; } } /** - * Event listener for HTTP server "listening" event. + * Event listener for HTTP server 'listening' event. */ -function onListening() { +function onListening(): void { const addr = server.address(); + + if (addr === null) { + debug('Address not found'); + process.exit(1); + } + const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; @@ -83,4 +93,7 @@ function onListening() { debug('Listening on ' + bind); } -module.exports = {server, app}; +export default { + server, + app, +}; \ No newline at end of file diff --git a/src/controllers/users.js b/src/controllers/users.js deleted file mode 100644 index 676a26f..0000000 --- a/src/controllers/users.js +++ /dev/null @@ -1,20 +0,0 @@ -const Model = require('../models/user'); - -/** - * @class Users - * @classdesc Users controller - */ -class Users { - /** - * Find and return user model. - * - * @returns {Promise} - */ - static async get() { - const userDoc = await Model.get(); - - return userDoc; - } -} - -module.exports = Users; diff --git a/src/frontend/js/modules/writing.js b/src/frontend/js/modules/writing.js index bb3ea77..7fab306 100644 --- a/src/frontend/js/modules/writing.js +++ b/src/frontend/js/modules/writing.js @@ -52,11 +52,16 @@ export default class Writing { this.editor = editor; }); + window.onbeforeunload = (e) => { + return ''; + } + /** * Activate form elements */ this.nodes.saveButton = moduleEl.querySelector('[name="js-submit-save"]'); this.nodes.saveButton.addEventListener('click', () => { + window.onbeforeunload = null; this.saveButtonClicked(); }); @@ -69,7 +74,7 @@ export default class Writing { if (!isUserAgree) { return; } - + window.onbeforeunload = null; this.removeButtonClicked(); }); } diff --git a/generatePassword.js b/src/generatePassword.ts similarity index 82% rename from generatePassword.js rename to src/generatePassword.ts index 9b5d32e..f0c09b4 100644 --- a/generatePassword.js +++ b/src/generatePassword.ts @@ -1,14 +1,17 @@ #!/usr/bin/env node -let { password: db } = require('./src/utils/database'); -const program = require('commander'); +import database from './backend/utils/database'; +import commander from 'commander'; +import bcrypt from 'bcrypt'; -const bcrypt = require('bcrypt'); +const db = database['password']; +const program = commander.program; const saltRounds = 12; /** * Script for generating password, that will be used to create and edit pages in CodeX.Docs. * Hashes password with bcrypt and inserts it to the database. + * * @see {https://github.com/tj/commander.js | CommanderJS} */ program @@ -23,7 +26,7 @@ program const userDoc = { passHash: hash }; - await db.remove({}, {multi: true}); + await db.remove({}, { multi: true }); await db.insert(userDoc); console.log('Password was successfully generated'); diff --git a/src/models/user.js b/src/models/user.js deleted file mode 100644 index c29e2c2..0000000 --- a/src/models/user.js +++ /dev/null @@ -1,36 +0,0 @@ -const { password: db } = require('../utils/database/index'); - -/** - * @class User - * @class User model - * - * @property {string} passHash - hashed password - */ -class User { - /** - * Find and return model of user. - * User is only one. - * - * @returns {Promise} - */ - static async get() { - const data = await db.findOne({}); - - if (!data) { - return null; - } - - return new User(data); - } - - /** - * @class - * - * @param {object} userData - */ - constructor(userData) { - this.passHash = userData.passHash; - } -} - -module.exports = User; diff --git a/src/routes/api/index.js b/src/routes/api/index.js deleted file mode 100644 index ed1c555..0000000 --- a/src/routes/api/index.js +++ /dev/null @@ -1,12 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -const pagesAPI = require('./pages'); -const transportAPI = require('./transport'); -const linksAPI = require('./links'); - -router.use('/', pagesAPI); -router.use('/', transportAPI); -router.use('/', linksAPI); - -module.exports = router; diff --git a/src/routes/api/links.js b/src/routes/api/links.js deleted file mode 100644 index 99d0e43..0000000 --- a/src/routes/api/links.js +++ /dev/null @@ -1,38 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const ogs = require('open-graph-scraper'); - -/** - * Accept file url to fetch - */ -router.get('/fetchUrl', async (req, res) => { - const response = { - success: 0 - }; - - if (!req.query.url) { - res.status(400).json(response); - return; - } - - try { - const linkData = (await ogs({ url: req.query.url })).result; - - response.success = 1; - response.meta = { - title: linkData.ogTitle, - description: linkData.ogDescription, - site_name: linkData.ogSiteName, - image: { - url: linkData.ogImage.url - } - }; - - res.status(200).json(response); - } catch (e) { - console.log(e); - res.status(500).json(response); - } -}); - -module.exports = router; diff --git a/src/routes/auth.js b/src/routes/auth.js deleted file mode 100644 index 1c1b5f8..0000000 --- a/src/routes/auth.js +++ /dev/null @@ -1,63 +0,0 @@ -require('dotenv').config(); - -const express = require('express'); -const jwt = require('jsonwebtoken'); -const router = express.Router(); -const Users = require('../controllers/users'); -const config = require('../../config/index'); -const bcrypt = require('bcrypt'); -const csrf = require('csurf'); -const csrfProtection = csrf({ cookie: true }); -const parseForm = express.urlencoded({ extended: false }); - -/** - * Authorization page - */ -router.get('/auth', csrfProtection, function (req, res) { - res.render('auth', { - title: 'Login page', - csrfToken: req.csrfToken(), - }); -}); - -/** - * Process given password - */ -router.post('/auth', parseForm, csrfProtection, async (req, res) => { - const userDoc = await Users.get(); - - if (!userDoc) { - res.render('auth', { - title: 'Login page', - header: 'Password not set', - csrfToken: req.csrfToken(), - }); - } - - const passHash = userDoc.passHash; - - bcrypt.compare(req.body.password, passHash, async (err, result) => { - if (err || result === false) { - res.render('auth', { - title: 'Login page', - header: 'Wrong password', - csrfToken: req.csrfToken(), - }); - } - - const token = jwt.sign({ - iss: 'Codex Team', - sub: 'auth', - iat: Date.now(), - }, passHash + config.secret); - - res.cookie('authToken', token, { - httpOnly: true, - expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year - }); - - res.redirect('/'); - }); -}); - -module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index aab43a2..0000000 --- a/src/routes/index.js +++ /dev/null @@ -1,18 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -const home = require('./home'); -const pages = require('./pages'); -const auth = require('./auth'); -const aliases = require('./aliases'); -const api = require('./api'); - -const pagesMiddleware = require('./middlewares/pages'); - -router.use('/', pagesMiddleware, home); -router.use('/', pagesMiddleware, pages); -router.use('/', pagesMiddleware, auth); -router.use('/api', api); -router.use('/', aliases); - -module.exports = router; diff --git a/src/routes/middlewares/locals.js b/src/routes/middlewares/locals.js deleted file mode 100644 index bd584cb..0000000 --- a/src/routes/middlewares/locals.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Middleware for checking locals.isAuthorized property, which allows to edit/create pages - * @param req - * @param res - * @param next - */ -module.exports = function allowEdit(req, res, next) { - if (res.locals.isAuthorized) { - next(); - } else { - res.redirect('/auth'); - } -}; diff --git a/src/routes/middlewares/token.js b/src/routes/middlewares/token.js deleted file mode 100644 index 9b7af2d..0000000 --- a/src/routes/middlewares/token.js +++ /dev/null @@ -1,26 +0,0 @@ -require('dotenv').config(); -const config = require('../../../config/index'); -const jwt = require('jsonwebtoken'); -const Users = require('../../controllers/users'); - -/** - * Middleware for checking jwt token - * @param req - * @param res - * @param next - */ -module.exports = async function verifyToken(req, res, next) { - let token = req.cookies.authToken; - const userDoc = await Users.get(); - - if (!userDoc) { - res.locals.isAuthorized = false; - next(); - return; - } - - jwt.verify(token, userDoc.passHash + config.secret, (err, decodedToken) => { - res.locals.isAuthorized = !(err || !decodedToken); - next(); - }); -}; diff --git a/src/routes/pages.js b/src/routes/pages.js deleted file mode 100644 index 46375ab..0000000 --- a/src/routes/pages.js +++ /dev/null @@ -1,65 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const Pages = require('../controllers/pages'); -const PagesOrder = require('../controllers/pagesOrder'); - -const verifyToken = require('./middlewares/token'); -const allowEdit = require('./middlewares/locals'); - -/** - * Create new page form - */ -router.get('/page/new', verifyToken, allowEdit, async (req, res, next) => { - const pagesAvailable = await Pages.getAll(); - - res.render('pages/form', { - pagesAvailable, - page: null, - }); -}); - -/** - * Edit page form - */ -router.get('/page/edit/:id', verifyToken, allowEdit, async (req, res, next) => { - const pageId = req.params.id; - - try { - const page = await Pages.get(pageId); - const pagesAvailable = await Pages.getAllExceptChildren(pageId); - const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true); - - res.render('pages/form', { - page, - parentsChildrenOrdered, - pagesAvailable, - }); - } catch (error) { - res.status(404); - next(error); - } -}); - -/** - * View page - */ -router.get('/page/:id', verifyToken, async (req, res, next) => { - const pageId = req.params.id; - - try { - const page = await Pages.get(pageId); - - const pageParent = await page.parent; - - res.render('pages/page', { - page, - pageParent, - config: req.app.locals.config, - }); - } catch (error) { - res.status(404); - next(error); - } -}); - -module.exports = router; diff --git a/src/test/database.ts b/src/test/database.ts new file mode 100644 index 0000000..a073223 --- /dev/null +++ b/src/test/database.ts @@ -0,0 +1,196 @@ +import fs from 'fs'; +import config from 'config'; +import { expect } from 'chai'; +import Datastore from 'nedb'; + +import { Database } from '../backend/utils/database'; + +interface Document { + data?: any; + _id?: string; + update?: boolean; + no?: any; +} + +describe('Database', () => { + const pathToDB = `./${config.get('database')}/test.db`; + let nedbInstance; + let db: Database; + + before(() => { + if (fs.existsSync(pathToDB)) { + fs.unlinkSync(pathToDB); + } + }); + + it('Creating db instance', async () => { + nedbInstance = new Datastore({ filename: pathToDB, autoload: true }); + db = new Database(nedbInstance); + }); + + it('Inserting document', async () => { + const data = 'Text data'; + + const insertedDoc = await db.insert({ data }) as Document; + + expect(insertedDoc).to.be.a('object'); + expect(insertedDoc.data).to.equal(data); + }); + + it('Finding document', async () => { + const data = 'Text data'; + + const insertedDoc = await db.insert({ data }) as Document; + + expect(insertedDoc).to.be.a('object'); + expect(insertedDoc.data).to.equal(data); + + const foundDoc = await db.findOne({ _id: insertedDoc._id }) as Document; + + expect(foundDoc).not.be.null; + expect(foundDoc._id).to.equal(insertedDoc._id); + expect(foundDoc.data).to.equal(data); + + const projectedDoc = await db.findOne({ _id: insertedDoc._id }, { data: 1, _id: 0 }); + + expect(Object.keys(projectedDoc).length).to.equal(1); + expect(Object.keys(projectedDoc).pop()).to.equal('data'); + }); + + it('Updating document', async () => { + const data = 'Text data'; + + const insertedDoc = await db.insert({ data }) as Document; + + expect(insertedDoc).to.be.a('object'); + expect(insertedDoc.data).to.equal(data); + + const updatedData = 'Updated text data'; + + await db.update({ _id: insertedDoc._id }, { data: updatedData }); + + const updatedDoc = await db.findOne({ _id: insertedDoc._id }) as Document; + + expect(updatedDoc).not.be.null; + expect(updatedDoc.data).not.equal(data); + expect(updatedDoc.data).to.equal(updatedData); + }); + + it('Updating documents with options', async () => { + const data = { + update: true, + data: 'Text data', + }; + + await db.insert(data); + await db.insert(data); + + let numberOfUpdatedDocs = await db.update({ update: true }, { $set: { data: 'First update' } }, { multi: true }); + + expect(numberOfUpdatedDocs).to.equal(2); + + const affectedDocs = await db.update( + { update: true }, + { $set: { data: 'Second update' } }, + { + multi: true, + returnUpdatedDocs: true, + } + ) as Array; + + expect(affectedDocs).to.be.a('array'); + affectedDocs.forEach((doc: Document) => { + expect(doc.data).to.equal('Second update'); + }); + + const upsertedDoc = await db.update({ update: true, data: 'First update' }, { $set: { data: 'Third update' } }, { upsert: true }) as Document; + + expect(upsertedDoc.update).to.be.true; + expect(upsertedDoc.data).to.equal('Third update'); + + numberOfUpdatedDocs = await db.update({ data: 'Third update' }, { $set: { data: 'Fourth update' } }, { upsert: true }); + + expect(numberOfUpdatedDocs).to.equal(1); + }); + + it('Finding documents', async () => { + const data1 = 'Text data 1'; + const data2 = 'Text data 2'; + + const insertedDoc1 = await db.insert({ data: data1, flag: true, no: 1 }) as Document; + const insertedDoc2 = await db.insert({ data: data2, flag: true, no: 2 }) as Document; + + const foundDocs = await db.find({ flag: true }) as Array; + + expect(foundDocs).to.be.a('array'); + expect(foundDocs.length).to.equal(2); + + foundDocs.sort(({ no: a }, { no: b }) => a - b); + + expect(foundDocs[0]._id).to.equal(insertedDoc1._id); + expect(foundDocs[0].data).to.equal(insertedDoc1.data); + expect(foundDocs[1]._id).to.equal(insertedDoc2._id); + expect(foundDocs[1].data).to.equal(insertedDoc2.data); + + const projectedDocs = await db.find({ flag: true }, { no: 1, _id: 0 }) as Array; + + expect(projectedDocs.length).to.equal(2); + projectedDocs.forEach(data => { + expect(Object.keys(data).length).to.equal(1); + expect(Object.keys(data).pop()).to.equal('no'); + }); + }); + + it('Removing document', async () => { + const data = 'Text data'; + + const insertedDoc = await db.insert({ data }) as Document; + + expect(insertedDoc).to.be.a('object'); + expect(insertedDoc.data).to.equal(data); + + await db.remove({ _id: insertedDoc._id }); + + const deletedDoc = await db.findOne({ _id: insertedDoc._id }); + + expect(deletedDoc).to.be.null; + }); + + it('Test invalid database queries', async () => { + try { + await db.insert({}); + } catch (err) { + expect((err as Error).message).to.equal('Cannot read property \'_id\' of undefined'); + } + + try { + await db.find({ size: { $invalidComparator: 1 } }); + } catch (err) { + expect((err as Error).message).to.equal('Unknown comparison function $invalidComparator'); + } + + try { + await db.findOne({ field: { $invalidComparator: 1 } }); + } catch (err) { + expect((err as Error).message).to.equal('Unknown comparison function $invalidComparator'); + } + + try { + await db.update({ field: { $undefinedComparator: 1 } }, {}); + } catch (err) { + expect((err as Error).message).to.equal('Unknown comparison function $undefinedComparator'); + } + + try { + await db.remove({ field: { $undefinedComparator: 1 } }); + } catch (err) { + expect((err as Error).message).to.equal('Unknown comparison function $undefinedComparator'); + } + }); + + after(() => { + if (fs.existsSync(pathToDB)) { + fs.unlinkSync(pathToDB); + } + }); +}); diff --git a/test/express.js b/src/test/express.ts similarity index 51% rename from test/express.js rename to src/test/express.ts index 79f8dec..c74dbed 100644 --- a/test/express.js +++ b/src/test/express.ts @@ -1,17 +1,19 @@ -const { app } = require('../bin/www'); -const chai = require('chai'); -const chaiHTTP = require('chai-http'); -const { expect } = chai; +import chaiHTTP from 'chai-http'; +import chai, { expect } from 'chai'; + +import server from '../bin/server'; + +const app = server.app; chai.use(chaiHTTP); describe('Express app', () => { it('App is available', async () => { - let agent = chai.request.agent(app); + const agent = chai.request.agent(app); const result = await agent .get('/'); expect(result).to.have.status(200); }); -}); +}); \ No newline at end of file diff --git a/test/models/alias.js b/src/test/models/alias.ts similarity index 89% rename from test/models/alias.js rename to src/test/models/alias.ts index 023f2c8..453457e 100644 --- a/test/models/alias.js +++ b/src/test/models/alias.ts @@ -1,14 +1,16 @@ -const {expect} = require('chai'); -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const Alias = require('../../src/models/alias'); -const {binaryMD5} = require('../../src/utils/crypto'); -const {aliases} = require('../../src/utils/database'); +import { expect } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import Alias from '../../backend/models/alias'; +import { binaryMD5 } from '../../backend/utils/crypto'; +import database from '../../backend/utils/database'; + +const aliases = database['aliases']; describe('Alias model', () => { after(() => { - const pathToDB = path.resolve(__dirname, '../../', config.database, './aliases.db'); + const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './aliases.db'); if (fs.existsSync(pathToDB)) { fs.unlinkSync(pathToDB); @@ -108,7 +110,7 @@ describe('Alias model', () => { expect(savedAlias.id).to.equal(initialData.id); expect(savedAlias.deprecated).to.equal(false); - const insertedAlias = await aliases.findOne({_id: savedAlias._id}); + const insertedAlias = await aliases.findOne({_id: savedAlias._id}) as Alias; expect(insertedAlias._id).to.equal(savedAlias._id); expect(insertedAlias.hash).to.equal(savedAlias.hash); @@ -128,7 +130,7 @@ describe('Alias model', () => { expect(alias._id).to.equal(insertedAlias._id); - const updatedAlias = await aliases.findOne({_id: alias._id}); + const updatedAlias = await aliases.findOne({_id: alias._id}) as Alias; expect(updatedAlias._id).to.equal(savedAlias._id); expect(updatedAlias.hash).to.equal(updateData.hash); diff --git a/test/models/file.js b/src/test/models/file.ts similarity index 75% rename from test/models/file.js rename to src/test/models/file.ts index 1e99b2e..f1d54a4 100644 --- a/test/models/file.js +++ b/src/test/models/file.ts @@ -1,14 +1,16 @@ -const {expect} = require('chai'); -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const File = require('../../src/models/file'); -const {files} = require('../../src/utils/database'); +import { expect } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import File from '../../backend/models/file'; +import database from '../../backend/utils/database'; + +const files = database['files']; describe('File model', () => { after(() => { - const pathToDB = path.resolve(__dirname, '../../', config.database, './files.db'); + const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './files.db'); if (fs.existsSync(pathToDB)) { fs.unlinkSync(pathToDB); @@ -20,7 +22,7 @@ describe('File model', () => { expect(file.data).to.be.a('object'); - let {data} = file; + let { data } = file; expect(data._id).to.be.undefined; expect(data.name).to.be.undefined; @@ -29,7 +31,7 @@ describe('File model', () => { expect(data.size).to.be.undefined; expect(data.mimetype).to.be.undefined; - file = new File(null); + file = new File(); data = file.data; @@ -51,8 +53,6 @@ describe('File model', () => { file = new File(initialData); - const json = file.toJSON(); - data = file.data; expect(data._id).to.equal(initialData._id); @@ -63,7 +63,7 @@ describe('File model', () => { expect(data.mimetype).to.equal(initialData.mimetype); const update = { - _id: 12345, + _id: '12345', name: 'updated filename', filename: 'updated randomname', path: '/uploads/updated randomname', @@ -94,7 +94,7 @@ describe('File model', () => { const file = new File(initialData); - let savedFile = await file.save(); + const savedFile = await file.save(); expect(savedFile._id).not.be.undefined; expect(savedFile.name).to.equal(initialData.name); @@ -103,7 +103,7 @@ describe('File model', () => { expect(savedFile.size).to.equal(initialData.size); expect(savedFile.mimetype).to.equal(initialData.mimetype); - const insertedFile = await files.findOne({_id: file._id}); + const insertedFile = await files.findOne({ _id: file._id }); expect(insertedFile._id).to.equal(file._id); expect(insertedFile.name).to.equal(file.name); @@ -113,7 +113,7 @@ describe('File model', () => { expect(insertedFile.mimetype).to.equal(file.mimetype); const updateData = { - _id: 12345, + _id: '12345', name: 'updated filename', filename: 'updated randomname', path: '/uploads/updated randomname', @@ -126,7 +126,7 @@ describe('File model', () => { expect(file._id).to.equal(insertedFile._id); - const updatedFile = await files.findOne({_id: file._id}); + const updatedFile = await files.findOne({ _id: file._id }); expect(updatedFile._id).to.equal(savedFile._id); expect(updatedFile.name).to.equal(updateData.name); @@ -139,7 +139,7 @@ describe('File model', () => { expect(file._id).to.be.undefined; - const removedFile = await files.findOne({_id: updatedFile._id}); + const removedFile = await files.findOne({ _id: updatedFile._id }); expect(removedFile).to.be.null; }); @@ -157,16 +157,18 @@ describe('File model', () => { const savedFile = await file.save(); - const foundFile = await File.get(savedFile._id); - - const {data} = foundFile; - - expect(data._id).to.equal(savedFile._id); - expect(data.name).to.equal(savedFile.name); - expect(data.filename).to.equal(savedFile.filename); - expect(data.path).to.equal(savedFile.path); - expect(data.size).to.equal(savedFile.size); - expect(data.mimetype).to.equal(savedFile.mimetype); + if (savedFile._id !== undefined){ + const foundFile = await File.get(savedFile._id); + + const { data } = foundFile; + + expect(data._id).to.equal(savedFile._id); + expect(data.name).to.equal(savedFile.name); + expect(data.filename).to.equal(savedFile.filename); + expect(data.path).to.equal(savedFile.path); + expect(data.size).to.equal(savedFile.size); + expect(data.mimetype).to.equal(savedFile.mimetype); + } await file.destroy(); }); @@ -184,16 +186,18 @@ describe('File model', () => { const savedFile = await file.save(); - const foundFile = await File.getByFilename(savedFile.filename); - - const {data} = foundFile; - - expect(data._id).to.equal(savedFile._id); - expect(data.name).to.equal(savedFile.name); - expect(data.filename).to.equal(savedFile.filename); - expect(data.path).to.equal(savedFile.path); - expect(data.size).to.equal(savedFile.size); - expect(data.mimetype).to.equal(savedFile.mimetype); + if (savedFile.filename !== undefined){ + const foundFile = await File.getByFilename(savedFile.filename); + + const { data } = foundFile; + + expect(data._id).to.equal(savedFile._id); + expect(data.name).to.equal(savedFile.name); + expect(data.filename).to.equal(savedFile.filename); + expect(data.path).to.equal(savedFile.path); + expect(data.size).to.equal(savedFile.size); + expect(data.mimetype).to.equal(savedFile.mimetype); + } await file.destroy(); }); @@ -218,7 +222,7 @@ describe('File model', () => { const savedFiles = await Promise.all(filesToSave.map(file => file.save())); - const foundFiles = await File.getAll({_id: {$in: savedFiles.map(file => file._id)}}); + const foundFiles = await File.getAll({ _id: { $in: savedFiles.map(file => file._id) } }); expect(foundFiles.length).to.equal(2); diff --git a/test/models/page.js b/src/test/models/page.ts similarity index 82% rename from test/models/page.js rename to src/test/models/page.ts index 6c99f3c..bd9d61d 100644 --- a/test/models/page.js +++ b/src/test/models/page.ts @@ -1,15 +1,16 @@ -const {expect} = require('chai'); -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const Page = require('../../src/models/page'); -const {pages} = require('../../src/utils/database'); -const translateString = require('../../src/utils/translation'); +import { expect } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import Page from '../../backend/models/page'; +import translateString from '../../backend/utils/translation'; +import database from '../../backend/utils/database'; + +const pages = database['pages']; describe('Page model', () => { - - const transformToUri = (string) => { - return translateString(string + const transformToUri = (text: string): string => { + return translateString(text .replace(/ /g, ' ') .replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ') .replace(/ +/g, ' ') @@ -20,7 +21,7 @@ describe('Page model', () => { }; after(() => { - const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); + const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './pages.db'); if (fs.existsSync(pathToDB)) { fs.unlinkSync(pathToDB); @@ -40,7 +41,7 @@ describe('Page model', () => { expect(data.body).to.be.undefined; expect(data.parent).to.be.equal('0'); - page = new Page(null); + page = new Page(); data = page.data; @@ -83,7 +84,7 @@ describe('Page model', () => { expect(json.parent).to.be.equal('0'); const update = { - _id: 12345, + _id: '12345', body: { blocks: [ { @@ -122,7 +123,7 @@ describe('Page model', () => { }; const page = new Page(initialData); - let savedPage = await page.save(); + const savedPage = await page.save(); expect(savedPage._id).not.be.undefined; expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text); @@ -188,7 +189,7 @@ describe('Page model', () => { const firstPage = new Page(initialData); let firstSavedPage = await firstPage.save(); const secondPage = new Page(initialData); - let secondSavedPage = await secondPage.save(); + const secondSavedPage = await secondPage.save(); expect(secondSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text) + '-1'); @@ -200,7 +201,7 @@ describe('Page model', () => { expect(firstSavedPage.uri).to.equal(newUri); const thirdPage = new Page(initialData); - let thirdSavedPage = await thirdPage.save(); + const thirdSavedPage = await thirdPage.save(); expect(thirdSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text)); }); @@ -222,6 +223,12 @@ describe('Page model', () => { const savedPage = await page.save(); + if (savedPage._id == undefined) { + await page.destroy(); + + return; + } + const foundPage = await Page.get(savedPage._id); const {data} = foundPage; @@ -302,26 +309,29 @@ describe('Page model', () => { } } ] - } + }, + parent: parentId, } ); - child.parent = parent; - const {_id: childId} = await child.save(); - const testedParent = await child.parent; + const testedParent = await child.getParent(); - expect(testedParent._id).to.equal(parentId); - expect(testedParent.title).to.equal(parent.body.blocks[0].data.text); - expect(testedParent.uri).to.equal(transformToUri(parent.body.blocks[0].data.text)); - expect(testedParent.body).to.deep.equal(parent.body); + expect(testedParent).to.be.not.null; + if (testedParent) { + expect(testedParent._id).to.equal(parentId); + expect(testedParent.title).to.equal(parent.body.blocks[0].data.text); + expect(testedParent.uri).to.equal(transformToUri(parent.body.blocks[0].data.text)); + expect(testedParent.body).to.deep.equal(parent.body); + } const children = await parent.children; expect(children.length).to.equal(1); - const testedChild = children.pop(); + const temp: Page|undefined = children.pop(); + const testedChild: Page = !temp ? new Page({}) : temp; expect(testedChild._id).to.equal(childId); expect(testedChild.title).to.equal(child.body.blocks[0].data.text); @@ -354,20 +364,22 @@ describe('Page model', () => { }); it('test deletion', async () => { - - const pages = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + const pageIndexes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; const orders = { '0' : ['1', '2', '3'], '1' : ['4', '5'], '5' : ['6', '7', '8'], - '3' : ['9'] - }; + '3' : ['9'], + } as { [key: string]: string[] }; + + function deleteRecursively(startFrom: string): void { + const order: string[] = orders[startFrom]; - function deleteRecursively(startFrom) { - const order = orders[startFrom]; if (!order) { - const found = pages.indexOf(startFrom); - pages.splice(found, 1); + const found = pageIndexes.indexOf(startFrom); + + pageIndexes.splice(found, 1); + return; } @@ -375,8 +387,8 @@ describe('Page model', () => { deleteRecursively(id); }); - const found = pages.indexOf(startFrom); - pages.splice(found, 1); + const found = pageIndexes.indexOf(startFrom); + pageIndexes.splice(found, 1); } }); }); diff --git a/test/models/pageOrder.js b/src/test/models/pageOrder.ts similarity index 76% rename from test/models/pageOrder.js rename to src/test/models/pageOrder.ts index 71df37d..569af52 100644 --- a/test/models/pageOrder.js +++ b/src/test/models/pageOrder.ts @@ -1,13 +1,15 @@ -const {expect} = require('chai'); -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const PageOrder = require('../../src/models/pageOrder'); -const {pagesOrder} = require('../../src/utils/database'); +import { expect } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import PageOrder from '../../backend/models/pageOrder'; +import database from '../../backend/utils/database'; + +const pagesOrder = database['pagesOrder']; describe('PageOrder model', () => { after(() => { - const pathToDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db'); + const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './pagesOrder.db'); if (fs.existsSync(pathToDB)) { fs.unlinkSync(pathToDB); @@ -15,7 +17,7 @@ describe('PageOrder model', () => { }); it('Empty Model', async () => { - let pageOrder = new PageOrder(); + const pageOrder = new PageOrder(); expect(pageOrder.data).to.be.a('object'); @@ -25,7 +27,7 @@ describe('PageOrder model', () => { expect(data.page).to.be.to.equal('0'); expect(data.order).to.be.an('array').that.is.empty; - page = new PageOrder(null); + let page = new PageOrder(); data = page.data; @@ -53,13 +55,13 @@ describe('PageOrder model', () => { order: ['1', '2'] }; const pageOrder = new PageOrder(testData); - let {data} = await pageOrder.save(); + const {data} = await pageOrder.save(); expect(data._id).not.be.undefined; expect(data.page).to.equal(testData.page); expect(data.order).to.deep.equals(testData.order); - const insertedPageOrder = await pagesOrder.findOne({_id: data._id}); + const insertedPageOrder = await pagesOrder.findOne({_id: data._id}) as PageOrder; expect(insertedPageOrder._id).to.equal(data._id); expect(insertedPageOrder.page).to.equal(data.page); expect(insertedPageOrder.order).to.deep.equal(data.order); @@ -74,7 +76,7 @@ describe('PageOrder model', () => { expect(pageOrder.data._id).to.equal(insertedPageOrder._id); - const updatedData = await pagesOrder.findOne({_id: insertedPageOrder._id}); + const updatedData = await pagesOrder.findOne({_id: insertedPageOrder._id}) as PageOrder; expect(updatedData.page).to.equal(updateData.page); expect(updatedData.order).to.deep.equal(updateData.order); @@ -97,9 +99,11 @@ describe('PageOrder model', () => { await pageOrder.save(); pageOrder.push('3'); expect(pageOrder.data.order).to.be.an('array').that.is.not.empty; - pageOrder.data.order.forEach((el) => { - expect(el).to.be.an('string') - }); + if (pageOrder.data.order !== undefined) { + pageOrder.data.order.forEach((el) => { + expect(el).to.be.an('string'); + }); + } expect(pageOrder.data.order).to.deep.equals(['1', '2', '3']); @@ -130,11 +134,13 @@ describe('PageOrder model', () => { const pageOrder = new PageOrder(testData); const insertedData = await pageOrder.save(); - const insertedPageOrder = await PageOrder.get(insertedData.data.page); - expect(insertedPageOrder).to.instanceOf(PageOrder); - expect(insertedPageOrder.data._id).to.be.equal(insertedData.data._id); + if (insertedData.data.page !== undefined) { + const insertedPageOrder = await PageOrder.get(insertedData.data.page); + expect(insertedPageOrder).to.instanceOf(PageOrder); + expect(insertedPageOrder.data._id).to.be.equal(insertedData.data._id); + } - const emptyInstance = await PageOrder.get(null); + const emptyInstance = await PageOrder.get(''); expect(emptyInstance.data.page).to.be.equal('0'); expect(emptyInstance.data.order).to.be.an('array').that.is.empty; diff --git a/test/rcparser.js b/src/test/rcparser.ts similarity index 56% rename from test/rcparser.js rename to src/test/rcparser.ts index 053952c..4172217 100644 --- a/test/rcparser.js +++ b/src/test/rcparser.ts @@ -1,18 +1,14 @@ -const {expect} = require('chai'); +import { expect } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import sinon = require('sinon'); -require('mocha-sinon'); -const fs = require('fs'); -const path = require('path'); -const config = require('../config'); -const rcParser = require('../src/utils/rcparser'); +import rcParser from '../backend/utils/rcparser'; -const rcPath = path.resolve(process.cwd(), config.rcFile); +const rcPath = path.resolve(process.cwd(), config.get('rcFile')); describe('RC file parser test', () => { - beforeEach(function () { - this.sinon.stub(console, 'log'); - }); - afterEach(() => { if (fs.existsSync(rcPath)) { fs.unlinkSync(rcPath); @@ -27,25 +23,27 @@ describe('RC file parser test', () => { it('Invalid JSON formatted config', () => { const invalidJson = '{title: "Codex Docs"}'; + const spy = sinon.spy(console, 'log'); fs.writeFileSync(rcPath, invalidJson, 'utf8'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('CodeX Docs rc file should be in JSON format.')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('CodeX Docs rc file should be in JSON format.')).to.be.true; expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS); + spy.restore(); }); it('Normal config', () => { const normalConfig = { title: 'Documentation', menu: [ - {title: 'Option 1', uri: '/option1'}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { title: 'Option 1', uri: '/option1' }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); @@ -58,10 +56,10 @@ describe('RC file parser test', () => { it('Missed title', () => { const normalConfig = { menu: [ - {title: 'Option 1', uri: '/option1'}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { title: 'Option 1', uri: '/option1' }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); @@ -74,7 +72,7 @@ describe('RC file parser test', () => { it('Missed menu', () => { const normalConfig = { - title: 'Documentation' + title: 'Documentation', }; fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); @@ -89,21 +87,23 @@ describe('RC file parser test', () => { const normalConfig = { title: 'Documentation', menu: { - 0: {title: 'Option 1', uri: '/option1'}, - 1: {title: 'Option 2', uri: '/option2'}, - 2: {title: 'Option 3', uri: '/option3'} - } + 0: { title: 'Option 1', uri: '/option1' }, + 1: { title: 'Option 2', uri: '/option2' }, + 2: { title: 'Option 3', uri: '/option3' }, + }, }; fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); + const spy = sinon.spy(console, 'log'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('Menu section in the rc file must be an array.')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('Menu section in the rc file must be an array.')).to.be.true; expect(parsedConfig.title).to.be.equal(normalConfig.title); expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu); + spy.restore(); }); it('Menu option is a string', () => { @@ -111,15 +111,15 @@ describe('RC file parser test', () => { title: 'Documentation', menu: [ 'Option 1', - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; const expectedMenu = [ - {title: 'Option 1', uri: '/option-1'}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} + { title: 'Option 1', uri: '/option-1' }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, ]; fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); @@ -134,129 +134,139 @@ describe('RC file parser test', () => { const normalConfig = { title: 'Documentation', menu: [ - [ {title: 'Option 1', uri: '/option1'} ], - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + [ { title: 'Option 1', uri: '/option1' } ], + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; const expectedMenu = [ - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, ]; + const spy = sinon.spy(console, 'log'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('Menu option #1 in rc file must be a string or an object')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('Menu option #1 in rc file must be a string or an object')).to.be.true; expect(parsedConfig.title).to.be.equal(normalConfig.title); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + spy.restore(); }); it('Menu option title is undefined', () => { const normalConfig = { title: 'Documentation', menu: [ - {uri: '/option1'}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { uri: '/option1' }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; const expectedMenu = [ - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, ]; + const spy = sinon.spy(console, 'log'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('Menu option #1 title must be a string.')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('Menu option #1 title must be a string.')).to.be.true; expect(parsedConfig.title).to.be.equal(normalConfig.title); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + spy.restore(); }); it('Menu option title is not a string', () => { const normalConfig = { title: 'Documentation', menu: [ - {title: [], uri: '/option1'}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { title: [], uri: '/option1' }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; const expectedMenu = [ - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, ]; + const spy = sinon.spy(console, 'log'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('Menu option #1 title must be a string.')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('Menu option #1 title must be a string.')).to.be.true; expect(parsedConfig.title).to.be.equal(normalConfig.title); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + spy.restore(); }); it('Menu option uri is undefined', () => { const normalConfig = { title: 'Documentation', menu: [ - {title: 'Option 1'}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { title: 'Option 1' }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; const expectedMenu = [ - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, ]; + const spy = sinon.spy(console, 'log'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('Menu option #1 uri must be a string.')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('Menu option #1 uri must be a string.')).to.be.true; expect(parsedConfig.title).to.be.equal(normalConfig.title); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + spy.restore(); }); it('Menu option title is not a string', () => { const normalConfig = { title: 'Documentation', menu: [ - {title: 'Option 1', uri: []}, - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} - ] + { title: 'Option 1', uri: [] }, + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, + ], }; const expectedMenu = [ - {title: 'Option 2', uri: '/option2'}, - {title: 'Option 3', uri: '/option3'} + { title: 'Option 2', uri: '/option2' }, + { title: 'Option 3', uri: '/option3' }, ]; + const spy = sinon.spy(console, 'log'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); const parsedConfig = rcParser.getConfiguration(); - expect(console.log.calledOnce).to.be.true; - expect(console.log.calledWith('Menu option #1 uri must be a string.')).to.be.true; + expect(spy.calledOnce).to.be.true; + expect(spy.calledWith('Menu option #1 uri must be a string.')).to.be.true; expect(parsedConfig.title).to.be.equal(normalConfig.title); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); + spy.restore(); }); }); diff --git a/test/rest/aliases.js b/src/test/rest/aliases.ts similarity index 71% rename from test/rest/aliases.js rename to src/test/rest/aliases.ts index 753099c..fe9ec3e 100644 --- a/test/rest/aliases.js +++ b/src/test/rest/aliases.ts @@ -1,29 +1,30 @@ -const {app} = require('../../bin/www'); +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import chai from 'chai'; +import chaiHTTP from 'chai-http'; +import server from '../../bin/server'; -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const chai = require('chai'); -const chaiHTTP = require('chai-http'); const {expect} = chai; +const app = server.app; chai.use(chaiHTTP); describe('Aliases REST: ', () => { - let agent; + let agent: ChaiHttp.Agent; before(async () => { agent = chai.request.agent(app); }); after(async () => { - const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); + const pathToDB = path.resolve(__dirname, '../../', config.get('database'), './pages.db'); if (fs.existsSync(pathToDB)) { fs.unlinkSync(pathToDB); } - const pathToAliasDB = path.resolve(__dirname, '../../', config.database, './aliases.db'); + const pathToAliasDB = path.resolve(__dirname, '../../', config.get('database'), './aliases.db'); if (fs.existsSync(pathToAliasDB)) { fs.unlinkSync(pathToAliasDB); diff --git a/test/rest/pages.js b/src/test/rest/pages.ts similarity index 91% rename from test/rest/pages.js rename to src/test/rest/pages.ts index bff52f9..ca73eb6 100644 --- a/test/rest/pages.js +++ b/src/test/rest/pages.ts @@ -1,22 +1,23 @@ -const {app} = require('../../bin/www'); -const model = require('../../src/models/page'); -const Page = require('../../src/models/page'); -const PageOrder = require('../../src/models/pageOrder'); -const translateString = require('../../src/utils/translation'); +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import chai from 'chai'; +import chaiHTTP from 'chai-http'; +import server from '../../bin/server'; +import model from '../../backend/models/page'; +import Page from '../../backend/models/page'; +import PageOrder from '../../backend/models/pageOrder'; +import translateString from '../../backend/utils/translation'; -const fs = require('fs'); -const path = require('path'); -const config = require('../../config'); -const chai = require('chai'); -const chaiHTTP = require('chai-http'); const {expect} = chai; +const app = server.app; chai.use(chaiHTTP); describe('Pages REST: ', () => { - let agent; - const transformToUri = (string) => { - return translateString(string + let agent: ChaiHttp.Agent; + const transformToUri = (text: string):string => { + return translateString(text .replace(/ /g, ' ') .replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ') .replace(/ +/g, ' ') @@ -31,9 +32,9 @@ describe('Pages REST: ', () => { }); after(async () => { - const pathToPagesDB = path.resolve(__dirname, '../../', config.database, './pages.db'); - const pathToPagesOrderDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db'); - const pathToAliasesDB = path.resolve(__dirname, '../../', config.database, './aliases.db'); + const pathToPagesDB = path.resolve(__dirname, '../../../', config.get('database'), './pages.db'); + const pathToPagesOrderDB = path.resolve(__dirname, '../../../', config.get('database'), './pagesOrder.db'); + const pathToAliasesDB = path.resolve(__dirname, '../../../', config.get('database'), './aliases.db'); if (fs.existsSync(pathToPagesDB)) { fs.unlinkSync(pathToPagesDB); @@ -93,15 +94,15 @@ describe('Pages REST: ', () => { it('Page data validation on create', async () => { const res = await agent .put('/api/page') - .send({someField: 'Some text'}); + .send({ someField: 'Some text' }); expect(res).to.have.status(400); expect(res).to.be.json; - const {success, error} = res.body; + const { success, error } = res.body; expect(success).to.be.false; - expect(error).to.equal('Error: Some of required fields is missed'); + expect(error).to.equal('validationError'); }); it('Finding page', async () => { @@ -363,7 +364,7 @@ describe('Pages REST: ', () => { expect(error).to.equal('Page with given id does not exist'); }); - async function createPageTree() { + async function createPageTree():Promise { /** * Creating page tree * @@ -474,7 +475,7 @@ describe('Pages REST: ', () => { } it('Removing a page and its children', async () => { - let pages = await createPageTree(); + const pages = await createPageTree(); /** * Deleting from tree page1 diff --git a/test/rest/test_file.json b/src/test/rest/test_file.json similarity index 100% rename from test/rest/test_file.json rename to src/test/rest/test_file.json diff --git a/test/rest/test_image.png b/src/test/rest/test_image.png similarity index 100% rename from test/rest/test_image.png rename to src/test/rest/test_image.png diff --git a/test/rest/transport.js b/src/test/rest/transport.ts similarity index 71% rename from test/rest/transport.js rename to src/test/rest/transport.ts index da375f0..ccee062 100644 --- a/test/rest/transport.js +++ b/src/test/rest/transport.ts @@ -1,44 +1,44 @@ -const fs = require('fs'); -const path = require('path'); -const fileType = require('file-type'); -const chai = require('chai'); -const chaiHTTP = require('chai-http'); -const rimraf = require('rimraf'); +import fs from 'fs'; +import path from 'path'; +import fileType from 'file-type'; +import chai from 'chai'; +import chaiHTTP from 'chai-http'; +import rimraf from 'rimraf'; +import config from 'config'; +import server from '../../bin/server'; +import model from '../../backend/models/file'; + const {expect} = chai; - -const {app} = require('../../bin/www'); -const model = require('../../src/models/file'); - -const config = require('../../config'); +const app = server.app; chai.use(chaiHTTP); describe('Transport routes: ', () => { - let agent; + let agent: ChaiHttp.Agent; before(async () => { agent = chai.request.agent(app); - if (!fs.existsSync('./' + config.uploads)) { - fs.mkdirSync('./' + config.uploads); + if (!fs.existsSync('./' + config.get('uploads'))) { + fs.mkdirSync('./' + config.get('uploads')); } }); after(async () => { - const pathToDB = path.resolve(__dirname, '../../', config.database, './files.db'); + const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './files.db'); if (fs.existsSync(pathToDB)) { fs.unlinkSync(pathToDB); } - if (fs.existsSync('./' + config.uploads)) { - rimraf.sync('./' + config.uploads); + if (fs.existsSync('./' + config.get('uploads'))) { + rimraf.sync('./' + config.get('uploads')); } }); it('Uploading an image', async () => { const name = 'test_image.png'; - const image = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const image = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); const res = await agent .post('/api/transport/image') .attach('image', image, name); @@ -55,19 +55,27 @@ describe('Transport routes: ', () => { expect(file.name).to.equal(name); expect(file.filename).to.equal(body.filename); expect(file.path).to.equal(body.path); - expect(file.mimetype).to.equal(fileType(image).mime); - expect(file.size).to.equal(image.byteLength); - const getRes = await agent - .get(file.path); + const type = await fileType.fromBuffer(image); + expect(type).to.be.not.undefined; + if (type !== undefined) { + expect(file.mimetype).to.equal(type.mime); + expect(file.size).to.equal(image.byteLength); - expect(getRes).to.have.status(200); - expect(getRes).to.have.header('content-type', fileType(image).mime); + expect(file.path).to.be.not.undefined; + if (file.path !== undefined) { + const getRes = await agent + .get(file.path); + + expect(getRes).to.have.status(200); + expect(getRes).to.have.header('content-type', type.mime); + } + } }); it('Uploading an image with map option', async () => { const name = 'test_image.png'; - const image = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const image = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); const res = await agent .post('/api/transport/image') .attach('image', image, name) @@ -88,7 +96,7 @@ describe('Transport routes: ', () => { it('Uploading a file', async () => { const name = 'test_file.json'; - const json = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const json = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); const res = await agent .post('/api/transport/file') .attach('file', json, name); @@ -107,16 +115,19 @@ describe('Transport routes: ', () => { expect(file.path).to.equal(body.path); expect(file.size).to.equal(json.byteLength); - const getRes = await agent - .get(file.path); - - expect(getRes).to.have.status(200); - expect(getRes).to.have.header('content-type', new RegExp(`^${file.mimetype}`)); + expect(file.path).to.be.not.undefined; + if (file.path !== undefined){ + const getRes = await agent + .get(file.path); + + expect(getRes).to.have.status(200); + expect(getRes).to.have.header('content-type', new RegExp(`^${file.mimetype}`)); + } }); it('Uploading a file with map option', async () => { const name = 'test_file.json'; - const json = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const json = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); const res = await agent .post('/api/transport/file') .attach('file', json, name) @@ -155,11 +166,14 @@ describe('Transport routes: ', () => { expect(file.path).to.equal(body.path); expect(file.size).to.equal(body.size); - const getRes = await agent - .get(file.path); - - expect(getRes).to.have.status(200); - expect(getRes).to.have.header('content-type', file.mimetype); + expect(file.path).to.be.not.undefined; + if (file.path !== undefined){ + const getRes = await agent + .get(file.path); + + expect(getRes).to.have.status(200); + expect(getRes).to.have.header('content-type', file.mimetype); + } }); it('Send an file URL to fetch with map option', async () => { @@ -193,7 +207,7 @@ describe('Transport routes: ', () => { expect(body.success).to.equal(0); const name = 'test_file.json'; - const json = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const json = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); res = await agent .post('/api/transport/file') .attach('file', json, name) @@ -216,7 +230,7 @@ describe('Transport routes: ', () => { expect(body.success).to.equal(0); let name = 'test_file.json'; - const json = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const json = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); res = await agent .post('/api/transport/image') .attach('image', json, name); @@ -224,7 +238,7 @@ describe('Transport routes: ', () => { expect(res).to.have.status(400); name = 'test_image.png'; - const image = fs.readFileSync(path.resolve(`./test/rest/${name}`)); + const image = fs.readFileSync(path.resolve(`./src/test/rest/${name}`)); res = await agent .post('/api/transport/image') .attach('image', image, name) diff --git a/src/utils/asyncMiddleware.js b/src/utils/asyncMiddleware.js deleted file mode 100644 index d9fba14..0000000 --- a/src/utils/asyncMiddleware.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Helper for making async middlewares for express router - * - * @param fn - * @returns {function(*=, *=, *=)} - */ -module.exports = function asyncMiddleware(fn) { - return (req, res, next) => { - Promise.resolve(fn(req, res, next)) - .catch(next); - }; -}; diff --git a/src/utils/database/aliases.js b/src/utils/database/aliases.js deleted file mode 100644 index 2770232..0000000 --- a/src/utils/database/aliases.js +++ /dev/null @@ -1,6 +0,0 @@ -const Datastore = require('nedb'); -const config = require('../../../config'); - -const db = new Datastore({ filename: `./${config.database}/aliases.db`, autoload: true }); - -module.exports = db; diff --git a/src/utils/database/files.js b/src/utils/database/files.js deleted file mode 100644 index aaeb816..0000000 --- a/src/utils/database/files.js +++ /dev/null @@ -1,6 +0,0 @@ -const Datastore = require('nedb'); -const config = require('../../../config'); - -const db = new Datastore({ filename: `./${config.database}/files.db`, autoload: true }); - -module.exports = db; diff --git a/src/utils/database/pages.js b/src/utils/database/pages.js deleted file mode 100644 index 115738e..0000000 --- a/src/utils/database/pages.js +++ /dev/null @@ -1,6 +0,0 @@ -const Datastore = require('nedb'); -const config = require('../../../config'); - -const db = new Datastore({ filename: `./${config.database}/pages.db`, autoload: true }); - -module.exports = db; diff --git a/src/utils/database/pagesOrder.js b/src/utils/database/pagesOrder.js deleted file mode 100644 index 253b72c..0000000 --- a/src/utils/database/pagesOrder.js +++ /dev/null @@ -1,5 +0,0 @@ -const Datastore = require('nedb'); -const config = require('../../../config'); -const db = new Datastore({ filename: `./${config.database}/pagesOrder.db`, autoload: true }); - -module.exports = db; diff --git a/src/utils/database/password.js b/src/utils/database/password.js deleted file mode 100644 index 33d0295..0000000 --- a/src/utils/database/password.js +++ /dev/null @@ -1,6 +0,0 @@ -const Datastore = require('nedb'); -const config = require('../../../config'); - -const db = new Datastore({ filename: `./${config.database}/password.db`, autoload: true }); - -module.exports = db; diff --git a/test/database.js b/test/database.js deleted file mode 100644 index af4e306..0000000 --- a/test/database.js +++ /dev/null @@ -1,179 +0,0 @@ -const fs = require('fs'); -const config = require('../config'); -const {expect} = require('chai'); - -const {class: Database} = require('../src/utils/database'); -const Datastore = require('nedb'); - -describe('Database', () => { - const pathToDB = `./${config.database}/test.db`; - let nedbInstance; - let db; - - before(() => { - if (fs.existsSync(pathToDB)) { - fs.unlinkSync(pathToDB); - } - }); - - it('Creating db instance', async () => { - nedbInstance = new Datastore({filename: pathToDB, autoload: true}); - db = new Database(nedbInstance); - }); - - it('Inserting document', async () => { - const data = 'Text data'; - - const insertedDoc = await db.insert({data}); - - expect(insertedDoc).to.be.a('object'); - expect(insertedDoc.data).to.equal(data); - }); - - it('Finding document', async () => { - const data = 'Text data'; - - const insertedDoc = await db.insert({data}); - - expect(insertedDoc).to.be.a('object'); - expect(insertedDoc.data).to.equal(data); - - const foundDoc = await db.findOne({_id: insertedDoc._id}); - - expect(foundDoc).not.be.null; - expect(foundDoc._id).to.equal(insertedDoc._id); - expect(foundDoc.data).to.equal(data); - - const projectedDoc = await db.findOne({_id: insertedDoc._id}, {data: 1, _id: 0}); - - expect(Object.keys(projectedDoc).length).to.equal(1); - expect(Object.keys(projectedDoc).pop()).to.equal('data'); - }); - - it('Updating document', async () => { - const data = 'Text data'; - - const insertedDoc = await db.insert({data}); - - expect(insertedDoc).to.be.a('object'); - expect(insertedDoc.data).to.equal(data); - - const updatedData = 'Updated text data'; - - await db.update({_id: insertedDoc._id}, {data: updatedData}); - - const updatedDoc = await db.findOne({_id: insertedDoc._id}); - - expect(updatedDoc).not.be.null; - expect(updatedDoc.data).not.equal(data); - expect(updatedDoc.data).to.equal(updatedData); - }); - - it('Updating documents with options', async () => { - const data = {update: true, data: 'Text data'}; - - await db.insert(data); - await db.insert(data); - - let numberOfUpdatedDocs = await db.update({update: true}, {$set: {data: 'First update'}}, {multi: true}); - - expect(numberOfUpdatedDocs).to.equal(2); - - const affectedDocs = await db.update({update: true}, {$set: {data: 'Second update'}}, {multi: true, returnUpdatedDocs: true}); - - expect(affectedDocs).to.be.a('array'); - affectedDocs.forEach(doc => { - expect(doc.data).to.equal('Second update'); - }); - - const upsertedDoc = await db.update({update: true, data: 'First update'}, {$set: {data: 'Third update'}}, {upsert: true}); - - expect(upsertedDoc.update).to.be.true; - expect(upsertedDoc.data).to.equal('Third update'); - - numberOfUpdatedDocs = await db.update({data: 'Third update'}, {$set: {data: 'Fourth update'}}, {upsert: true}); - - expect(numberOfUpdatedDocs).to.equal(1); - }); - - it('Finding documents', async () => { - const data1 = 'Text data 1'; - const data2 = 'Text data 2'; - - const insertedDoc1 = await db.insert({data: data1, flag: true, no: 1}); - const insertedDoc2 = await db.insert({data: data2, flag: true, no: 2}); - - const foundDocs = await db.find({flag: true}); - - expect(foundDocs).to.be.a('array'); - expect(foundDocs.length).to.equal(2); - - foundDocs.sort(({no: a}, {no: b}) => a - b); - - expect(foundDocs[0]._id).to.equal(insertedDoc1._id); - expect(foundDocs[0].data).to.equal(insertedDoc1.data); - expect(foundDocs[1]._id).to.equal(insertedDoc2._id); - expect(foundDocs[1].data).to.equal(insertedDoc2.data); - - const projectedDocs = await db.find({flag: true}, {no: 1, _id: 0}); - - expect(projectedDocs.length).to.equal(2); - projectedDocs.forEach(data => { - expect(Object.keys(data).length).to.equal(1); - expect(Object.keys(data).pop()).to.equal('no'); - }); - }); - - it('Removing document', async () => { - const data = 'Text data'; - - const insertedDoc = await db.insert({data}); - - expect(insertedDoc).to.be.a('object'); - expect(insertedDoc.data).to.equal(data); - - await db.remove({_id: insertedDoc._id}); - - const deletedDoc = await db.findOne({_id: insertedDoc._id}); - - expect(deletedDoc).to.be.null; - }); - - it('Test invalid database queries', async () => { - try { - await db.insert(); - } catch (err) { - expect(err.message).to.equal('Cannot read property \'_id\' of undefined'); - } - - try { - await db.find({size: {$invalidComparator: 1}}); - } catch (err) { - expect(err.message).to.equal('Unknown comparison function $invalidComparator'); - } - - try { - await db.findOne({field: {$invalidComparator: 1}}); - } catch (err) { - expect(err.message).to.equal('Unknown comparison function $invalidComparator'); - } - - try { - await db.update({field: {$undefinedComparator: 1}}); - } catch (err) { - expect(err.message).to.equal('Unknown comparison function $undefinedComparator'); - } - - try { - await db.remove({field: {$undefinedComparator: 1}}); - } catch (err) { - expect(err.message).to.equal('Unknown comparison function $undefinedComparator'); - } - }); - - after(() => { - if (fs.existsSync(pathToDB)) { - fs.unlinkSync(pathToDB); - } - }); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..68155e3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,72 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist/", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}