1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-18 20:59:42 +02:00

Typescript rewrite (#147)

* Updated highlight.js

* Update .codexdocsrc.sample

remove undefined page for a fresh new install

* backend rewritten in TS

* test -> TS, .added dockerignore, bug fixed

* Removed compiled js files, eslint codex/ts added

* fixed jsdocs warning, leaving editor confirmation

* use path.resolve for DB paths

* db drives updated + fixed User model

* redundant cleared + style fixed

* explicit type fixing

* fixing testing code

* added body block type

* compiled JS files -> dist, fixed compiling errors

* fixed compiling error, re-organized ts source code

* updated Dockerfile

* fixed link to parent page

* up nodejs version

* fix package name

* fix deps

Co-authored-by: nvc8996 <nvc.8996@gmail.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
This commit is contained in:
Nikita Melnikov 2022-03-05 22:57:23 +04:00 committed by GitHub
parent 059cfb96f9
commit 34514761f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 3817 additions and 2249 deletions

View file

@ -1,18 +1,25 @@
{ {
"extends": [ "extends": [
"codex" "codex/ts",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
], ],
"plugins": [ "plugins": [
"chai-friendly" "chai-friendly",
"@typescript-eslint"
], ],
"env": { "env": {
"mocha": true "mocha": true
}, },
"rules": { "rules": {
"no-unused-expressions": 0, "no-unused-expressions": 1,
"chai-friendly/no-unused-expressions": 2 "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": { "globals": {
"fetch": true, "fetch": true,
"alert": true "alert": true

3
.gitignore vendored
View file

@ -76,3 +76,6 @@ typings/
# Uploads # Uploads
/public/uploads /public/uploads
/public/uploads_test /public/uploads_test
# Compiled files
/dist/*

2
.nvmrc
View file

@ -1 +1 @@
9.11.1 16.14.0

View file

@ -33,6 +33,10 @@ $ yarn install --frozen-lockfile
``` ```
### Available scripts ### Available scripts
#### Compile to Javascript
```
$ yarn compile
```
#### Start the server #### Start the server

View file

@ -1,6 +1,7 @@
{ {
"port": 3000, "port": 3000,
"database": ".db", "database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "public/uploads", "uploads": "public/uploads",
"secret": "iamasecretstring" "secret": "iamasecretstring"
} }

View file

@ -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;

View file

@ -1,6 +1,7 @@
{ {
"port": 3000, "port": 3000,
"database": ".db", "database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "public/uploads", "uploads": "public/uploads",
"secret": "iamasecretstring" "secret": "iamasecretstring"
} }

View file

@ -1,7 +1,7 @@
{ {
"port": 3001, "port": 3001,
"database": ".testdb", "database": ".testdb",
"rcFile": "./test/.codexdocsrc", "rcFile": "./src/test/.codexdocsrc",
"uploads": "public/uploads_test", "uploads": "public/uploads_test",
"secret": "iamasecretstring" "secret": "iamasecretstring"
} }

View file

@ -1,12 +1,14 @@
FROM node:12.14.1-alpine3.11 FROM node:16.14.0-alpine3.15
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apk add --no-cache git gcc g++ python make musl-dev RUN apk add --no-cache git gcc g++ python3 make musl-dev
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn install --prod RUN yarn install
COPY . . COPY . .
RUN yarn compile
CMD ["yarn", "start"] CMD ["yarn", "start"]

View file

@ -1,11 +1,15 @@
{ {
"ignore": [ "ignore": [
"node_modules", "node_modules",
"src/frontend", "src/frontend",
"public/dist" "public/dist"
], ],
"events": { "events": {
"restart": "echo \"App restarted due to: '$FILENAME'\"" "restart": "echo \"App restarted due to: '$FILENAME'\""
}, },
"ext": "js,twig" "watch": ["src/"],
"execMap": {
"ts": "node -r ts-node/register"
},
"ext": "js,json,ts,twig"
} }

View file

@ -9,40 +9,45 @@
"> 1%" "> 1%"
], ],
"scripts": { "scripts": {
"start": "cross-env NODE_ENV=production nodemon ./bin/www", "start:ts": "cross-env NODE_ENV=production nodemon --config nodemon.json ./src/bin/server.ts",
"start:dev": "cross-env NODE_ENV=development nodemon ./bin/www", "start:dev": "cross-env NODE_ENV=development nodemon --config nodemon.json ./src/bin/server.ts",
"test": "cross-env NODE_ENV=testing mocha --recursive ./test", "test": "cross-env NODE_ENV=testing mocha --recursive ./dist/test --exit",
"lint": "eslint --fix --cache ./src/**/*.js", "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": "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", "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", "precommit": "yarn lint && yarn test:ts",
"generatePassword": "node ./generatePassword.js", "generatePassword:ts": "ts-node ./src/generatePassword.ts",
"editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest" "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": { "dependencies": {
"@editorjs/embed": "^2.5.0", "@editorjs/embed": "^2.5.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"commander": "^2.19.0", "commander": "^8.1.0",
"cookie-parser": "~1.4.3", "config": "^3.3.6",
"cross-env": "^5.2.0", "cookie-parser": "^1.4.5",
"csurf": "^1.9.0", "cross-env": "^7.0.3",
"debug": "~4.1.0", "csurf": "^1.11.0",
"dotenv": "^6.2.0", "debug": "^4.3.2",
"express": "~4.16.0", "dotenv": "^10.0.0",
"file-type": "^10.7.1", "express": "^4.17.1",
"http-errors": "~1.7.1", "file-type": "^16.5.2",
"jsonwebtoken": "^8.4.0", "http-errors": "^1.8.0",
"mime": "^2.4.0", "jsonwebtoken": "^8.5.1",
"mkdirp": "^0.5.1", "mime": "^2.5.2",
"morgan": "~1.9.0", "mkdirp": "^1.0.4",
"multer": "^1.3.1", "morgan": "^1.10.0",
"multer": "^1.4.2",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"nodemon": "^1.18.3", "nodemon": "^2.0.12",
"open-graph-scraper": "^4.5.0", "open-graph-scraper": "^4.9.0",
"twig": "~1.12.0", "ts-node": "^10.1.0",
"twig": "^1.15.4",
"typescript-eslint": "^0.0.1-alpha.0", "typescript-eslint": "^0.0.1-alpha.0",
"uuid4": "^1.0.0" "uuid4": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
@ -63,6 +68,31 @@
"@editorjs/raw": "^2.3.0", "@editorjs/raw": "^2.3.0",
"@editorjs/table": "^2.0.1", "@editorjs/table": "^2.0.1",
"@editorjs/warning": "^1.2.0", "@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", "autoprefixer": "^9.1.3",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
@ -71,17 +101,16 @@
"chai-http": "^4.0.0", "chai-http": "^4.0.0",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"cssnano": "^4.1.0", "cssnano": "^4.1.0",
"eslint": "^6.8.0", "eslint": "^7.31.0",
"eslint-config-codex": "^1.3.4", "eslint-config-codex": "^1.6.2",
"eslint-plugin-chai-friendly": "^0.4.1", "eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.14.0", "eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.1", "eslint-plugin-node": "^8.0.1",
"eslint-plugin-standard": "^4.0.0",
"highlight.js": "^11.1.0", "highlight.js": "^11.1.0",
"husky": "^1.1.2", "husky": "^1.1.2",
"mini-css-extract-plugin": "^0.4.3", "mini-css-extract-plugin": "^0.4.3",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mocha-sinon": "^2.1.0", "mocha-sinon": "^2.1.2",
"module-dispatcher": "^2.0.0", "module-dispatcher": "^2.0.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"nyc": "^13.1.0", "nyc": "^13.1.0",
@ -99,8 +128,10 @@
"postcss-nested-ancestors": "^2.0.0", "postcss-nested-ancestors": "^2.0.0",
"postcss-nesting": "^7.0.0", "postcss-nesting": "^7.0.0",
"postcss-smart-import": "^0.7.6", "postcss-smart-import": "^0.7.6",
"rimraf": "^2.6.3", "rimraf": "^3.0.2",
"sinon": "^7.0.0", "sinon": "^11.1.2",
"ts-mocha": "^8.0.0",
"typescript": "^4.3.5",
"webpack": "^4.17.1", "webpack": "^4.17.1",
"webpack-cli": "^3.1.0" "webpack-cli": "^3.1.0"
} }

View file

@ -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;

38
src/backend/app.ts Normal file
View file

@ -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;

View file

@ -1,4 +1,4 @@
const Alias = require('../models/alias'); import Alias from '../models/alias';
/** /**
* @class Aliases * @class Aliases
@ -11,7 +11,7 @@ class Aliases {
* @param {string} aliasName - alias name of entity * @param {string} aliasName - alias name of entity
* @returns {Promise<Alias>} * @returns {Promise<Alias>}
*/ */
static async get(aliasName) { public static async get(aliasName: string): Promise<Alias> {
const alias = await Alias.get(aliasName); const alias = await Alias.get(aliasName);
if (!alias.id) { if (!alias.id) {
@ -22,4 +22,4 @@ class Aliases {
} }
} }
module.exports = Aliases; export default Aliases;

View file

@ -1,5 +1,7 @@
const Model = require('../models/page'); import Page, { PageData } from '../models/page';
const Alias = require('../models/alias'); import Alias from '../models/alias';
type PageDataFields = keyof PageData;
/** /**
* @class Pages * @class Pages
@ -11,7 +13,7 @@ class Pages {
* *
* @returns {['title', 'body']} * @returns {['title', 'body']}
*/ */
static get REQUIRED_FIELDS() { public static get REQUIRED_FIELDS(): Array<PageDataFields> {
return [ 'body' ]; return [ 'body' ];
} }
@ -21,8 +23,8 @@ class Pages {
* @param {string} id - page id * @param {string} id - page id
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
static async get(id) { public static async get(id: string): Promise<Page> {
const page = await Model.get(id); const page = await Page.get(id);
if (!page._id) { if (!page._id) {
throw new Error('Page with given id does not exist'); throw new Error('Page with given id does not exist');
@ -36,8 +38,8 @@ class Pages {
* *
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
static async getAll() { public static async getAll(): Promise<Page[]> {
return Model.getAll(); return Page.getAll();
} }
/** /**
@ -46,20 +48,28 @@ class Pages {
* @param {string} parent - id of current page * @param {string} parent - id of current page
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
static async getAllExceptChildren(parent) { public static async getAllExceptChildren(parent: string): Promise<Page[]> {
const pagesAvailable = this.removeChildren(await Pages.getAll(), parent); 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 * Set all children elements to null
* *
* @param {Page[]} [pagesAvailable] - Array of all pages * @param {Array<Page|null>} [pagesAvailable] - Array of all pages
* @param {string} parent - id of parent page * @param {string} parent - id of parent page
* @returns {Array<?Page>} * @returns {Array<?Page>}
*/ */
static removeChildren(pagesAvailable, parent) { public static removeChildren(pagesAvailable: Array<Page|null>, parent: string | undefined): Array<Page | null> {
pagesAvailable.forEach(async (item, index) => { pagesAvailable.forEach(async (item, index) => {
if (item === null || item._parent !== parent) { if (item === null || item._parent !== parent) {
return; return;
@ -74,14 +84,14 @@ class Pages {
/** /**
* Create new page model and save it in the database * Create new page model and save it in the database
* *
* @param {PageData} data * @param {PageData} data - info about page
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
static async insert(data) { public static async insert(data: PageData): Promise<Page> {
try { try {
Pages.validate(data); Pages.validate(data);
const page = new Model(data); const page = new Page(data);
const insertedPage = await page.save(); const insertedPage = await page.save();
@ -95,18 +105,80 @@ class Pages {
} }
return insertedPage; return insertedPage;
} catch (validationError) { } catch (e) {
throw new Error(validationError); 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<Page>}
*/
public static async update(id: string, data: PageData): Promise<Page> {
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<Page>}
*/
public static async remove(id: string): Promise<Page> {
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 * Check PageData object for required fields
* *
* @param {PageData} data * @param {PageData} data - info about page
* @throws {Error} - validation error * @throws {Error} - validation error
*/ */
static validate(data) { private static validate(data: PageData): void {
const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined'); const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
if (!allRequiredFields) { if (!allRequiredFields) {
@ -131,64 +203,6 @@ class Pages {
throw new Error('Please, fill page Header'); 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<Page>}
*/
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<Page>}
*/
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;

View file

@ -1,4 +1,5 @@
const Model = require('../models/pageOrder'); import PageOrder from '../models/pageOrder';
import Page from '../models/page';
/** /**
* @class PagesOrder * @class PagesOrder
@ -13,8 +14,8 @@ class PagesOrder {
* @param {string} parentId - of which page we want to get children order * @param {string} parentId - of which page we want to get children order
* @returns {Promise<PageOrder>} * @returns {Promise<PageOrder>}
*/ */
static async get(parentId) { public static async get(parentId: string): Promise<PageOrder> {
const order = await Model.get(parentId); const order = await PageOrder.get(parentId);
if (!order._id) { if (!order._id) {
throw new Error('Page with given id does not contain order'); 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 all records about page's order
* *
* @returns {Promise<PagesOrder[]>} * @returns {Promise<PageOrder[]>}
*/ */
static async getAll() { public static async getAll(): Promise<PageOrder[]> {
return Model.getAll(); return PageOrder.getAll();
} }
/** /**
@ -38,8 +39,8 @@ class PagesOrder {
* @param {string} parentId - parent page's id * @param {string} parentId - parent page's id
* @param {string} childId - new page pushed to the order * @param {string} childId - new page pushed to the order
*/ */
static async push(parentId, childId) { public static async push(parentId: string, childId: string): Promise<void> {
const order = await Model.get(parentId); const order = await PageOrder.get(parentId);
order.push(childId); order.push(childId);
await order.save(); await order.save();
@ -52,13 +53,13 @@ class PagesOrder {
* @param {string} newParentId - new parent page's id * @param {string} newParentId - new parent page's id
* @param {string} targetPageId - page's id which is changing the parent page * @param {string} targetPageId - page's id which is changing the parent page
*/ */
static async move(oldParentId, newParentId, targetPageId) { public static async move(oldParentId: string, newParentId: string, targetPageId: string): Promise<void> {
const oldParentOrder = await Model.get(oldParentId); const oldParentOrder = await PageOrder.get(oldParentId);
oldParentOrder.remove(targetPageId); oldParentOrder.remove(targetPageId);
await oldParentOrder.save(); await oldParentOrder.save();
const newParentOrder = await Model.get(newParentId); const newParentOrder = await PageOrder.get(newParentId);
newParentOrder.push(targetPageId); newParentOrder.push(targetPageId);
await newParentOrder.save(); await newParentOrder.save();
@ -73,14 +74,14 @@ class PagesOrder {
* @param {boolean} ignoreSelf - should we ignore current page in list or not * @param {boolean} ignoreSelf - should we ignore current page in list or not
* @returns {Page[]} * @returns {Page[]}
*/ */
static async getOrderedChildren(pages, currentPageId, parentPageId, ignoreSelf = false) { public static async getOrderedChildren(pages: Page[], currentPageId: string, parentPageId: string, ignoreSelf = false): Promise<Page[]> {
const children = await Model.get(parentPageId); const children = await PageOrder.get(parentPageId);
const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id); const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id);
// Create unique array with ordered and unordered pages 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 => { ordered.forEach(pageId => {
pages.forEach(page => { 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} currentPageId - page's id that changes the order
* @param {string} parentPageId - parent page's id that contains both two pages * @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 * @param {string} putAbovePageId - page's id above which we put the target page
*/ */
static async update(unordered, currentPageId, parentPageId, putAbovePageId) { public static async update(unordered: string[], currentPageId: string, parentPageId: string, putAbovePageId: string): Promise<void> {
const pageOrder = await Model.get(parentPageId); const pageOrder = await PageOrder.get(parentPageId);
// Create unique array with ordered and unordered pages id // 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); pageOrder.putAbove(currentPageId, putAbovePageId);
await pageOrder.save(); await pageOrder.save();
} }
/** /**
* @param parentId * @param {string} parentId - identity of parent page
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async remove(parentId) { public static async remove(parentId: string): Promise<void> {
const order = await Model.get(parentId); const order = await PageOrder.get(parentId);
if (!order._id) { if (!order._id) {
throw new Error('Page with given id does not contain order'); throw new Error('Page with given id does not contain order');
@ -123,4 +124,4 @@ class PagesOrder {
} }
} }
module.exports = PagesOrder; export default PagesOrder;

View file

@ -1,12 +1,17 @@
const fileType = require('file-type'); import fileType from 'file-type';
const fetch = require('node-fetch'); import fetch from 'node-fetch';
const fs = require('fs'); import fs from 'fs';
const nodePath = require('path'); 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 = crypto.random16;
const { random16 } = require('../utils/crypto');
const { deepMerge } = require('../utils/objects'); interface Dict {
const config = require('../../config'); [key: string]: any;
}
/** /**
* @class Transport * @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 * @param {object} map - object that represents how should fields of File object should be mapped to response
* @returns {Promise<FileData>} * @returns {Promise<FileData>}
*/ */
static async save(multerData, map) { public static async save(multerData: Dict, map: Dict): Promise<FileData> {
const { originalname: name, path, filename, size, mimetype } = multerData; const { originalname: name, path, filename, size, mimetype } = multerData;
const file = new Model({ const file = new File({
name, name,
filename, filename,
path, path,
@ -57,22 +62,33 @@ class Transport {
* @param {object} map - object that represents how should fields of File object should be mapped to response * @param {object} map - object that represents how should fields of File object should be mapped to response
* @returns {Promise<FileData>} * @returns {Promise<FileData>}
*/ */
static async fetch(url, map) { public static async fetch(url: string, map: Dict): Promise<FileData> {
const fetchedFile = await fetch(url); const fetchedFile = await fetch(url);
const buffer = await fetchedFile.buffer(); const buffer = await fetchedFile.buffer();
const filename = await random16(); const filename = await random16();
const type = fileType(buffer); const type = await fileType.fromBuffer(buffer);
const ext = type ? type.ext : nodePath.extname(url).slice(1); 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, name: url,
filename: `${filename}.${ext}`, filename: `${filename}.${ext}`,
path: `${config.uploads}/${filename}.${ext}`, path: `${config.get('uploads')}/${filename}.${ext}`,
size: buffer.length, size: buffer.length,
mimetype: type ? type.mime : fetchedFile.headers.get('content-type'), mimetype: mimeType,
}); });
await file.save(); await file.save();
@ -89,19 +105,19 @@ class Transport {
/** /**
* Map fields of File object to response by provided map object * 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 * @param {object} map - object that represents how should fields of File object should be mapped to response
* *
*/ */
static composeResponse(file, map) { public static composeResponse(file: File, map: Dict): Dict {
const response = {}; const response: Dict = {};
const { data } = file; const { data } = file;
Object.entries(map).forEach(([name, path]) => { Object.entries(map).forEach(([name, path]) => {
const fields = path.split(':'); const fields: string[] = path.split(':');
if (fields.length > 1) { if (fields.length > 1) {
let object = {}; let object: Dict = {};
const result = object; const result = object;
fields.forEach((field, i) => { fields.forEach((field, i) => {
@ -125,4 +141,4 @@ class Transport {
} }
} }
module.exports = Transport; export default Transport;

View file

@ -0,0 +1,20 @@
import User from '../models/user';
/**
* @class Users
* @classdesc Users controller
*/
class Users {
/**
* Find and return user model.
*
* @returns {Promise<User>}
*/
public static async get(): Promise<User> {
const userData: User = await User.get();
return userData;
}
}
export default Users;

View file

@ -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;

View file

@ -1,5 +1,8 @@
const { aliases: aliasesDb } = require('../utils/database/index'); import crypto from '../utils/crypto';
const { binaryMD5 } = require('../utils/crypto'); import database from '../utils/database/index';
const binaryMD5 = crypto.binaryMD5;
const aliasesDb = database['aliases'];
/** /**
* @typedef {object} AliasData * @typedef {object} AliasData
@ -10,6 +13,13 @@ const { binaryMD5 } = require('../utils/crypto');
* @property {string} id - entity id * @property {string} id - entity id
* *
*/ */
export interface AliasData {
_id?: string;
hash?: string;
type?: string;
deprecated?: boolean;
id?: string;
}
/** /**
* @class Alias * @class Alias
@ -22,16 +32,40 @@ const { binaryMD5 } = require('../utils/crypto');
* @property {string} id - entity title * @property {string} id - entity title
*/ */
class Alias { 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 * Return Alias types
* *
* @returns {object} * @returns {object}
*/ */
static get types() { public static get types(): { PAGE: string } {
return { return {
PAGE: 'page', PAGE: 'page',
}; };
}; }
/** /**
* Find and return alias with given alias * Find and return alias with given alias
@ -39,7 +73,7 @@ class Alias {
* @param {string} aliasName - alias of entity * @param {string} aliasName - alias of entity
* @returns {Promise<Alias>} * @returns {Promise<Alias>}
*/ */
static async get(aliasName) { public static async get(aliasName: string): Promise<Alias> {
const hash = binaryMD5(aliasName); const hash = binaryMD5(aliasName);
let data = await aliasesDb.findOne({ let data = await aliasesDb.findOne({
hash: hash, hash: hash,
@ -54,22 +88,17 @@ class Alias {
} }
/** /**
* @class * Mark alias as deprecated
* *
* @param {AliasData} data
* @param {string} aliasName - alias of entity * @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/ */
constructor(data = {}, aliasName = '') { public static async markAsDeprecated(aliasName: string): Promise<Alias> {
if (data === null) { const alias = await Alias.get(aliasName);
data = {};
} alias.deprecated = true;
if (data._id) {
this._id = data._id; return alias.save();
}
if (aliasName) {
this.hash = binaryMD5(aliasName);
}
this.data = data;
} }
/** /**
@ -77,9 +106,9 @@ class Alias {
* *
* @returns {Promise<Alias>} * @returns {Promise<Alias>}
*/ */
async save() { public async save(): Promise<Alias> {
if (!this._id) { if (!this._id) {
const insertedRow = await aliasesDb.insert(this.data); const insertedRow = await aliasesDb.insert(this.data) as { _id: string };
this._id = insertedRow._id; this._id = insertedRow._id;
} else { } else {
@ -92,9 +121,9 @@ class Alias {
/** /**
* Set AliasData object fields to internal model fields * 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; const { id, type, hash, deprecated } = aliasData;
this.id = id || this.id; this.id = id || this.id;
@ -108,7 +137,7 @@ class Alias {
* *
* @returns {AliasData} * @returns {AliasData}
*/ */
get data() { public get data(): AliasData {
return { return {
_id: this._id, _id: this._id,
id: this.id, id: this.id,
@ -119,23 +148,9 @@ class Alias {
} }
/** /**
* Mark alias as deprecated
*
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>} * @returns {Promise<Alias>}
*/ */
static async markAsDeprecated(aliasName) { public async destroy(): Promise<Alias> {
const alias = await Alias.get(aliasName);
alias.deprecated = true;
return alias.save();
}
/**
* @returns {Promise<Alias>}
*/
async destroy() {
await aliasesDb.remove({ _id: this._id }); await aliasesDb.remove({ _id: this._id });
delete this._id; delete this._id;
@ -144,4 +159,4 @@ class Alias {
} }
} }
module.exports = Alias; export default Alias;

View file

@ -1,4 +1,6 @@
const { files: filesDb } = require('../utils/database/index'); import database from '../utils/database/index';
const filesDb = database['files'];
/** /**
* @typedef {object} FileData * @typedef {object} FileData
@ -10,6 +12,15 @@ const { files: filesDb } = require('../utils/database/index');
* @property {string} mimetype - file MIME type * @property {string} mimetype - file MIME type
* @property {number} size - size of the file in * @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 * @class File
@ -23,48 +34,19 @@ const { files: filesDb } = require('../utils/database/index');
* @property {number} size - size of the file in * @property {number} size - size of the file in
*/ */
class File { class File {
/** public _id?: string;
* Find and return model of file with given id public name?: string;
* public filename?: string;
* @param {string} _id - file id public path?: string;
* @returns {Promise<File>} public mimetype?: string;
*/ public size?: number;
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<File>}
*/
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<File[]>}
*/
static async getAll(query = {}) {
const docs = await filesDb.find(query);
return Promise.all(docs.map(doc => new File(doc)));
}
/** /**
* @class * @class
* *
* @param {FileData} data * @param {FileData} data - info about file
*/ */
constructor(data = {}) { constructor(data: FileData = {}) {
if (data === null) { if (data === null) {
data = {}; data = {};
} }
@ -75,13 +57,48 @@ class File {
this.data = data; this.data = data;
} }
/**
* Find and return model of file with given id
*
* @param {string} _id - file id
* @returns {Promise<File>}
*/
public static async get(_id: string): Promise<File> {
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<File>}
*/
public static async getByFilename(filename: string): Promise<File> {
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<File[]>}
*/
public static async getAll(query: Record<string, unknown> = {}): Promise<File[]> {
const docs = await filesDb.find(query);
return Promise.all(docs.map(doc => new File(doc)));
}
/** /**
* Set FileData object fields to internal model fields * 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; const { name, filename, path, mimetype, size } = fileData;
this.name = name || this.name; this.name = name || this.name;
@ -96,7 +113,7 @@ class File {
* *
* @returns {FileData} * @returns {FileData}
*/ */
get data() { public get data(): FileData {
return { return {
_id: this._id, _id: this._id,
name: this.name, name: this.name,
@ -112,9 +129,9 @@ class File {
* *
* @returns {Promise<File>} * @returns {Promise<File>}
*/ */
async save() { public async save(): Promise<File> {
if (!this._id) { if (!this._id) {
const insertedRow = await filesDb.insert(this.data); const insertedRow = await filesDb.insert(this.data) as { _id: string };
this._id = insertedRow._id; this._id = insertedRow._id;
} else { } else {
@ -129,7 +146,7 @@ class File {
* *
* @returns {Promise<File>} * @returns {Promise<File>}
*/ */
async destroy() { public async destroy(): Promise<File> {
await filesDb.remove({ _id: this._id }); await filesDb.remove({ _id: this._id });
delete this._id; delete this._id;
@ -137,24 +154,24 @@ class File {
return this; return this;
} }
/**
* Removes unnecessary public folder prefix
*
* @param {string} path
* @returns {string}
*/
processPath(path) {
return path.replace(/^public/, '');
}
/** /**
* Return readable file data * Return readable file data
* *
* @returns {FileData} * @returns {FileData}
*/ */
toJSON() { public toJSON(): FileData {
return this.data; 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;

View file

@ -1,5 +1,7 @@
const urlify = require('../utils/urlify'); import urlify from '../utils/urlify';
const { pages: pagesDb } = require('../utils/database/index'); import database from '../utils/database/index';
const pagesDb = database['pages'];
/** /**
* @typedef {object} PageData * @typedef {object} PageData
@ -9,6 +11,13 @@ const { pages: pagesDb } = require('../utils/database/index');
* @property {*} body - page body * @property {*} body - page body
* @property {string} parent - id of parent page * @property {string} parent - id of parent page
*/ */
export interface PageData {
_id?: string;
title?: string;
uri?: string;
body?: any;
parent?: string;
}
/** /**
* @class Page * @class Page
@ -21,48 +30,18 @@ const { pages: pagesDb } = require('../utils/database/index');
* @property {string} _parent - id of parent page * @property {string} _parent - id of parent page
*/ */
class Page { class Page {
/** public _id?: string;
* Find and return model of page with given id public body?: any;
* public title?: string;
* @param {string} _id - page id public uri?: string;
* @returns {Promise<Page>} public _parent?: string;
*/
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<Page>}
*/
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<Page[]>}
*/
static async getAll(query = {}) {
const docs = await pagesDb.find(query);
return Promise.all(docs.map(doc => new Page(doc)));
}
/** /**
* @class * @class
* *
* @param {PageData} data * @param {PageData} data - page's data
*/ */
constructor(data = {}) { constructor(data: PageData = {}) {
if (data === null) { if (data === null) {
data = {}; data = {};
} }
@ -74,12 +53,48 @@ class Page {
this.data = data; this.data = data;
} }
/**
* Find and return model of page with given id
*
* @param {string} _id - page id
* @returns {Promise<Page>}
*/
public static async get(_id: string): Promise<Page> {
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<Page>}
*/
public static async getByUri(uri: string): Promise<Page> {
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<Page[]>}
*/
public static async getAll(query: Record<string, unknown> = {}): Promise<Page[]> {
const docs = await pagesDb.find(query);
return Promise.all(docs.map(doc => new Page(doc)));
}
/** /**
* Set PageData object fields to internal model fields * 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; const { body, parent, uri } = pageData;
this.body = body || this.body; this.body = body || this.body;
@ -93,7 +108,7 @@ class Page {
* *
* @returns {PageData} * @returns {PageData}
*/ */
get data() { public get data(): PageData {
return { return {
_id: this._id, _id: this._id,
title: this.title, 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 * 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; this._parent = parentPage._id;
} }
@ -137,9 +132,10 @@ class Page {
* *
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
get parent() { public async getParent(): Promise<Page> {
return pagesDb.findOne({ _id: this._parent }) const data = await pagesDb.findOne({ _id: this._parent });
.then(data => new Page(data));
return new Page(data);
} }
/** /**
@ -147,9 +143,11 @@ class Page {
* *
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
get children() { public get children(): Promise<Page[]> {
return pagesDb.find({ parent: this._id }) 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<Page>} * @returns {Promise<Page>}
*/ */
async save() { public async save(): Promise<Page> {
this.uri = await this.composeUri(this.uri); if (this.uri !== undefined) {
this.uri = await this.composeUri(this.uri);
}
if (!this._id) { if (!this._id) {
const insertedRow = await pagesDb.insert(this.data); const insertedRow = await pagesDb.insert(this.data) as { _id: string };
this._id = insertedRow._id; this._id = insertedRow._id;
} else { } else {
@ -176,7 +176,7 @@ class Page {
* *
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
async destroy() { public async destroy(): Promise<Page> {
await pagesDb.remove({ _id: this._id }); await pagesDb.remove({ _id: this._id });
delete this._id; delete this._id;
@ -184,13 +184,22 @@ class Page {
return this; return this;
} }
/**
* Return readable page data
*
* @returns {PageData}
*/
public toJSON(): PageData {
return this.data;
}
/** /**
* Find and return available uri * Find and return available uri
* *
* @returns {Promise<string>} * @returns {Promise<string>}
* @param uri * @param uri - input uri to be composed
*/ */
async composeUri(uri) { private async composeUri(uri: string): Promise<string> {
let pageWithSameUriCount = 0; let pageWithSameUriCount = 0;
if (!this._id) { if (!this._id) {
@ -210,13 +219,28 @@ class Page {
} }
/** /**
* Return readable page data * Extract first header from editor data
* *
* @returns {PageData} * @returns {string}
*/ */
toJSON() { private extractTitleFromBody(): string {
return this.data; const headerBlock = this.body ? this.body.blocks.find((block: Record<string, unknown>) => 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;

View file

@ -1,4 +1,6 @@
const { pagesOrder: db } = require('../utils/database/index'); import database from '../utils/database/index';
const db = database['pagesOrder'];
/** /**
* @typedef {object} PageOrderData * @typedef {object} PageOrderData
@ -6,6 +8,11 @@ const { pagesOrder: db } = require('../utils/database/index');
* @property {string} page - page id * @property {string} page - page id
* @property {Array<string>} order - list of ordered pages * @property {Array<string>} order - list of ordered pages
*/ */
export interface PageOrderData {
_id?: string;
page?: string;
order?: string[];
}
/** /**
* @class PageOrder * @class PageOrder
@ -14,44 +21,17 @@ const { pagesOrder: db } = require('../utils/database/index');
* Creates order for Pages with children * Creates order for Pages with children
*/ */
class PageOrder { class PageOrder {
/** public _id?: string;
* Returns current Page's children order public page?: string;
* private _order?: string[];
* @param {string} pageId - page's id
* @returns {PageOrder}
*/
static async get(pageId) {
const order = await db.findOne({ page: pageId });
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<Page[]>}
*/
static async getAll(query = {}) {
const docs = await db.find(query);
return Promise.all(docs.map(doc => new PageOrder(doc)));
}
/** /**
* @class * @class
* *
* @param {PageOrderData} data * @param {PageOrderData} data - info about pageOrder
*/ */
constructor(data = {}) { constructor(data: PageOrderData = {}) {
if (data === null) { if (data === null) {
data = {}; data = {};
} }
@ -63,14 +43,46 @@ class PageOrder {
this.data = data; this.data = data;
} }
/**
* Returns current Page's children order
*
* @param {string} pageId - page's id
* @returns {Promise<PageOrder>}
*/
public static async get(pageId: string): Promise<PageOrder> {
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<PageOrder[]>}
*/
public static async getAll(query: Record<string, unknown> = {}): Promise<PageOrder[]> {
const docs = await db.find(query);
return Promise.all(docs.map(doc => new PageOrder(doc)));
}
/** /**
* constructor data setter * constructor data setter
* *
* @param {PageOrderData} pageOrderData * @param {PageOrderData} pageOrderData - info about pageOrder
*/ */
set data(pageOrderData) { public set data(pageOrderData: PageOrderData) {
this._page = pageOrderData.page || 0; this.page = pageOrderData.page || '0';
this._order = pageOrderData.order || []; this.order = pageOrderData.order || [];
} }
/** /**
@ -78,11 +90,11 @@ class PageOrder {
* *
* @returns {PageOrderData} * @returns {PageOrderData}
*/ */
get data() { public get data(): PageOrderData {
return { return {
_id: this._id, _id: this._id,
page: '' + this._page, page: '' + this.page,
order: this._order, order: this.order,
}; };
} }
@ -91,9 +103,12 @@ class PageOrder {
* *
* @param {string} pageId - page's id * @param {string} pageId - page's id
*/ */
push(pageId) { public push(pageId: string | number): void {
if (typeof pageId === 'string') { if (typeof pageId === 'string') {
this._order.push(pageId); if (this.order === undefined) {
this.order = [];
}
this.order.push(pageId);
} else { } else {
throw new Error('given id is not string'); throw new Error('given id is not string');
} }
@ -104,11 +119,15 @@ class PageOrder {
* *
* @param {string} pageId - page's id * @param {string} pageId - page's id
*/ */
remove(pageId) { public remove(pageId: string): void {
const found = this._order.indexOf(pageId); if (this.order === undefined) {
return;
}
const found = this.order.indexOf(pageId);
if (found >= 0) { 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} currentPageId - page's id that changes the order
* @param {string} putAbovePageId - page's id above which we put the target page * @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 found1 = this.order.indexOf(putAbovePageId);
const found2 = this.order.indexOf(currentPageId); const found2 = this.order.indexOf(currentPageId);
@ -135,16 +158,20 @@ class PageOrder {
/** /**
* Returns page before passed page with id * 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); const currentPageInOrder = this.order.indexOf(pageId);
/** /**
* If page not found or first return nothing * If page not found or first return nothing
*/ */
if (currentPageInOrder <= 0) { if (currentPageInOrder <= 0) {
return; return null;
} }
return this.order[currentPageInOrder - 1]; return this.order[currentPageInOrder - 1];
@ -153,16 +180,20 @@ class PageOrder {
/** /**
* Returns page before passed page with id * 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); const currentPageInOrder = this.order.indexOf(pageId);
/** /**
* If page not found or is last * If page not found or is last
*/ */
if (currentPageInOrder === -1 || currentPageInOrder === this.order.length - 1) { if (currentPageInOrder === -1 || currentPageInOrder === this.order.length - 1) {
return; return null;
} }
return this.order[currentPageInOrder + 1]; return this.order[currentPageInOrder + 1];
@ -171,7 +202,7 @@ class PageOrder {
/** /**
* @param {string[]} order - define new order * @param {string[]} order - define new order
*/ */
set order(order) { public set order(order: string[]) {
this._order = order; this._order = order;
} }
@ -180,16 +211,18 @@ class PageOrder {
* *
* @returns {string[]} * @returns {string[]}
*/ */
get order() { public get order(): string[] {
return this._order; return this._order || [];
} }
/** /**
* Save or update page data in the database * Save or update page data in the database
*
* @returns {Promise<PageOrder>}
*/ */
async save() { public async save(): Promise<PageOrder> {
if (!this._id) { if (!this._id) {
const insertedRow = await db.insert(this.data); const insertedRow = await db.insert(this.data) as { _id: string};
this._id = insertedRow._id; this._id = insertedRow._id;
} else { } else {
@ -201,14 +234,14 @@ class PageOrder {
/** /**
* Remove page data from the database * Remove page data from the database
*
* @returns {Promise<void>}
*/ */
async destroy() { public async destroy(): Promise<void> {
await db.remove({ _id: this._id }); await db.remove({ _id: this._id });
delete this._id; delete this._id;
return this;
} }
} }
module.exports = PageOrder; export default PageOrder;

View file

@ -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<User>}
*/
public static async get(): Promise<User> {
const userData: UserData = await db.findOne({});
return new User(userData);
}
}
export default User;

View file

@ -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 router = express.Router();
const Aliases = require('../controllers/aliases');
const Pages = require('../controllers/pages');
const Alias = require('../models/alias');
const verifyToken = require('./middlewares/token');
/** /**
* GET /* * GET /*
* *
* Return document with given alias * Return document with given alias
*/ */
router.get('*', verifyToken, async (req, res) => { router.get('*', verifyToken, async (req: Request, res: Response) => {
try { try {
let url = req.originalUrl.slice(1); // Cuts first '/' character let url = req.originalUrl.slice(1); // Cuts first '/' character
const queryParamsIndex = url.indexOf('?'); const queryParamsIndex = url.indexOf('?');
@ -21,11 +22,15 @@ router.get('*', verifyToken, async (req, res) => {
const alias = await Aliases.get(url); const alias = await Aliases.get(url);
if (alias.id === undefined) {
throw new Error('Alias not found');
}
switch (alias.type) { switch (alias.type) {
case Alias.types.PAGE: { case Alias.types.PAGE: {
const page = await Pages.get(alias.id); const page = await Pages.get(alias.id);
const pageParent = await page.parent; const pageParent = await page.getParent();
res.render('pages/page', { res.render('pages/page', {
page, page,
@ -37,9 +42,9 @@ router.get('*', verifyToken, async (req, res) => {
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: err.message, error: err,
}); });
} }
}); });
module.exports = router; export default router;

View file

@ -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;

View file

@ -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;

View file

@ -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 router = express.Router();
const multer = require('multer')(); const multer = multerFunc();
const Pages = require('../../controllers/pages');
const PagesOrder = require('../../controllers/pagesOrder');
/** /**
* GET /page/:id * GET /page/:id
@ -10,18 +12,18 @@ const PagesOrder = require('../../controllers/pagesOrder');
* Return PageData of page with given id * Return PageData of page with given id
*/ */
router.get('/page/:id', async (req, res) => { router.get('/page/:id', async (req: Request, res: Response) => {
try { try {
const page = await Pages.get(req.params.id); const page = await Pages.get(req.params.id);
res.json({ res.json({
success: true, success: true,
result: page.data result: page.data,
}); });
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({
success: false, 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 * Return PageData for all pages
*/ */
router.get('/pages', async (req, res) => { router.get('/pages', async (req: Request, res: Response) => {
try { try {
const pages = await Pages.getAll(); const pages = await Pages.getAll();
res.json({ res.json({
success: true, success: true,
result: pages result: pages,
}); });
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({
success: false, 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 * 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 { try {
const { title, body, parent } = req.body; 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 */ /** push to the orders array */
await PagesOrder.push(parent, page._id); await PagesOrder.push(parent, page._id);
res.json({ res.json({
success: true, success: true,
result: page result: page,
}); });
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({
success: false, 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 * 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; const { id } = req.params;
try { try {
@ -85,25 +95,46 @@ router.post('/page/:id', multer.none(), async (req, res) => {
const pages = await Pages.getAll(); const pages = await Pages.getAll();
let page = await Pages.get(id); 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) { if (page._parent !== parent) {
await PagesOrder.move(page._parent, parent, id); await PagesOrder.move(page._parent, parent, id);
} else { } else {
if (putAbovePageId && putAbovePageId !== '0') { if (putAbovePageId && putAbovePageId !== '0') {
const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id); 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({ res.json({
success: true, success: true,
result: page result: page,
}); });
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({
success: false, 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 * Remove page from the database
*/ */
router.delete('/page/:id', async (req, res) => { router.delete('/page/:id', async (req: Request, res: Response) => {
try { try {
const pageId = req.params.id; const pageId = req.params.id;
const page = await Pages.get(pageId); 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 parentPageOrder = await PagesOrder.get(page._parent);
const pageBeforeId = parentPageOrder.getPageBefore(page._id); const pageBeforeId = parentPageOrder.getPageBefore(page._id);
const pageAfterId = parentPageOrder.getPageAfter(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 * remove current page and go deeper to remove children with orders
* *
* @param startFrom * @param {string} startFrom - start point to delete
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const deleteRecursively = async (startFrom) => { const deleteRecursively = async (startFrom: string): Promise<void> => {
let order = []; let order: string[] = [];
try { try {
const children = await PagesOrder.get(startFrom); const children = await PagesOrder.get(startFrom);
order = children.order; order = children.order;
} catch (e) {} } catch (e) {
order = [];
}
order.forEach(async id => { order.forEach(async id => {
await deleteRecursively(id); await deleteRecursively(id);
@ -153,7 +195,9 @@ router.delete('/page/:id', async (req, res) => {
await Pages.remove(startFrom); await Pages.remove(startFrom);
try { try {
await PagesOrder.remove(startFrom); await PagesOrder.remove(startFrom);
} catch (e) {} } catch (e) {
order = [];
}
}; };
await deleteRecursively(req.params.id); await deleteRecursively(req.params.id);
@ -164,14 +208,14 @@ router.delete('/page/:id', async (req, res) => {
res.json({ res.json({
success: true, success: true,
result: pageToRedirect result: pageToRedirect,
}); });
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: err.message error: (err as Error).message,
}); });
} }
}); });
module.exports = router; export default router;

View file

@ -1,59 +1,79 @@
const express = require('express'); import { Request, Response, Router } from 'express';
const router = express.Router(); import multer, { StorageEngine } from 'multer';
const multer = require('multer'); import mime from 'mime';
const mime = require('mime'); import mkdirp from 'mkdirp';
const mkdirp = require('mkdirp'); import config from 'config';
const Transport = require('../../controllers/transport'); import Transport from '../../controllers/transport';
const { random16 } = require('../../utils/crypto'); import { random16 } from '../../utils/crypto';
const config = require('../../../config');
const router = Router();
/** /**
* Multer storage for uploaded files and images * 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) => { 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) => { filename: async (req, file, cb) => {
const filename = await random16(); const filename = await random16();
cb(null, `${filename}.${mime.getExtension(file.mimetype)}`); cb(null, `${filename}.${mime.getExtension(file.mimetype)}`);
} },
}); });
/** /**
* Multer middleware for image uploading * Multer middleware for image uploading
*/ */
const imageUploader = multer({ const imageUploader = multer({
storage, storage: storage,
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
if (!/image/.test(file.mimetype) && !/video\/mp4/.test(file.mimetype)) { if (!/image/.test(file.mimetype) && !/video\/mp4/.test(file.mimetype)) {
cb(null, false); cb(null, false);
return; return;
} }
cb(null, true); cb(null, true);
} },
}).fields([ { name: 'image', maxCount: 1 } ]); }).fields([ {
name: 'image',
maxCount: 1,
} ]);
/** /**
* Multer middleware for file uploading * Multer middleware for file uploading
*/ */
const fileUploader = multer({ const fileUploader = multer({
storage storage: storage,
}).fields([ { name: 'file', maxCount: 1 } ]); }).fields([ {
name: 'file',
maxCount: 1,
} ]);
/** /**
* Accepts images to upload * Accepts images to upload
*/ */
router.post('/transport/image', imageUploader, async (req, res) => { router.post('/transport/image', imageUploader, async (req: Request, res: Response) => {
let response = { success: 0 }; 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); res.status(400).json(response);
return;
}
if (!('image' in req.files)) {
res.status(400).json(response);
return; return;
} }
@ -73,11 +93,17 @@ router.post('/transport/image', imageUploader, async (req, res) => {
/** /**
* Accepts files to upload * Accepts files to upload
*/ */
router.post('/transport/file', fileUploader, async (req, res) => { router.post('/transport/file', fileUploader, async (req: Request, res: Response) => {
let response = { success: 0 }; const response = { success: 0 };
if (!req.files || !req.files.file) { if (req.files === undefined) {
res.status(400).json(response); res.status(400).json(response);
return;
}
if (!('file' in req.files)) {
res.status(400).json(response);
return; return;
} }
@ -97,11 +123,12 @@ router.post('/transport/file', fileUploader, async (req, res) => {
/** /**
* Accept file url to fetch * Accept file url to fetch
*/ */
router.post('/transport/fetch', multer().none(), async (req, res) => { router.post('/transport/fetch', multer().none(), async (req: Request, res: Response) => {
let response = { success: 0 }; const response = { success: 0 };
if (!req.body.url) { if (!req.body.url) {
res.status(400).json(response); res.status(400).json(response);
return; return;
} }
@ -116,4 +143,4 @@ router.post('/transport/fetch', multer().none(), async (req, res) => {
} }
}); });
module.exports = router; export default router;

View file

@ -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;

View file

@ -1,9 +1,10 @@
const express = require('express'); import express, { Request, Response } from 'express';
const verifyToken = require('./middlewares/token'); import verifyToken from './middlewares/token';
const router = express.Router(); const router = express.Router();
/* GET home page. */ /* GET home page. */
router.get('/', verifyToken, async (req, res) => { router.get('/', verifyToken, async (req: Request, res: Response) => {
const config = req.app.locals.config; const config = req.app.locals.config;
if (config.startPage) { if (config.startPage) {
@ -12,4 +13,4 @@ router.get('/', verifyToken, async (req, res) => {
res.render('pages/index', { isAuthorized: res.locals.isAuthorized }); res.render('pages/index', { isAuthorized: res.locals.isAuthorized });
}); });
module.exports = router; export default router;

View file

@ -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;

View file

@ -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');
}
}

View file

@ -1,6 +1,9 @@
const Pages = require('../../controllers/pages'); import { NextFunction, Request, Response } from 'express';
const PagesOrder = require('../../controllers/pagesOrder'); import Pages from '../../controllers/pages';
const asyncMiddleware = require('../../utils/asyncMiddleware'); 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 * 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 {string} parentPageId - parent page id
* @param {Page[]} pages - list of all available pages * @param {Page[]} pages - list of all available pages
* @param {PagesOrder[]} pagesOrder - list of pages order * @param {PagesOrder[]} pagesOrder - list of pages order
* @param {number} level * @param {number} level - max level recursion
* @param {number} currentLevel * @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); 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 * 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 * otherwise just find all pages includes parent tree
*/ */
let ordered = []; let ordered: any[] = [];
if (childrenOrder) { if (childrenOrder) {
ordered = childrenOrder.order.map(pageId => { ordered = childrenOrder.order.map((pageId: string) => {
return pages.find(page => page._id === pageId); return pages.find(page => page._id === pageId);
}); });
} }
const unordered = pages.filter(page => page._parent === parentPageId); 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 * 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 branch.filter(page => page && page._id).map(page => {
return Object.assign({ return Object.assign({
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1) children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1),
}, page.data); }, page.data);
}); });
} }
/** /**
* Middleware for all /page/... routes * Middleware for all /page/... routes
* @param req *
* @param res * @param {Request} req
* @param next * @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 * Pages without parent
*
* @type {string} * @type {string}
*/ */
const parentIdOfRootPages = '0'; const parentIdOfRootPages = '0';

View file

@ -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<void> {
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();
}
}

View file

@ -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;

View file

@ -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);
};
}

View file

@ -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 * Create binary md5
@ -6,10 +16,10 @@ const crypto = require('crypto');
* @param stringToHash - string to hash * @param stringToHash - string to hash
* @returns {string} - binary hash of argument * @returns {string} - binary hash of argument
*/ */
function binaryMD5(stringToHash) { export function binaryMD5(stringToHash: string): string {
return crypto.createHash('md5') return hexToBinary(crypto.createHash('md5')
.update(stringToHash) .update(stringToHash)
.digest('binary'); .digest('hex'));
} }
/** /**
@ -17,7 +27,7 @@ function binaryMD5(stringToHash) {
* *
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
function random16() { export function random16(): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
crypto.randomBytes(16, (err, raw) => { crypto.randomBytes(16, (err, raw) => {
if (err) { if (err) {
@ -29,7 +39,7 @@ function random16() {
}); });
} }
module.exports = { export default {
binaryMD5, binaryMD5,
random16, random16,
}; };

View file

@ -1,33 +1,58 @@
const pages = require('./pages'); import Datastore from 'nedb';
const files = require('./files'); import { AliasData } from '../../models/alias';
const password = require('./password'); import { FileData } from '../../models/file';
const aliases = require('./aliases'); import { PageData } from '../../models/page';
const pagesOrder = require('./pagesOrder'); 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 * @class Database
* @classdesc Simple decorator class to work with nedb datastore * @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<DocType> {
private db: Datastore;
/** /**
* @constructor * @class
* *
* @param {Object} nedbInstance - nedb Datastore object * @param {Object} nedbInstance - nedb Datastore object
*/ */
constructor(nedbInstance) { constructor(nedbInstance: Datastore) {
this.db = nedbInstance; this.db = nedbInstance;
} }
/** /**
* Insert new document into the database * Insert new document into the database
*
* @see https://github.com/louischatriot/nedb#inserting-documents * @see https://github.com/louischatriot/nedb#inserting-documents
* *
* @param {Object} doc - object to insert * @param {Object} doc - object to insert
* @returns {Promise<Object|Error>} - inserted doc or Error object * @returns {Promise<Object|Error>} - inserted doc or Error object
*/ */
async insert(doc) { public async insert(doc: DocType): Promise<DocType> {
return new Promise((resolve, reject) => this.db.insert(doc, (err, newDoc) => { return new Promise((resolve, reject) => this.db.insert(doc, (err, newDoc) => {
if (err) { if (err) {
reject(err); reject(err);
@ -39,14 +64,15 @@ class Database {
/** /**
* Find documents that match passed query * Find documents that match passed query
*
* @see https://github.com/louischatriot/nedb#finding-documents * @see https://github.com/louischatriot/nedb#finding-documents
* *
* @param {Object} query - query object * @param {Object} query - query object
* @param {Object} projection - projection object * @param {Object} projection - projection object
* @returns {Promise<Array<Object>|Error>} - found docs or Error object * @returns {Promise<Array<Object>|Error>} - found docs or Error object
*/ */
async find(query, projection) { public async find(query: Record<string, unknown>, projection?: DocType): Promise<Array<DocType>> {
const cbk = (resolve, reject) => (err, docs) => { const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, docs: DocType[]) => {
if (err) { if (err) {
reject(err); reject(err);
} }
@ -65,14 +91,15 @@ class Database {
/** /**
* Find one document matches passed query * Find one document matches passed query
*
* @see https://github.com/louischatriot/nedb#finding-documents * @see https://github.com/louischatriot/nedb#finding-documents
* *
* @param {Object} query - query object * @param {Object} query - query object
* @param {Object} projection - projection object * @param {Object} projection - projection object
* @returns {Promise<Object|Error>} - found doc or Error object * @returns {Promise<Object|Error>} - found doc or Error object
*/ */
async findOne(query, projection) { public async findOne(query: Record<string, unknown>, projection?: DocType): Promise<DocType> {
const cbk = (resolve, reject) => (err, doc) => { const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, doc: DocType) => {
if (err) { if (err) {
reject(err); reject(err);
} }
@ -91,18 +118,15 @@ class Database {
/** /**
* Update document matches query * Update document matches query
*
* @see https://github.com/louischatriot/nedb#updating-documents * @see https://github.com/louischatriot/nedb#updating-documents
* *
* @param {Object} query - query object * @param {Object} query - query object
* @param {Object} update - fields to update * @param {Object} update - fields to update
* @param {Object} options * @param {Options} options - optional params
* @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
* @returns {Promise<number|Object|Object[]|Error>} - number of updated rows or affected docs or Error object * @returns {Promise<number|Object|Object[]|Error>} - number of updated rows or affected docs or Error object
*/ */
async update(query, update, options = {}) { public async update(query: Record<string, unknown>, update: DocType, options: Options = {}): Promise<number|boolean|Array<DocType>> {
return new Promise((resolve, reject) => this.db.update(query, update, options, (err, result, affectedDocs) => { return new Promise((resolve, reject) => this.db.update(query, update, options, (err, result, affectedDocs) => {
if (err) { if (err) {
reject(err); reject(err);
@ -126,14 +150,14 @@ class Database {
/** /**
* Remove document matches passed query * Remove document matches passed query
*
* @see https://github.com/louischatriot/nedb#removing-documents * @see https://github.com/louischatriot/nedb#removing-documents
* *
* @param {Object} query - query object * @param {Object} query - query object
* @param {Object} options * @param {Options} options - optional params
* @param {Boolean} options.multi - (false) if true, remove several docs
* @returns {Promise<number|Error>} - number of removed rows or Error object * @returns {Promise<number|Error>} - number of removed rows or Error object
*/ */
async remove(query, options = {}) { public async remove(query: Record<string, unknown>, options: Options = {}): Promise<number> {
return new Promise((resolve, reject) => this.db.remove(query, options, (err, result) => { return new Promise((resolve, reject) => this.db.remove(query, options, (err, result) => {
if (err) { if (err) {
reject(err); reject(err);
@ -144,11 +168,10 @@ class Database {
} }
} }
module.exports = { export default {
class: Database, pages: new Database<PageData>(initDb('pages')),
pages: new Database(pages), password: new Database<UserData>(initDb('password')),
password: new Database(password), aliases: new Database<AliasData>(initDb('aliases')),
aliases: new Database(aliases), pagesOrder: new Database<PageOrderData>(initDb('pagesOrder')),
pagesOrder: new Database(pagesOrder), files: new Database<FileData>(initDb('files')),
files: new Database(files)
}; };

View file

@ -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,
});
}

View file

@ -5,8 +5,13 @@
* @param {object[]} sources * @param {object[]} sources
* @returns {object} * @returns {object}
*/ */
function deepMerge(target, ...sources) {
const isObject = item => item && typeof item === 'object' && !Array.isArray(item); /**
* @param {Record<string, any>} target - target to merge into
* @param {...any[]} sources - sources to merge from
*/
function deepMerge(target: Record<string, any>, ...sources: any[]): Record<string, unknown> {
const isObject = (item: unknown): boolean => !!item && typeof item === 'object' && !Array.isArray(item);
if (!sources.length) { if (!sources.length) {
return target; return target;
@ -30,6 +35,4 @@ function deepMerge(target, ...sources) {
return deepMerge(target, ...sources); return deepMerge(target, ...sources);
} }
module.exports = { export default deepMerge;
deepMerge,
};

View file

@ -1,28 +1,43 @@
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const config = require('../../config'); import config from 'config';
const rcPath = path.resolve(__dirname, '../../', config.rcFile || './.codexdocsrc');
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 * @typedef {object} RCData
* @property {string} title - website title * @property {string} title - website title
* @property {object[]} menu - options for website menu * @property {Menu[]} menu - options for website menu
* @property {string} menu[].title - menu option title
* @property {string} menu[].uri - menu option href
*/ */
interface RCData {
title: string;
menu: Menu[];
[key: string]: string | Menu[];
}
/** /**
* @class RCParser * @class RCParser
* @classdesc Class to parse runtime configuration file for CodeX Docs engine * @classdesc Class to parse runtime configuration file for CodeX Docs engine
*/ */
module.exports = class RCParser { export default class RCParser {
/** /**
* Default CodeX Docs configuration * Default CodeX Docs configuration
* *
* @static * @static
* @returns {{title: string, menu: Array}} * @returns {{title: string, menu: Array}}
*/ */
static get DEFAULTS() { public static get DEFAULTS():RCData {
return { return {
title: 'CodeX Docs', title: 'CodeX Docs',
menu: [], menu: [],
@ -35,12 +50,12 @@ module.exports = class RCParser {
* @static * @static
* @returns {{title: string, menu: []}} * @returns {{title: string, menu: []}}
*/ */
static getConfiguration() { public static getConfiguration(): RCData {
if (!fs.existsSync(rcPath)) { if (!fs.existsSync(rcPath)) {
return RCParser.DEFAULTS; return RCParser.DEFAULTS;
} }
const file = fs.readFileSync(rcPath, { encoding: 'UTF-8' }); const file = fs.readFileSync(rcPath, 'utf-8');
const rConfig = RCParser.DEFAULTS; const rConfig = RCParser.DEFAULTS;
let userConfig; let userConfig;
@ -63,7 +78,7 @@ module.exports = class RCParser {
rConfig.menu = RCParser.DEFAULTS.menu; 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; i = i + 1;
if (typeof option === 'string') { if (typeof option === 'string') {
return true; return true;
@ -92,7 +107,7 @@ module.exports = class RCParser {
return true; return true;
}); });
rConfig.menu = rConfig.menu.map(option => { rConfig.menu = rConfig.menu.map((option: string | Menu) => {
if (typeof option === 'string') { if (typeof option === 'string') {
return { return {
title: option, title: option,
@ -106,4 +121,4 @@ module.exports = class RCParser {
return rConfig; return rConfig;
} }
}; }

View file

@ -1,4 +1,7 @@
const translationTable = { interface TransTable {
[key: string]: string;
}
const translationTable: TransTable = {
а: 'a', а: 'a',
б: 'b', б: 'b',
в: 'v', в: 'v',
@ -73,6 +76,10 @@ const translationTable = {
* @returns {string} - translated string * @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); return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char);
}; }

View file

@ -1,22 +1,22 @@
/** /**
* Twig extensions * Twig extensions
*/ */
const twig = require('twig'); import twig from 'twig';
const fs = require('fs'); import fs from 'fs';
const urlify = require('./urlify'); import urlify from './urlify';
module.exports = (function () { export default (function () {
'use strict'; 'use strict';
/** /**
* Function for include svg on page * Function for include svg on page
* *
* @example svg('path/from/root/dir') * @example svg('path/from/root/dir')
* @param filename - name of icon * @param {string} filename - name of icon
* @returns {string} - svg code * @returns {string} - svg code
*/ */
twig.extendFunction('svg', function (filename) { twig.extendFunction('svg', function (filename: string) {
return fs.readFileSync(`${__dirname}/../frontend/svg/${filename}.svg`, 'utf-8'); return fs.readFileSync(`./src/frontend/svg/${filename}.svg`, 'utf-8');
}); });
/** /**
@ -26,7 +26,7 @@ module.exports = (function () {
* @param {string} string - source string with HTML * @param {string} string - source string with HTML
* @returns {string} alias-like string * @returns {string} alias-like string
*/ */
twig.extendFilter('urlify', function (string) { twig.extendFilter('urlify', function (string: string) {
return urlify(string); return urlify(string);
}); });
@ -34,13 +34,15 @@ module.exports = (function () {
* Parse link as URL object * Parse link as URL object
* *
* @param {string} linkUrl - link to be processed * @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 { try {
return new URL(linkUrl); return new URL(linkUrl).toString();
} catch (e) { } catch (e) {
console.log(e); console.log(e);
return '';
} }
}); });
}()); }());

View file

@ -1,4 +1,4 @@
const translateString = require('./translation'); import translateString from './translation';
/** /**
* Convert text to URL-like string * Convert text to URL-like string
@ -7,7 +7,7 @@ const translateString = require('./translation');
* @param {string} string - source string with HTML * @param {string} string - source string with HTML
* @returns {string} alias-like string * @returns {string} alias-like string
*/ */
module.exports = function urlify(string) { export default function urlify(string: string): string {
// strip tags // strip tags
string = string.replace(/(<([^>]+)>)/ig, ''); string = string.replace(/(<([^>]+)>)/ig, '');
@ -30,4 +30,4 @@ module.exports = function urlify(string) {
string = translateString(string); string = translateString(string);
return string; return string;
}; }

39
bin/www → src/bin/server.ts Executable file → Normal file
View file

@ -1,17 +1,17 @@
#!/usr/bin/env node
/** /**
* Module dependencies. * Module dependencies.
*/ */
const app = require('../src/app'); import app from '../backend/app';
const debug = require('debug')('codex.editor.docs:server'); import http from 'http';
const http = require('http'); import config from 'config';
const config = require('../config'); import Debug from 'debug';
const debug = Debug.debug('codex.editor.docs:server');
/** /**
* Get port from environment and store in Express. * 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); app.set('port', port);
@ -29,8 +29,9 @@ server.on('listening', onListening);
/** /**
* Normalize a port into a number, string, or false. * 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); const value = parseInt(val, 10);
if (isNaN(value)) { 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') { if (error.syscall !== 'listen') {
throw error; throw error;
} }
@ -63,19 +65,27 @@ function onError(error) {
case 'EACCES': case 'EACCES':
console.error(bind + ' requires elevated privileges'); console.error(bind + ' requires elevated privileges');
process.exit(1); process.exit(1);
break;
case 'EADDRINUSE': case 'EADDRINUSE':
console.error(bind + ' is already in use'); console.error(bind + ' is already in use');
process.exit(1); process.exit(1);
break;
default: default:
throw error; 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(); const addr = server.address();
if (addr === null) {
debug('Address not found');
process.exit(1);
}
const bind = typeof addr === 'string' const bind = typeof addr === 'string'
? 'pipe ' + addr ? 'pipe ' + addr
: 'port ' + addr.port; : 'port ' + addr.port;
@ -83,4 +93,7 @@ function onListening() {
debug('Listening on ' + bind); debug('Listening on ' + bind);
} }
module.exports = {server, app}; export default {
server,
app,
};

View file

@ -1,20 +0,0 @@
const Model = require('../models/user');
/**
* @class Users
* @classdesc Users controller
*/
class Users {
/**
* Find and return user model.
*
* @returns {Promise<User>}
*/
static async get() {
const userDoc = await Model.get();
return userDoc;
}
}
module.exports = Users;

View file

@ -52,11 +52,16 @@ export default class Writing {
this.editor = editor; this.editor = editor;
}); });
window.onbeforeunload = (e) => {
return '';
}
/** /**
* Activate form elements * Activate form elements
*/ */
this.nodes.saveButton = moduleEl.querySelector('[name="js-submit-save"]'); this.nodes.saveButton = moduleEl.querySelector('[name="js-submit-save"]');
this.nodes.saveButton.addEventListener('click', () => { this.nodes.saveButton.addEventListener('click', () => {
window.onbeforeunload = null;
this.saveButtonClicked(); this.saveButtonClicked();
}); });
@ -69,7 +74,7 @@ export default class Writing {
if (!isUserAgree) { if (!isUserAgree) {
return; return;
} }
window.onbeforeunload = null;
this.removeButtonClicked(); this.removeButtonClicked();
}); });
} }

View file

@ -1,14 +1,17 @@
#!/usr/bin/env node #!/usr/bin/env node
let { password: db } = require('./src/utils/database'); import database from './backend/utils/database';
const program = require('commander'); import commander from 'commander';
import bcrypt from 'bcrypt';
const bcrypt = require('bcrypt'); const db = database['password'];
const program = commander.program;
const saltRounds = 12; const saltRounds = 12;
/** /**
* Script for generating password, that will be used to create and edit pages in CodeX.Docs. * 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. * Hashes password with bcrypt and inserts it to the database.
*
* @see {https://github.com/tj/commander.js | CommanderJS} * @see {https://github.com/tj/commander.js | CommanderJS}
*/ */
program program
@ -23,7 +26,7 @@ program
const userDoc = { passHash: hash }; const userDoc = { passHash: hash };
await db.remove({}, {multi: true}); await db.remove({}, { multi: true });
await db.insert(userDoc); await db.insert(userDoc);
console.log('Password was successfully generated'); console.log('Password was successfully generated');

View file

@ -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<User>}
*/
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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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');
}
};

View file

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

View file

@ -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;

196
src/test/database.ts Normal file
View file

@ -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<any>;
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<Document>;
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<Document>;
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<Document>;
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);
}
});
});

View file

@ -1,13 +1,15 @@
const { app } = require('../bin/www'); import chaiHTTP from 'chai-http';
const chai = require('chai'); import chai, { expect } from 'chai';
const chaiHTTP = require('chai-http');
const { expect } = chai; import server from '../bin/server';
const app = server.app;
chai.use(chaiHTTP); chai.use(chaiHTTP);
describe('Express app', () => { describe('Express app', () => {
it('App is available', async () => { it('App is available', async () => {
let agent = chai.request.agent(app); const agent = chai.request.agent(app);
const result = await agent const result = await agent
.get('/'); .get('/');

View file

@ -1,14 +1,16 @@
const {expect} = require('chai'); import { expect } from 'chai';
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const config = require('../../config'); import config from 'config';
const Alias = require('../../src/models/alias'); import Alias from '../../backend/models/alias';
const {binaryMD5} = require('../../src/utils/crypto'); import { binaryMD5 } from '../../backend/utils/crypto';
const {aliases} = require('../../src/utils/database'); import database from '../../backend/utils/database';
const aliases = database['aliases'];
describe('Alias model', () => { describe('Alias model', () => {
after(() => { after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './aliases.db'); const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './aliases.db');
if (fs.existsSync(pathToDB)) { if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB); fs.unlinkSync(pathToDB);
@ -108,7 +110,7 @@ describe('Alias model', () => {
expect(savedAlias.id).to.equal(initialData.id); expect(savedAlias.id).to.equal(initialData.id);
expect(savedAlias.deprecated).to.equal(false); 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._id).to.equal(savedAlias._id);
expect(insertedAlias.hash).to.equal(savedAlias.hash); expect(insertedAlias.hash).to.equal(savedAlias.hash);
@ -128,7 +130,7 @@ describe('Alias model', () => {
expect(alias._id).to.equal(insertedAlias._id); 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._id).to.equal(savedAlias._id);
expect(updatedAlias.hash).to.equal(updateData.hash); expect(updatedAlias.hash).to.equal(updateData.hash);

View file

@ -1,14 +1,16 @@
const {expect} = require('chai'); import { expect } from 'chai';
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const config = require('../../config'); import config from 'config';
const File = require('../../src/models/file'); import File from '../../backend/models/file';
const {files} = require('../../src/utils/database'); import database from '../../backend/utils/database';
const files = database['files'];
describe('File model', () => { describe('File model', () => {
after(() => { after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './files.db'); const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './files.db');
if (fs.existsSync(pathToDB)) { if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB); fs.unlinkSync(pathToDB);
@ -20,7 +22,7 @@ describe('File model', () => {
expect(file.data).to.be.a('object'); expect(file.data).to.be.a('object');
let {data} = file; let { data } = file;
expect(data._id).to.be.undefined; expect(data._id).to.be.undefined;
expect(data.name).to.be.undefined; expect(data.name).to.be.undefined;
@ -29,7 +31,7 @@ describe('File model', () => {
expect(data.size).to.be.undefined; expect(data.size).to.be.undefined;
expect(data.mimetype).to.be.undefined; expect(data.mimetype).to.be.undefined;
file = new File(null); file = new File();
data = file.data; data = file.data;
@ -51,8 +53,6 @@ describe('File model', () => {
file = new File(initialData); file = new File(initialData);
const json = file.toJSON();
data = file.data; data = file.data;
expect(data._id).to.equal(initialData._id); expect(data._id).to.equal(initialData._id);
@ -63,7 +63,7 @@ describe('File model', () => {
expect(data.mimetype).to.equal(initialData.mimetype); expect(data.mimetype).to.equal(initialData.mimetype);
const update = { const update = {
_id: 12345, _id: '12345',
name: 'updated filename', name: 'updated filename',
filename: 'updated randomname', filename: 'updated randomname',
path: '/uploads/updated randomname', path: '/uploads/updated randomname',
@ -94,7 +94,7 @@ describe('File model', () => {
const file = new File(initialData); const file = new File(initialData);
let savedFile = await file.save(); const savedFile = await file.save();
expect(savedFile._id).not.be.undefined; expect(savedFile._id).not.be.undefined;
expect(savedFile.name).to.equal(initialData.name); expect(savedFile.name).to.equal(initialData.name);
@ -103,7 +103,7 @@ describe('File model', () => {
expect(savedFile.size).to.equal(initialData.size); expect(savedFile.size).to.equal(initialData.size);
expect(savedFile.mimetype).to.equal(initialData.mimetype); 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._id).to.equal(file._id);
expect(insertedFile.name).to.equal(file.name); expect(insertedFile.name).to.equal(file.name);
@ -113,7 +113,7 @@ describe('File model', () => {
expect(insertedFile.mimetype).to.equal(file.mimetype); expect(insertedFile.mimetype).to.equal(file.mimetype);
const updateData = { const updateData = {
_id: 12345, _id: '12345',
name: 'updated filename', name: 'updated filename',
filename: 'updated randomname', filename: 'updated randomname',
path: '/uploads/updated randomname', path: '/uploads/updated randomname',
@ -126,7 +126,7 @@ describe('File model', () => {
expect(file._id).to.equal(insertedFile._id); 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._id).to.equal(savedFile._id);
expect(updatedFile.name).to.equal(updateData.name); expect(updatedFile.name).to.equal(updateData.name);
@ -139,7 +139,7 @@ describe('File model', () => {
expect(file._id).to.be.undefined; 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; expect(removedFile).to.be.null;
}); });
@ -157,16 +157,18 @@ describe('File model', () => {
const savedFile = await file.save(); const savedFile = await file.save();
const foundFile = await File.get(savedFile._id); if (savedFile._id !== undefined){
const foundFile = await File.get(savedFile._id);
const {data} = foundFile; const { data } = foundFile;
expect(data._id).to.equal(savedFile._id); expect(data._id).to.equal(savedFile._id);
expect(data.name).to.equal(savedFile.name); expect(data.name).to.equal(savedFile.name);
expect(data.filename).to.equal(savedFile.filename); expect(data.filename).to.equal(savedFile.filename);
expect(data.path).to.equal(savedFile.path); expect(data.path).to.equal(savedFile.path);
expect(data.size).to.equal(savedFile.size); expect(data.size).to.equal(savedFile.size);
expect(data.mimetype).to.equal(savedFile.mimetype); expect(data.mimetype).to.equal(savedFile.mimetype);
}
await file.destroy(); await file.destroy();
}); });
@ -184,16 +186,18 @@ describe('File model', () => {
const savedFile = await file.save(); const savedFile = await file.save();
const foundFile = await File.getByFilename(savedFile.filename); if (savedFile.filename !== undefined){
const foundFile = await File.getByFilename(savedFile.filename);
const {data} = foundFile; const { data } = foundFile;
expect(data._id).to.equal(savedFile._id); expect(data._id).to.equal(savedFile._id);
expect(data.name).to.equal(savedFile.name); expect(data.name).to.equal(savedFile.name);
expect(data.filename).to.equal(savedFile.filename); expect(data.filename).to.equal(savedFile.filename);
expect(data.path).to.equal(savedFile.path); expect(data.path).to.equal(savedFile.path);
expect(data.size).to.equal(savedFile.size); expect(data.size).to.equal(savedFile.size);
expect(data.mimetype).to.equal(savedFile.mimetype); expect(data.mimetype).to.equal(savedFile.mimetype);
}
await file.destroy(); await file.destroy();
}); });
@ -218,7 +222,7 @@ describe('File model', () => {
const savedFiles = await Promise.all(filesToSave.map(file => file.save())); 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); expect(foundFiles.length).to.equal(2);

View file

@ -1,15 +1,16 @@
const {expect} = require('chai'); import { expect } from 'chai';
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const config = require('../../config'); import config from 'config';
const Page = require('../../src/models/page'); import Page from '../../backend/models/page';
const {pages} = require('../../src/utils/database'); import translateString from '../../backend/utils/translation';
const translateString = require('../../src/utils/translation'); import database from '../../backend/utils/database';
const pages = database['pages'];
describe('Page model', () => { describe('Page model', () => {
const transformToUri = (text: string): string => {
const transformToUri = (string) => { return translateString(text
return translateString(string
.replace(/&nbsp;/g, ' ') .replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ') .replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ') .replace(/ +/g, ' ')
@ -20,7 +21,7 @@ describe('Page model', () => {
}; };
after(() => { after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './pages.db');
if (fs.existsSync(pathToDB)) { if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB); fs.unlinkSync(pathToDB);
@ -40,7 +41,7 @@ describe('Page model', () => {
expect(data.body).to.be.undefined; expect(data.body).to.be.undefined;
expect(data.parent).to.be.equal('0'); expect(data.parent).to.be.equal('0');
page = new Page(null); page = new Page();
data = page.data; data = page.data;
@ -83,7 +84,7 @@ describe('Page model', () => {
expect(json.parent).to.be.equal('0'); expect(json.parent).to.be.equal('0');
const update = { const update = {
_id: 12345, _id: '12345',
body: { body: {
blocks: [ blocks: [
{ {
@ -122,7 +123,7 @@ describe('Page model', () => {
}; };
const page = new Page(initialData); const page = new Page(initialData);
let savedPage = await page.save(); const savedPage = await page.save();
expect(savedPage._id).not.be.undefined; expect(savedPage._id).not.be.undefined;
expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text); expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text);
@ -188,7 +189,7 @@ describe('Page model', () => {
const firstPage = new Page(initialData); const firstPage = new Page(initialData);
let firstSavedPage = await firstPage.save(); let firstSavedPage = await firstPage.save();
const secondPage = new Page(initialData); 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'); 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); expect(firstSavedPage.uri).to.equal(newUri);
const thirdPage = new Page(initialData); 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)); expect(thirdSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
}); });
@ -222,6 +223,12 @@ describe('Page model', () => {
const savedPage = await page.save(); const savedPage = await page.save();
if (savedPage._id == undefined) {
await page.destroy();
return;
}
const foundPage = await Page.get(savedPage._id); const foundPage = await Page.get(savedPage._id);
const {data} = foundPage; const {data} = foundPage;
@ -302,26 +309,29 @@ describe('Page model', () => {
} }
} }
] ]
} },
parent: parentId,
} }
); );
child.parent = parent;
const {_id: childId} = await child.save(); const {_id: childId} = await child.save();
const testedParent = await child.parent; const testedParent = await child.getParent();
expect(testedParent._id).to.equal(parentId); expect(testedParent).to.be.not.null;
expect(testedParent.title).to.equal(parent.body.blocks[0].data.text); if (testedParent) {
expect(testedParent.uri).to.equal(transformToUri(parent.body.blocks[0].data.text)); expect(testedParent._id).to.equal(parentId);
expect(testedParent.body).to.deep.equal(parent.body); 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; const children = await parent.children;
expect(children.length).to.equal(1); 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._id).to.equal(childId);
expect(testedChild.title).to.equal(child.body.blocks[0].data.text); expect(testedChild.title).to.equal(child.body.blocks[0].data.text);
@ -354,20 +364,22 @@ describe('Page model', () => {
}); });
it('test deletion', async () => { it('test deletion', async () => {
const pageIndexes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const pages = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const orders = { const orders = {
'0' : ['1', '2', '3'], '0' : ['1', '2', '3'],
'1' : ['4', '5'], '1' : ['4', '5'],
'5' : ['6', '7', '8'], '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) { if (!order) {
const found = pages.indexOf(startFrom); const found = pageIndexes.indexOf(startFrom);
pages.splice(found, 1);
pageIndexes.splice(found, 1);
return; return;
} }
@ -375,8 +387,8 @@ describe('Page model', () => {
deleteRecursively(id); deleteRecursively(id);
}); });
const found = pages.indexOf(startFrom); const found = pageIndexes.indexOf(startFrom);
pages.splice(found, 1); pageIndexes.splice(found, 1);
} }
}); });
}); });

View file

@ -1,13 +1,15 @@
const {expect} = require('chai'); import { expect } from 'chai';
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const config = require('../../config'); import config from 'config';
const PageOrder = require('../../src/models/pageOrder'); import PageOrder from '../../backend/models/pageOrder';
const {pagesOrder} = require('../../src/utils/database'); import database from '../../backend/utils/database';
const pagesOrder = database['pagesOrder'];
describe('PageOrder model', () => { describe('PageOrder model', () => {
after(() => { after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db'); const pathToDB = path.resolve(__dirname, '../../../', config.get('database'), './pagesOrder.db');
if (fs.existsSync(pathToDB)) { if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB); fs.unlinkSync(pathToDB);
@ -15,7 +17,7 @@ describe('PageOrder model', () => {
}); });
it('Empty Model', async () => { it('Empty Model', async () => {
let pageOrder = new PageOrder(); const pageOrder = new PageOrder();
expect(pageOrder.data).to.be.a('object'); expect(pageOrder.data).to.be.a('object');
@ -25,7 +27,7 @@ describe('PageOrder model', () => {
expect(data.page).to.be.to.equal('0'); expect(data.page).to.be.to.equal('0');
expect(data.order).to.be.an('array').that.is.empty; expect(data.order).to.be.an('array').that.is.empty;
page = new PageOrder(null); let page = new PageOrder();
data = page.data; data = page.data;
@ -53,13 +55,13 @@ describe('PageOrder model', () => {
order: ['1', '2'] order: ['1', '2']
}; };
const pageOrder = new PageOrder(testData); const pageOrder = new PageOrder(testData);
let {data} = await pageOrder.save(); const {data} = await pageOrder.save();
expect(data._id).not.be.undefined; expect(data._id).not.be.undefined;
expect(data.page).to.equal(testData.page); expect(data.page).to.equal(testData.page);
expect(data.order).to.deep.equals(testData.order); 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._id).to.equal(data._id);
expect(insertedPageOrder.page).to.equal(data.page); expect(insertedPageOrder.page).to.equal(data.page);
expect(insertedPageOrder.order).to.deep.equal(data.order); expect(insertedPageOrder.order).to.deep.equal(data.order);
@ -74,7 +76,7 @@ describe('PageOrder model', () => {
expect(pageOrder.data._id).to.equal(insertedPageOrder._id); 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.page).to.equal(updateData.page);
expect(updatedData.order).to.deep.equal(updateData.order); expect(updatedData.order).to.deep.equal(updateData.order);
@ -97,9 +99,11 @@ describe('PageOrder model', () => {
await pageOrder.save(); await pageOrder.save();
pageOrder.push('3'); pageOrder.push('3');
expect(pageOrder.data.order).to.be.an('array').that.is.not.empty; expect(pageOrder.data.order).to.be.an('array').that.is.not.empty;
pageOrder.data.order.forEach((el) => { if (pageOrder.data.order !== undefined) {
expect(el).to.be.an('string') pageOrder.data.order.forEach((el) => {
}); expect(el).to.be.an('string');
});
}
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3']); expect(pageOrder.data.order).to.deep.equals(['1', '2', '3']);
@ -130,11 +134,13 @@ describe('PageOrder model', () => {
const pageOrder = new PageOrder(testData); const pageOrder = new PageOrder(testData);
const insertedData = await pageOrder.save(); const insertedData = await pageOrder.save();
const insertedPageOrder = await PageOrder.get(insertedData.data.page); if (insertedData.data.page !== undefined) {
expect(insertedPageOrder).to.instanceOf(PageOrder); const insertedPageOrder = await PageOrder.get(insertedData.data.page);
expect(insertedPageOrder.data._id).to.be.equal(insertedData.data._id); 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.page).to.be.equal('0');
expect(emptyInstance.data.order).to.be.an('array').that.is.empty; expect(emptyInstance.data.order).to.be.an('array').that.is.empty;

View file

@ -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'); import rcParser from '../backend/utils/rcparser';
const fs = require('fs');
const path = require('path');
const config = require('../config');
const rcParser = require('../src/utils/rcparser');
const rcPath = path.resolve(process.cwd(), config.rcFile); const rcPath = path.resolve(process.cwd(), config.get('rcFile'));
describe('RC file parser test', () => { describe('RC file parser test', () => {
beforeEach(function () {
this.sinon.stub(console, 'log');
});
afterEach(() => { afterEach(() => {
if (fs.existsSync(rcPath)) { if (fs.existsSync(rcPath)) {
fs.unlinkSync(rcPath); fs.unlinkSync(rcPath);
@ -27,25 +23,27 @@ describe('RC file parser test', () => {
it('Invalid JSON formatted config', () => { it('Invalid JSON formatted config', () => {
const invalidJson = '{title: "Codex Docs"}'; const invalidJson = '{title: "Codex Docs"}';
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, invalidJson, 'utf8'); fs.writeFileSync(rcPath, invalidJson, 'utf8');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.calledOnce).to.be.true;
expect(console.log.calledWith('CodeX Docs rc file should be in JSON format.')).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); expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS);
spy.restore();
}); });
it('Normal config', () => { it('Normal config', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
{title: 'Option 1', uri: '/option1'}, { title: 'Option 1', uri: '/option1' },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
@ -58,10 +56,10 @@ describe('RC file parser test', () => {
it('Missed title', () => { it('Missed title', () => {
const normalConfig = { const normalConfig = {
menu: [ menu: [
{title: 'Option 1', uri: '/option1'}, { title: 'Option 1', uri: '/option1' },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
@ -74,7 +72,7 @@ describe('RC file parser test', () => {
it('Missed menu', () => { it('Missed menu', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation' title: 'Documentation',
}; };
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
@ -89,21 +87,23 @@ describe('RC file parser test', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: { menu: {
0: {title: 'Option 1', uri: '/option1'}, 0: { title: 'Option 1', uri: '/option1' },
1: {title: 'Option 2', uri: '/option2'}, 1: { title: 'Option 2', uri: '/option2' },
2: {title: 'Option 3', uri: '/option3'} 2: { title: 'Option 3', uri: '/option3' },
} },
}; };
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const spy = sinon.spy(console, 'log');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.calledOnce).to.be.true;
expect(console.log.calledWith('Menu section in the rc file must be an array.')).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.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu); expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu);
spy.restore();
}); });
it('Menu option is a string', () => { it('Menu option is a string', () => {
@ -111,15 +111,15 @@ describe('RC file parser test', () => {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
'Option 1', 'Option 1',
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
const expectedMenu = [ const expectedMenu = [
{title: 'Option 1', uri: '/option-1'}, { title: 'Option 1', uri: '/option-1' },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
]; ];
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
@ -134,129 +134,139 @@ describe('RC file parser test', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
[ {title: 'Option 1', uri: '/option1'} ], [ { title: 'Option 1', uri: '/option1' } ],
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
const expectedMenu = [ const expectedMenu = [
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
]; ];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.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.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.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
}); });
it('Menu option title is undefined', () => { it('Menu option title is undefined', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
{uri: '/option1'}, { uri: '/option1' },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
const expectedMenu = [ const expectedMenu = [
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
]; ];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.calledOnce).to.be.true;
expect(console.log.calledWith('Menu option #1 title must be a string.')).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.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
}); });
it('Menu option title is not a string', () => { it('Menu option title is not a string', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
{title: [], uri: '/option1'}, { title: [], uri: '/option1' },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
const expectedMenu = [ const expectedMenu = [
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
]; ];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.calledOnce).to.be.true;
expect(console.log.calledWith('Menu option #1 title must be a string.')).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.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
}); });
it('Menu option uri is undefined', () => { it('Menu option uri is undefined', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
{title: 'Option 1'}, { title: 'Option 1' },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
const expectedMenu = [ const expectedMenu = [
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
]; ];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.calledOnce).to.be.true;
expect(console.log.calledWith('Menu option #1 uri must be a string.')).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.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
}); });
it('Menu option title is not a string', () => { it('Menu option title is not a string', () => {
const normalConfig = { const normalConfig = {
title: 'Documentation', title: 'Documentation',
menu: [ menu: [
{title: 'Option 1', uri: []}, { title: 'Option 1', uri: [] },
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
] ],
}; };
const expectedMenu = [ const expectedMenu = [
{title: 'Option 2', uri: '/option2'}, { title: 'Option 2', uri: '/option2' },
{title: 'Option 3', uri: '/option3'} { title: 'Option 3', uri: '/option3' },
]; ];
const spy = sinon.spy(console, 'log');
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8'); fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
const parsedConfig = rcParser.getConfiguration(); const parsedConfig = rcParser.getConfiguration();
expect(console.log.calledOnce).to.be.true; expect(spy.calledOnce).to.be.true;
expect(console.log.calledWith('Menu option #1 uri must be a string.')).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.title).to.be.equal(normalConfig.title);
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu); expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
spy.restore();
}); });
}); });

View file

@ -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 {expect} = chai;
const app = server.app;
chai.use(chaiHTTP); chai.use(chaiHTTP);
describe('Aliases REST: ', () => { describe('Aliases REST: ', () => {
let agent; let agent: ChaiHttp.Agent;
before(async () => { before(async () => {
agent = chai.request.agent(app); agent = chai.request.agent(app);
}); });
after(async () => { 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)) { if (fs.existsSync(pathToDB)) {
fs.unlinkSync(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)) { if (fs.existsSync(pathToAliasDB)) {
fs.unlinkSync(pathToAliasDB); fs.unlinkSync(pathToAliasDB);

View file

@ -1,22 +1,23 @@
const {app} = require('../../bin/www'); import fs from 'fs';
const model = require('../../src/models/page'); import path from 'path';
const Page = require('../../src/models/page'); import config from 'config';
const PageOrder = require('../../src/models/pageOrder'); import chai from 'chai';
const translateString = require('../../src/utils/translation'); 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 {expect} = chai;
const app = server.app;
chai.use(chaiHTTP); chai.use(chaiHTTP);
describe('Pages REST: ', () => { describe('Pages REST: ', () => {
let agent; let agent: ChaiHttp.Agent;
const transformToUri = (string) => { const transformToUri = (text: string):string => {
return translateString(string return translateString(text
.replace(/&nbsp;/g, ' ') .replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ') .replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ') .replace(/ +/g, ' ')
@ -31,9 +32,9 @@ describe('Pages REST: ', () => {
}); });
after(async () => { after(async () => {
const pathToPagesDB = path.resolve(__dirname, '../../', config.database, './pages.db'); const pathToPagesDB = path.resolve(__dirname, '../../../', config.get('database'), './pages.db');
const pathToPagesOrderDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db'); const pathToPagesOrderDB = path.resolve(__dirname, '../../../', config.get('database'), './pagesOrder.db');
const pathToAliasesDB = path.resolve(__dirname, '../../', config.database, './aliases.db'); const pathToAliasesDB = path.resolve(__dirname, '../../../', config.get('database'), './aliases.db');
if (fs.existsSync(pathToPagesDB)) { if (fs.existsSync(pathToPagesDB)) {
fs.unlinkSync(pathToPagesDB); fs.unlinkSync(pathToPagesDB);
@ -93,15 +94,15 @@ describe('Pages REST: ', () => {
it('Page data validation on create', async () => { it('Page data validation on create', async () => {
const res = await agent const res = await agent
.put('/api/page') .put('/api/page')
.send({someField: 'Some text'}); .send({ someField: 'Some text' });
expect(res).to.have.status(400); expect(res).to.have.status(400);
expect(res).to.be.json; expect(res).to.be.json;
const {success, error} = res.body; const { success, error } = res.body;
expect(success).to.be.false; 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 () => { it('Finding page', async () => {
@ -363,7 +364,7 @@ describe('Pages REST: ', () => {
expect(error).to.equal('Page with given id does not exist'); expect(error).to.equal('Page with given id does not exist');
}); });
async function createPageTree() { async function createPageTree():Promise<string[]> {
/** /**
* Creating page tree * Creating page tree
* *
@ -474,7 +475,7 @@ describe('Pages REST: ', () => {
} }
it('Removing a page and its children', async () => { it('Removing a page and its children', async () => {
let pages = await createPageTree(); const pages = await createPageTree();
/** /**
* Deleting from tree page1 * Deleting from tree page1

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -1,44 +1,44 @@
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const fileType = require('file-type'); import fileType from 'file-type';
const chai = require('chai'); import chai from 'chai';
const chaiHTTP = require('chai-http'); import chaiHTTP from 'chai-http';
const rimraf = require('rimraf'); import rimraf from 'rimraf';
import config from 'config';
import server from '../../bin/server';
import model from '../../backend/models/file';
const {expect} = chai; const {expect} = chai;
const app = server.app;
const {app} = require('../../bin/www');
const model = require('../../src/models/file');
const config = require('../../config');
chai.use(chaiHTTP); chai.use(chaiHTTP);
describe('Transport routes: ', () => { describe('Transport routes: ', () => {
let agent; let agent: ChaiHttp.Agent;
before(async () => { before(async () => {
agent = chai.request.agent(app); agent = chai.request.agent(app);
if (!fs.existsSync('./' + config.uploads)) { if (!fs.existsSync('./' + config.get('uploads'))) {
fs.mkdirSync('./' + config.uploads); fs.mkdirSync('./' + config.get('uploads'));
} }
}); });
after(async () => { 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)) { if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB); fs.unlinkSync(pathToDB);
} }
if (fs.existsSync('./' + config.uploads)) { if (fs.existsSync('./' + config.get('uploads'))) {
rimraf.sync('./' + config.uploads); rimraf.sync('./' + config.get('uploads'));
} }
}); });
it('Uploading an image', async () => { it('Uploading an image', async () => {
const name = 'test_image.png'; 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 const res = await agent
.post('/api/transport/image') .post('/api/transport/image')
.attach('image', image, name); .attach('image', image, name);
@ -55,19 +55,27 @@ describe('Transport routes: ', () => {
expect(file.name).to.equal(name); expect(file.name).to.equal(name);
expect(file.filename).to.equal(body.filename); expect(file.filename).to.equal(body.filename);
expect(file.path).to.equal(body.path); 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 const type = await fileType.fromBuffer(image);
.get(file.path); 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(file.path).to.be.not.undefined;
expect(getRes).to.have.header('content-type', fileType(image).mime); 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 () => { it('Uploading an image with map option', async () => {
const name = 'test_image.png'; 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 const res = await agent
.post('/api/transport/image') .post('/api/transport/image')
.attach('image', image, name) .attach('image', image, name)
@ -88,7 +96,7 @@ describe('Transport routes: ', () => {
it('Uploading a file', async () => { it('Uploading a file', async () => {
const name = 'test_file.json'; 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 const res = await agent
.post('/api/transport/file') .post('/api/transport/file')
.attach('file', json, name); .attach('file', json, name);
@ -107,16 +115,19 @@ describe('Transport routes: ', () => {
expect(file.path).to.equal(body.path); expect(file.path).to.equal(body.path);
expect(file.size).to.equal(json.byteLength); expect(file.size).to.equal(json.byteLength);
const getRes = await agent expect(file.path).to.be.not.undefined;
.get(file.path); if (file.path !== undefined){
const getRes = await agent
.get(file.path);
expect(getRes).to.have.status(200); expect(getRes).to.have.status(200);
expect(getRes).to.have.header('content-type', new RegExp(`^${file.mimetype}`)); expect(getRes).to.have.header('content-type', new RegExp(`^${file.mimetype}`));
}
}); });
it('Uploading a file with map option', async () => { it('Uploading a file with map option', async () => {
const name = 'test_file.json'; 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 const res = await agent
.post('/api/transport/file') .post('/api/transport/file')
.attach('file', json, name) .attach('file', json, name)
@ -155,11 +166,14 @@ describe('Transport routes: ', () => {
expect(file.path).to.equal(body.path); expect(file.path).to.equal(body.path);
expect(file.size).to.equal(body.size); expect(file.size).to.equal(body.size);
const getRes = await agent expect(file.path).to.be.not.undefined;
.get(file.path); if (file.path !== undefined){
const getRes = await agent
.get(file.path);
expect(getRes).to.have.status(200); expect(getRes).to.have.status(200);
expect(getRes).to.have.header('content-type', file.mimetype); expect(getRes).to.have.header('content-type', file.mimetype);
}
}); });
it('Send an file URL to fetch with map option', async () => { it('Send an file URL to fetch with map option', async () => {
@ -193,7 +207,7 @@ describe('Transport routes: ', () => {
expect(body.success).to.equal(0); expect(body.success).to.equal(0);
const name = 'test_file.json'; 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 res = await agent
.post('/api/transport/file') .post('/api/transport/file')
.attach('file', json, name) .attach('file', json, name)
@ -216,7 +230,7 @@ describe('Transport routes: ', () => {
expect(body.success).to.equal(0); expect(body.success).to.equal(0);
let name = 'test_file.json'; 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 res = await agent
.post('/api/transport/image') .post('/api/transport/image')
.attach('image', json, name); .attach('image', json, name);
@ -224,7 +238,7 @@ describe('Transport routes: ', () => {
expect(res).to.have.status(400); expect(res).to.have.status(400);
name = 'test_image.png'; 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 res = await agent
.post('/api/transport/image') .post('/api/transport/image')
.attach('image', image, name) .attach('image', image, name)

View file

@ -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);
};
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
});
});

72
tsconfig.json Normal file
View file

@ -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. */
}
}

2706
yarn.lock

File diff suppressed because it is too large Load diff