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": [
"codex"
"codex/ts",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": [
"chai-friendly"
"chai-friendly",
"@typescript-eslint"
],
"env": {
"mocha": true
},
"rules": {
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": 2
"no-unused-expressions": 1,
"chai-friendly/no-unused-expressions": 2,
"@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-magic-numbers": 0,
"@typescript-eslint/no-explicit-any": 1
},
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"globals": {
"fetch": true,
"alert": true

3
.gitignore vendored
View file

@ -76,3 +76,6 @@ typings/
# Uploads
/public/uploads
/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
#### Compile to Javascript
```
$ yarn compile
```
#### Start the server

View file

@ -1,6 +1,7 @@
{
"port": 3000,
"database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "public/uploads",
"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,
"database": ".db",
"rcFile": "./.codexdocsrc",
"uploads": "public/uploads",
"secret": "iamasecretstring"
}

View file

@ -1,7 +1,7 @@
{
"port": 3001,
"database": ".testdb",
"rcFile": "./test/.codexdocsrc",
"rcFile": "./src/test/.codexdocsrc",
"uploads": "public/uploads_test",
"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
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 ./
RUN yarn install --prod
RUN yarn install
COPY . .
RUN yarn compile
CMD ["yarn", "start"]

View file

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

View file

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

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
@ -11,7 +11,7 @@ class Aliases {
* @param {string} aliasName - alias name of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
public static async get(aliasName: string): Promise<Alias> {
const alias = await Alias.get(aliasName);
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');
const Alias = require('../models/alias');
import Page, { PageData } from '../models/page';
import Alias from '../models/alias';
type PageDataFields = keyof PageData;
/**
* @class Pages
@ -11,7 +13,7 @@ class Pages {
*
* @returns {['title', 'body']}
*/
static get REQUIRED_FIELDS() {
public static get REQUIRED_FIELDS(): Array<PageDataFields> {
return [ 'body' ];
}
@ -21,8 +23,8 @@ class Pages {
* @param {string} id - page id
* @returns {Promise<Page>}
*/
static async get(id) {
const page = await Model.get(id);
public static async get(id: string): Promise<Page> {
const page = await Page.get(id);
if (!page._id) {
throw new Error('Page with given id does not exist');
@ -36,8 +38,8 @@ class Pages {
*
* @returns {Promise<Page[]>}
*/
static async getAll() {
return Model.getAll();
public static async getAll(): Promise<Page[]> {
return Page.getAll();
}
/**
@ -46,20 +48,28 @@ class Pages {
* @param {string} parent - id of current page
* @returns {Promise<Page[]>}
*/
static async getAllExceptChildren(parent) {
public static async getAllExceptChildren(parent: string): Promise<Page[]> {
const pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
return pagesAvailable.filter((item) => item !== null);
const nullFilteredPages: Page[] = [];
pagesAvailable.forEach(async item => {
if (item instanceof Page) {
nullFilteredPages.push(item);
}
});
return nullFilteredPages;
}
/**
* Set all children elements to null
*
* @param {Page[]} [pagesAvailable] - Array of all pages
* @param {Array<Page|null>} [pagesAvailable] - Array of all pages
* @param {string} parent - id of parent 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) => {
if (item === null || item._parent !== parent) {
return;
@ -74,14 +84,14 @@ class Pages {
/**
* Create new page model and save it in the database
*
* @param {PageData} data
* @param {PageData} data - info about page
* @returns {Promise<Page>}
*/
static async insert(data) {
public static async insert(data: PageData): Promise<Page> {
try {
Pages.validate(data);
const page = new Model(data);
const page = new Page(data);
const insertedPage = await page.save();
@ -95,18 +105,80 @@ class Pages {
}
return insertedPage;
} catch (validationError) {
throw new Error(validationError);
} catch (e) {
throw new Error('validationError');
}
}
/**
* Update page with given id in the database
*
* @param {string} id - page id
* @param {PageData} data - info about page
* @returns {Promise<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
*
* @param {PageData} data
* @param {PageData} data - info about page
* @throws {Error} - validation error
*/
static validate(data) {
private static validate(data: PageData): void {
const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
if (!allRequiredFields) {
@ -131,64 +203,6 @@ class Pages {
throw new Error('Please, fill page Header');
}
}
/**
* Update page with given id in the database
*
* @param {string} id - page id
* @param {PageData} data
* @returns {Promise<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
@ -13,8 +14,8 @@ class PagesOrder {
* @param {string} parentId - of which page we want to get children order
* @returns {Promise<PageOrder>}
*/
static async get(parentId) {
const order = await Model.get(parentId);
public static async get(parentId: string): Promise<PageOrder> {
const order = await PageOrder.get(parentId);
if (!order._id) {
throw new Error('Page with given id does not contain order');
@ -26,10 +27,10 @@ class PagesOrder {
/**
* Returns all records about page's order
*
* @returns {Promise<PagesOrder[]>}
* @returns {Promise<PageOrder[]>}
*/
static async getAll() {
return Model.getAll();
public static async getAll(): Promise<PageOrder[]> {
return PageOrder.getAll();
}
/**
@ -38,8 +39,8 @@ class PagesOrder {
* @param {string} parentId - parent page's id
* @param {string} childId - new page pushed to the order
*/
static async push(parentId, childId) {
const order = await Model.get(parentId);
public static async push(parentId: string, childId: string): Promise<void> {
const order = await PageOrder.get(parentId);
order.push(childId);
await order.save();
@ -52,13 +53,13 @@ class PagesOrder {
* @param {string} newParentId - new parent page's id
* @param {string} targetPageId - page's id which is changing the parent page
*/
static async move(oldParentId, newParentId, targetPageId) {
const oldParentOrder = await Model.get(oldParentId);
public static async move(oldParentId: string, newParentId: string, targetPageId: string): Promise<void> {
const oldParentOrder = await PageOrder.get(oldParentId);
oldParentOrder.remove(targetPageId);
await oldParentOrder.save();
const newParentOrder = await Model.get(newParentId);
const newParentOrder = await PageOrder.get(newParentId);
newParentOrder.push(targetPageId);
await newParentOrder.save();
@ -73,14 +74,14 @@ class PagesOrder {
* @param {boolean} ignoreSelf - should we ignore current page in list or not
* @returns {Page[]}
*/
static async getOrderedChildren(pages, currentPageId, parentPageId, ignoreSelf = false) {
const children = await Model.get(parentPageId);
public static async getOrderedChildren(pages: Page[], currentPageId: string, parentPageId: string, ignoreSelf = false): Promise<Page[]> {
const children = await PageOrder.get(parentPageId);
const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id);
// Create unique array with ordered and unordered pages id
const ordered = [ ...new Set([...children.order, ...unordered]) ];
const ordered = Array.from(new Set([...children.order, ...unordered]));
const result = [];
const result: Page[] = [];
ordered.forEach(pageId => {
pages.forEach(page => {
@ -94,26 +95,26 @@ class PagesOrder {
}
/**
* @param {string[]} unordered
* @param {string[]} unordered - list of pages
* @param {string} currentPageId - page's id that changes the order
* @param {string} parentPageId - parent page's id that contains both two pages
* @param {string} putAbovePageId - page's id above which we put the target page
*/
static async update(unordered, currentPageId, parentPageId, putAbovePageId) {
const pageOrder = await Model.get(parentPageId);
public static async update(unordered: string[], currentPageId: string, parentPageId: string, putAbovePageId: string): Promise<void> {
const pageOrder = await PageOrder.get(parentPageId);
// Create unique array with ordered and unordered pages id
pageOrder.order = [ ...new Set([...pageOrder.order, ...unordered]) ];
pageOrder.order = Array.from(new Set([...pageOrder.order, ...unordered]));
pageOrder.putAbove(currentPageId, putAbovePageId);
await pageOrder.save();
}
/**
* @param parentId
* @param {string} parentId - identity of parent page
* @returns {Promise<void>}
*/
static async remove(parentId) {
const order = await Model.get(parentId);
public static async remove(parentId: string): Promise<void> {
const order = await PageOrder.get(parentId);
if (!order._id) {
throw new Error('Page with given id does not contain order');
@ -123,4 +124,4 @@ class PagesOrder {
}
}
module.exports = PagesOrder;
export default PagesOrder;

View file

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

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');
const { binaryMD5 } = require('../utils/crypto');
import crypto from '../utils/crypto';
import database from '../utils/database/index';
const binaryMD5 = crypto.binaryMD5;
const aliasesDb = database['aliases'];
/**
* @typedef {object} AliasData
@ -10,6 +13,13 @@ const { binaryMD5 } = require('../utils/crypto');
* @property {string} id - entity id
*
*/
export interface AliasData {
_id?: string;
hash?: string;
type?: string;
deprecated?: boolean;
id?: string;
}
/**
* @class Alias
@ -22,16 +32,40 @@ const { binaryMD5 } = require('../utils/crypto');
* @property {string} id - entity title
*/
class Alias {
public _id?: string;
public hash?: string;
public type?: string;
public deprecated?: boolean;
public id?: string;
/**
* @class
*
* @param {AliasData} data - info about alias
* @param {string} aliasName - alias of entity
*/
constructor(data: AliasData = {}, aliasName = '') {
if (data === null) {
data = {};
}
if (data._id) {
this._id = data._id;
}
if (aliasName) {
this.hash = binaryMD5(aliasName);
}
this.data = data;
}
/**
* Return Alias types
*
* @returns {object}
*/
static get types() {
public static get types(): { PAGE: string } {
return {
PAGE: 'page',
};
};
}
/**
* Find and return alias with given alias
@ -39,7 +73,7 @@ class Alias {
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
public static async get(aliasName: string): Promise<Alias> {
const hash = binaryMD5(aliasName);
let data = await aliasesDb.findOne({
hash: hash,
@ -54,22 +88,17 @@ class Alias {
}
/**
* @class
* Mark alias as deprecated
*
* @param {AliasData} data
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
constructor(data = {}, aliasName = '') {
if (data === null) {
data = {};
}
if (data._id) {
this._id = data._id;
}
if (aliasName) {
this.hash = binaryMD5(aliasName);
}
this.data = data;
public static async markAsDeprecated(aliasName: string): Promise<Alias> {
const alias = await Alias.get(aliasName);
alias.deprecated = true;
return alias.save();
}
/**
@ -77,9 +106,9 @@ class Alias {
*
* @returns {Promise<Alias>}
*/
async save() {
public async save(): Promise<Alias> {
if (!this._id) {
const insertedRow = await aliasesDb.insert(this.data);
const insertedRow = await aliasesDb.insert(this.data) as { _id: string };
this._id = insertedRow._id;
} else {
@ -92,9 +121,9 @@ class Alias {
/**
* Set AliasData object fields to internal model fields
*
* @param {AliasData} aliasData
* @param {AliasData} aliasData - info about alias
*/
set data(aliasData) {
public set data(aliasData: AliasData) {
const { id, type, hash, deprecated } = aliasData;
this.id = id || this.id;
@ -108,7 +137,7 @@ class Alias {
*
* @returns {AliasData}
*/
get data() {
public get data(): AliasData {
return {
_id: this._id,
id: this.id,
@ -119,23 +148,9 @@ class Alias {
}
/**
* Mark alias as deprecated
*
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async markAsDeprecated(aliasName) {
const alias = await Alias.get(aliasName);
alias.deprecated = true;
return alias.save();
}
/**
* @returns {Promise<Alias>}
*/
async destroy() {
public async destroy(): Promise<Alias> {
await aliasesDb.remove({ _id: 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
@ -10,6 +12,15 @@ const { files: filesDb } = require('../utils/database/index');
* @property {string} mimetype - file MIME type
* @property {number} size - size of the file in
*/
export interface FileData {
_id?: string;
name?: string;
filename?: string;
path?: string;
mimetype?: string;
size?: number;
[key: string]: string | number | undefined;
}
/**
* @class File
@ -23,48 +34,19 @@ const { files: filesDb } = require('../utils/database/index');
* @property {number} size - size of the file in
*/
class File {
/**
* Find and return model of file with given id
*
* @param {string} _id - file id
* @returns {Promise<File>}
*/
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)));
}
public _id?: string;
public name?: string;
public filename?: string;
public path?: string;
public mimetype?: string;
public size?: number;
/**
* @class
*
* @param {FileData} data
* @param {FileData} data - info about file
*/
constructor(data = {}) {
constructor(data: FileData = {}) {
if (data === null) {
data = {};
}
@ -75,13 +57,48 @@ class File {
this.data = data;
}
/**
* Find and return model of file with given id
*
* @param {string} _id - file id
* @returns {Promise<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
*
* @param {FileData} fileData
* @param {FileData} fileData - info about file
*/
set data(fileData) {
public set data(fileData: FileData) {
const { name, filename, path, mimetype, size } = fileData;
this.name = name || this.name;
@ -96,7 +113,7 @@ class File {
*
* @returns {FileData}
*/
get data() {
public get data(): FileData {
return {
_id: this._id,
name: this.name,
@ -112,9 +129,9 @@ class File {
*
* @returns {Promise<File>}
*/
async save() {
public async save(): Promise<File> {
if (!this._id) {
const insertedRow = await filesDb.insert(this.data);
const insertedRow = await filesDb.insert(this.data) as { _id: string };
this._id = insertedRow._id;
} else {
@ -129,7 +146,7 @@ class File {
*
* @returns {Promise<File>}
*/
async destroy() {
public async destroy(): Promise<File> {
await filesDb.remove({ _id: this._id });
delete this._id;
@ -137,24 +154,24 @@ class File {
return this;
}
/**
* Removes unnecessary public folder prefix
*
* @param {string} path
* @returns {string}
*/
processPath(path) {
return path.replace(/^public/, '');
}
/**
* Return readable file data
*
* @returns {FileData}
*/
toJSON() {
public toJSON(): FileData {
return this.data;
}
/**
* Removes unnecessary public folder prefix
*
* @param {string} path - input path to be processed
* @returns {string}
*/
private processPath(path: string): string {
return path.replace(/^public/, '');
}
}
module.exports = File;
export default File;

View file

@ -1,5 +1,7 @@
const urlify = require('../utils/urlify');
const { pages: pagesDb } = require('../utils/database/index');
import urlify from '../utils/urlify';
import database from '../utils/database/index';
const pagesDb = database['pages'];
/**
* @typedef {object} PageData
@ -9,6 +11,13 @@ const { pages: pagesDb } = require('../utils/database/index');
* @property {*} body - page body
* @property {string} parent - id of parent page
*/
export interface PageData {
_id?: string;
title?: string;
uri?: string;
body?: any;
parent?: string;
}
/**
* @class Page
@ -21,48 +30,18 @@ const { pages: pagesDb } = require('../utils/database/index');
* @property {string} _parent - id of parent page
*/
class Page {
/**
* Find and return model of page with given id
*
* @param {string} _id - page id
* @returns {Promise<Page>}
*/
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)));
}
public _id?: string;
public body?: any;
public title?: string;
public uri?: string;
public _parent?: string;
/**
* @class
*
* @param {PageData} data
* @param {PageData} data - page's data
*/
constructor(data = {}) {
constructor(data: PageData = {}) {
if (data === null) {
data = {};
}
@ -74,12 +53,48 @@ class Page {
this.data = data;
}
/**
* Find and return model of page with given id
*
* @param {string} _id - page id
* @returns {Promise<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
*
* @param {PageData} pageData
* @param {PageData} pageData - page's data
*/
set data(pageData) {
public set data(pageData: PageData) {
const { body, parent, uri } = pageData;
this.body = body || this.body;
@ -93,7 +108,7 @@ class Page {
*
* @returns {PageData}
*/
get data() {
public get data(): PageData {
return {
_id: this._id,
title: this.title,
@ -103,32 +118,12 @@ class Page {
};
}
/**
* Extract first header from editor data
*
* @returns {string}
*/
extractTitleFromBody() {
const headerBlock = this.body ? this.body.blocks.find(block => block.type === 'header') : '';
return headerBlock ? headerBlock.data.text : '';
}
/**
* Transform title for uri
*
* @returns {string}
*/
transformTitleToUri() {
return urlify(this.title);
}
/**
* Link given page as parent
*
* @param {Page} parentPage
* @param {Page} parentPage - the page to be set as parent
*/
set parent(parentPage) {
public set parent(parentPage: Page) {
this._parent = parentPage._id;
}
@ -137,9 +132,10 @@ class Page {
*
* @returns {Promise<Page>}
*/
get parent() {
return pagesDb.findOne({ _id: this._parent })
.then(data => new Page(data));
public async getParent(): Promise<Page> {
const data = await pagesDb.findOne({ _id: this._parent });
return new Page(data);
}
/**
@ -147,9 +143,11 @@ class Page {
*
* @returns {Promise<Page[]>}
*/
get children() {
public get children(): Promise<Page[]> {
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>}
*/
async save() {
this.uri = await this.composeUri(this.uri);
public async save(): Promise<Page> {
if (this.uri !== undefined) {
this.uri = await this.composeUri(this.uri);
}
if (!this._id) {
const insertedRow = await pagesDb.insert(this.data);
const insertedRow = await pagesDb.insert(this.data) as { _id: string };
this._id = insertedRow._id;
} else {
@ -176,7 +176,7 @@ class Page {
*
* @returns {Promise<Page>}
*/
async destroy() {
public async destroy(): Promise<Page> {
await pagesDb.remove({ _id: this._id });
delete this._id;
@ -184,13 +184,22 @@ class Page {
return this;
}
/**
* Return readable page data
*
* @returns {PageData}
*/
public toJSON(): PageData {
return this.data;
}
/**
* Find and return available uri
*
* @returns {Promise<string>}
* @param uri
* @param uri - input uri to be composed
*/
async composeUri(uri) {
private async composeUri(uri: string): Promise<string> {
let pageWithSameUriCount = 0;
if (!this._id) {
@ -210,13 +219,28 @@ class Page {
}
/**
* Return readable page data
* Extract first header from editor data
*
* @returns {PageData}
* @returns {string}
*/
toJSON() {
return this.data;
private extractTitleFromBody(): string {
const headerBlock = this.body ? this.body.blocks.find((block: Record<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
@ -6,6 +8,11 @@ const { pagesOrder: db } = require('../utils/database/index');
* @property {string} page - page id
* @property {Array<string>} order - list of ordered pages
*/
export interface PageOrderData {
_id?: string;
page?: string;
order?: string[];
}
/**
* @class PageOrder
@ -14,44 +21,17 @@ const { pagesOrder: db } = require('../utils/database/index');
* Creates order for Pages with children
*/
class PageOrder {
/**
* Returns current Page's children order
*
* @param {string} pageId - page's id
* @returns {PageOrder}
*/
static async get(pageId) {
const order = await db.findOne({ page: pageId });
public _id?: string;
public page?: string;
private _order?: string[];
let data = {};
if (!order) {
data.page = pageId;
} else {
data = order;
}
return new PageOrder(data);
}
/**
* Find all pages which match passed query object
*
* @param {object} query
* @returns {Promise<Page[]>}
*/
static async getAll(query = {}) {
const docs = await db.find(query);
return Promise.all(docs.map(doc => new PageOrder(doc)));
}
/**
* @class
*
* @param {PageOrderData} data
* @param {PageOrderData} data - info about pageOrder
*/
constructor(data = {}) {
constructor(data: PageOrderData = {}) {
if (data === null) {
data = {};
}
@ -63,14 +43,46 @@ class PageOrder {
this.data = data;
}
/**
* Returns current Page's children order
*
* @param {string} pageId - page's id
* @returns {Promise<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
*
* @param {PageOrderData} pageOrderData
* @param {PageOrderData} pageOrderData - info about pageOrder
*/
set data(pageOrderData) {
this._page = pageOrderData.page || 0;
this._order = pageOrderData.order || [];
public set data(pageOrderData: PageOrderData) {
this.page = pageOrderData.page || '0';
this.order = pageOrderData.order || [];
}
/**
@ -78,11 +90,11 @@ class PageOrder {
*
* @returns {PageOrderData}
*/
get data() {
public get data(): PageOrderData {
return {
_id: this._id,
page: '' + this._page,
order: this._order,
page: '' + this.page,
order: this.order,
};
}
@ -91,9 +103,12 @@ class PageOrder {
*
* @param {string} pageId - page's id
*/
push(pageId) {
public push(pageId: string | number): void {
if (typeof pageId === 'string') {
this._order.push(pageId);
if (this.order === undefined) {
this.order = [];
}
this.order.push(pageId);
} else {
throw new Error('given id is not string');
}
@ -104,11 +119,15 @@ class PageOrder {
*
* @param {string} pageId - page's id
*/
remove(pageId) {
const found = this._order.indexOf(pageId);
public remove(pageId: string): void {
if (this.order === undefined) {
return;
}
const found = this.order.indexOf(pageId);
if (found >= 0) {
this._order.splice(found, 1);
this.order.splice(found, 1);
}
}
@ -116,9 +135,13 @@ class PageOrder {
* @param {string} currentPageId - page's id that changes the order
* @param {string} putAbovePageId - page's id above which we put the target page
*
* @returns void
* @returns {void}
*/
putAbove(currentPageId, putAbovePageId) {
public putAbove(currentPageId: string, putAbovePageId: string): void {
if (this.order === undefined) {
return;
}
const found1 = this.order.indexOf(putAbovePageId);
const found2 = this.order.indexOf(currentPageId);
@ -135,16 +158,20 @@ class PageOrder {
/**
* Returns page before passed page with id
*
* @param {string} pageId
* @param {string} pageId - identity of page
*/
getPageBefore(pageId) {
public getPageBefore(pageId: string): string | null {
if (this.order === undefined) {
return null;
}
const currentPageInOrder = this.order.indexOf(pageId);
/**
* If page not found or first return nothing
*/
if (currentPageInOrder <= 0) {
return;
return null;
}
return this.order[currentPageInOrder - 1];
@ -153,16 +180,20 @@ class PageOrder {
/**
* Returns page before passed page with id
*
* @param pageId
* @param pageId - identity of page
*/
getPageAfter(pageId) {
public getPageAfter(pageId: string): string | null {
if (this.order === undefined) {
return null;
}
const currentPageInOrder = this.order.indexOf(pageId);
/**
* If page not found or is last
*/
if (currentPageInOrder === -1 || currentPageInOrder === this.order.length - 1) {
return;
return null;
}
return this.order[currentPageInOrder + 1];
@ -171,7 +202,7 @@ class PageOrder {
/**
* @param {string[]} order - define new order
*/
set order(order) {
public set order(order: string[]) {
this._order = order;
}
@ -180,16 +211,18 @@ class PageOrder {
*
* @returns {string[]}
*/
get order() {
return this._order;
public get order(): string[] {
return this._order || [];
}
/**
* Save or update page data in the database
*
* @returns {Promise<PageOrder>}
*/
async save() {
public async save(): Promise<PageOrder> {
if (!this._id) {
const insertedRow = await db.insert(this.data);
const insertedRow = await db.insert(this.data) as { _id: string};
this._id = insertedRow._id;
} else {
@ -201,14 +234,14 @@ class PageOrder {
/**
* Remove page data from the database
*
* @returns {Promise<void>}
*/
async destroy() {
public async destroy(): Promise<void> {
await db.remove({ _id: 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 Aliases = require('../controllers/aliases');
const Pages = require('../controllers/pages');
const Alias = require('../models/alias');
const verifyToken = require('./middlewares/token');
/**
* GET /*
*
* Return document with given alias
*/
router.get('*', verifyToken, async (req, res) => {
router.get('*', verifyToken, async (req: Request, res: Response) => {
try {
let url = req.originalUrl.slice(1); // Cuts first '/' character
const queryParamsIndex = url.indexOf('?');
@ -21,11 +22,15 @@ router.get('*', verifyToken, async (req, res) => {
const alias = await Aliases.get(url);
if (alias.id === undefined) {
throw new Error('Alias not found');
}
switch (alias.type) {
case Alias.types.PAGE: {
const page = await Pages.get(alias.id);
const pageParent = await page.parent;
const pageParent = await page.getParent();
res.render('pages/page', {
page,
@ -37,9 +42,9 @@ router.get('*', verifyToken, async (req, res) => {
} catch (err) {
res.status(400).json({
success: false,
error: err.message,
error: err,
});
}
});
module.exports = router;
export default router;

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

View file

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

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

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');
const PagesOrder = require('../../controllers/pagesOrder');
const asyncMiddleware = require('../../utils/asyncMiddleware');
import { NextFunction, Request, Response } from 'express';
import Pages from '../../controllers/pages';
import PagesOrder from '../../controllers/pagesOrder';
import Page from '../../models/page';
import asyncMiddleware from '../../utils/asyncMiddleware';
import PageOrder from '../../models/pageOrder';
/**
* Process one-level pages list to parent-children list
@ -8,12 +11,12 @@ const asyncMiddleware = require('../../utils/asyncMiddleware');
* @param {string} parentPageId - parent page id
* @param {Page[]} pages - list of all available pages
* @param {PagesOrder[]} pagesOrder - list of pages order
* @param {number} level
* @param {number} currentLevel
* @param {number} level - max level recursion
* @param {number} currentLevel - current level of element
*
* @return {Page[]}
* @returns {Page[]}
*/
function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel = 1) {
function createMenuTree(parentPageId: string, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
const childrenOrder = pagesOrder.find(order => order.data.page === parentPageId);
/**
@ -21,16 +24,16 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
* if we got some children order on parents tree, then we push found pages in order sequence
* otherwise just find all pages includes parent tree
*/
let ordered = [];
let ordered: any[] = [];
if (childrenOrder) {
ordered = childrenOrder.order.map(pageId => {
ordered = childrenOrder.order.map((pageId: string) => {
return pages.find(page => page._id === pageId);
});
}
const unordered = pages.filter(page => page._parent === parentPageId);
const branch = [ ...new Set([...ordered, ...unordered]) ];
const branch = Array.from(new Set([...ordered, ...unordered]));
/**
* stop recursion when we got the passed max level
@ -44,20 +47,22 @@ function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel
*/
return branch.filter(page => page && page._id).map(page => {
return Object.assign({
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1)
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1),
}, page.data);
});
}
/**
* Middleware for all /page/... routes
* @param req
* @param res
* @param next
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
module.exports = asyncMiddleware(async (req, res, next) => {
export default asyncMiddleware(async (req: Request, res: Response, next: NextFunction) => {
/**
* Pages without parent
*
* @type {string}
*/
const parentIdOfRootPages = '0';

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

View file

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

View file

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

View file

@ -1,4 +1,7 @@
const translationTable = {
interface TransTable {
[key: string]: string;
}
const translationTable: TransTable = {
а: 'a',
б: 'b',
в: 'v',
@ -73,6 +76,10 @@ const translationTable = {
* @returns {string} - translated string
*/
module.exports = function translateString(string) {
/**
* @param {string} string - input text to be translated
* @returns {string} text - translated text
*/
export default function translateString(string: string): string {
return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char);
};
}

View file

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

View file

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

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

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

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

View file

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

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,17 +1,19 @@
const { app } = require('../bin/www');
const chai = require('chai');
const chaiHTTP = require('chai-http');
const { expect } = chai;
import chaiHTTP from 'chai-http';
import chai, { expect } from 'chai';
import server from '../bin/server';
const app = server.app;
chai.use(chaiHTTP);
describe('Express app', () => {
it('App is available', async () => {
let agent = chai.request.agent(app);
const agent = chai.request.agent(app);
const result = await agent
.get('/');
expect(result).to.have.status(200);
});
});
});

View file

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

View file

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

View file

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

View file

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

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

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 app = server.app;
chai.use(chaiHTTP);
describe('Aliases REST: ', () => {
let agent;
let agent: ChaiHttp.Agent;
before(async () => {
agent = chai.request.agent(app);
});
after(async () => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
const pathToDB = path.resolve(__dirname, '../../', config.get('database'), './pages.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
const pathToAliasDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
const pathToAliasDB = path.resolve(__dirname, '../../', config.get('database'), './aliases.db');
if (fs.existsSync(pathToAliasDB)) {
fs.unlinkSync(pathToAliasDB);

View file

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

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

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