mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-31 02:59:43 +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:
parent
059cfb96f9
commit
34514761f5
99 changed files with 3817 additions and 2249 deletions
18
src/backend/utils/asyncMiddleware.ts
Normal file
18
src/backend/utils/asyncMiddleware.ts
Normal 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);
|
||||
};
|
||||
}
|
45
src/backend/utils/crypto.ts
Normal file
45
src/backend/utils/crypto.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
*
|
||||
* @param stringToHash - string to hash
|
||||
* @returns {string} - binary hash of argument
|
||||
*/
|
||||
export function binaryMD5(stringToHash: string): string {
|
||||
return hexToBinary(crypto.createHash('md5')
|
||||
.update(stringToHash)
|
||||
.digest('hex'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 16 random bytes in hex format
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export function random16(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.randomBytes(16, (err, raw) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve(raw.toString('hex'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
binaryMD5,
|
||||
random16,
|
||||
};
|
177
src/backend/utils/database/index.ts
Normal file
177
src/backend/utils/database/index.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
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 {Datastore} db - nedb Datastore object
|
||||
*/
|
||||
export class Database<DocType> {
|
||||
private db: Datastore;
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {Object} nedbInstance - nedb Datastore object
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public async insert(doc: DocType): Promise<DocType> {
|
||||
return new Promise((resolve, reject) => this.db.insert(doc, (err, newDoc) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
resolve(newDoc);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
resolve(docs);
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (projection) {
|
||||
this.db.find(query, projection, cbk(resolve, reject));
|
||||
} else {
|
||||
this.db.find(query, cbk(resolve, reject));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
resolve(doc);
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (projection) {
|
||||
this.db.findOne(query, projection, cbk(resolve, reject));
|
||||
} else {
|
||||
this.db.findOne(query, cbk(resolve, reject));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document matches query
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#updating-documents
|
||||
*
|
||||
* @param {Object} query - query object
|
||||
* @param {Object} update - fields to update
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number|Object|Object[]|Error>} - number of updated rows or affected docs or Error object
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case options.returnUpdatedDocs:
|
||||
resolve(affectedDocs);
|
||||
break;
|
||||
case options.upsert:
|
||||
if (affectedDocs) {
|
||||
resolve(affectedDocs);
|
||||
}
|
||||
resolve(result);
|
||||
break;
|
||||
default:
|
||||
resolve(result);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove document matches passed query
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#removing-documents
|
||||
*
|
||||
* @param {Object} query - query object
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number|Error>} - number of removed rows or Error object
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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')),
|
||||
};
|
16
src/backend/utils/database/initDb.ts
Normal file
16
src/backend/utils/database/initDb.ts
Normal 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,
|
||||
});
|
||||
}
|
38
src/backend/utils/objects.ts
Normal file
38
src/backend/utils/objects.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Merge to objects recursively
|
||||
*
|
||||
* @param {object} target
|
||||
* @param {object[]} sources
|
||||
* @returns {object}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
const source = sources.shift();
|
||||
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
if (isObject(source[key])) {
|
||||
if (!target[key]) {
|
||||
Object.assign(target, { [key]: {} });
|
||||
}
|
||||
|
||||
deepMerge(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(target, { [key]: source[key] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deepMerge(target, ...sources);
|
||||
}
|
||||
|
||||
export default deepMerge;
|
124
src/backend/utils/rcparser.ts
Normal file
124
src/backend/utils/rcparser.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
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 {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
|
||||
*/
|
||||
export default class RCParser {
|
||||
/**
|
||||
* Default CodeX Docs configuration
|
||||
*
|
||||
* @static
|
||||
* @returns {{title: string, menu: Array}}
|
||||
*/
|
||||
public static get DEFAULTS():RCData {
|
||||
return {
|
||||
title: 'CodeX Docs',
|
||||
menu: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and parse runtime configuration file
|
||||
*
|
||||
* @static
|
||||
* @returns {{title: string, menu: []}}
|
||||
*/
|
||||
public static getConfiguration(): RCData {
|
||||
if (!fs.existsSync(rcPath)) {
|
||||
return RCParser.DEFAULTS;
|
||||
}
|
||||
|
||||
const file = fs.readFileSync(rcPath, 'utf-8');
|
||||
const rConfig = RCParser.DEFAULTS;
|
||||
let userConfig;
|
||||
|
||||
try {
|
||||
userConfig = JSON.parse(file);
|
||||
} catch (e) {
|
||||
console.log('CodeX Docs rc file should be in JSON format.');
|
||||
|
||||
return RCParser.DEFAULTS;
|
||||
}
|
||||
|
||||
for (const option in userConfig) {
|
||||
if (Object.prototype.hasOwnProperty.call(userConfig, option)) {
|
||||
rConfig[option] = userConfig[option] || RCParser.DEFAULTS[option] || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(rConfig.menu instanceof Array)) {
|
||||
console.log('Menu section in the rc file must be an array.');
|
||||
rConfig.menu = RCParser.DEFAULTS.menu;
|
||||
}
|
||||
|
||||
rConfig.menu = rConfig.menu.filter((option: string | Menu, i:number) => {
|
||||
i = i + 1;
|
||||
if (typeof option === 'string') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!option || option instanceof Array || typeof option !== 'object') {
|
||||
console.log(`Menu option #${i} in rc file must be a string or an object`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const { title, uri } = option;
|
||||
|
||||
if (!title || typeof title !== 'string') {
|
||||
console.log(`Menu option #${i} title must be a string.`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uri || typeof uri !== 'string') {
|
||||
console.log(`Menu option #${i} uri must be a string.`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
rConfig.menu = rConfig.menu.map((option: string | Menu) => {
|
||||
if (typeof option === 'string') {
|
||||
return {
|
||||
title: option,
|
||||
/* Replace all non alpha- and numeric-symbols with '-' */
|
||||
uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-'),
|
||||
};
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
return rConfig;
|
||||
}
|
||||
}
|
85
src/backend/utils/translation.ts
Normal file
85
src/backend/utils/translation.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
interface TransTable {
|
||||
[key: string]: string;
|
||||
}
|
||||
const translationTable: TransTable = {
|
||||
а: 'a',
|
||||
б: 'b',
|
||||
в: 'v',
|
||||
г: 'g',
|
||||
д: 'd',
|
||||
е: 'e',
|
||||
ж: 'g',
|
||||
з: 'z',
|
||||
и: 'i',
|
||||
й: 'y',
|
||||
к: 'k',
|
||||
л: 'l',
|
||||
м: 'm',
|
||||
н: 'n',
|
||||
о: 'o',
|
||||
п: 'p',
|
||||
р: 'r',
|
||||
с: 's',
|
||||
т: 't',
|
||||
у: 'u',
|
||||
ф: 'f',
|
||||
ы: 'i',
|
||||
э: 'e',
|
||||
А: 'A',
|
||||
Б: 'B',
|
||||
В: 'V',
|
||||
Г: 'G',
|
||||
Д: 'D',
|
||||
Е: 'E',
|
||||
Ж: 'G',
|
||||
З: 'Z',
|
||||
И: 'I',
|
||||
Й: 'Y',
|
||||
К: 'K',
|
||||
Л: 'L',
|
||||
М: 'M',
|
||||
Н: 'N',
|
||||
О: 'O',
|
||||
П: 'P',
|
||||
Р: 'R',
|
||||
С: 'S',
|
||||
Т: 'T',
|
||||
У: 'U',
|
||||
Ф: 'F',
|
||||
Ы: 'I',
|
||||
Э: 'E',
|
||||
ё: 'yo',
|
||||
х: 'h',
|
||||
ц: 'ts',
|
||||
ч: 'ch',
|
||||
ш: 'sh',
|
||||
щ: 'shch',
|
||||
ъ: "''",
|
||||
ь: "'",
|
||||
ю: 'yu',
|
||||
я: 'ya',
|
||||
Ё: 'YO',
|
||||
Х: 'H',
|
||||
Ц: 'TS',
|
||||
Ч: 'CH',
|
||||
Ш: 'SH',
|
||||
Щ: 'SHCH',
|
||||
Ъ: "''",
|
||||
Ь: "'",
|
||||
Ю: 'YU',
|
||||
Я: 'YA',
|
||||
};
|
||||
/**
|
||||
* Function to translate string
|
||||
*
|
||||
* @param string - string to translate
|
||||
* @returns {string} - translated 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);
|
||||
}
|
48
src/backend/utils/twig.ts
Normal file
48
src/backend/utils/twig.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Twig extensions
|
||||
*/
|
||||
import twig from 'twig';
|
||||
import fs from 'fs';
|
||||
import urlify from './urlify';
|
||||
|
||||
export default (function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Function for include svg on page
|
||||
*
|
||||
* @example svg('path/from/root/dir')
|
||||
* @param {string} filename - name of icon
|
||||
* @returns {string} - svg code
|
||||
*/
|
||||
twig.extendFunction('svg', function (filename: string) {
|
||||
return fs.readFileSync(`./src/frontend/svg/${filename}.svg`, 'utf-8');
|
||||
});
|
||||
|
||||
/**
|
||||
* Convert text to URL-like string
|
||||
* Example: "What is <mark>clean data</mark>" -> "what-is-clean-data"
|
||||
*
|
||||
* @param {string} string - source string with HTML
|
||||
* @returns {string} alias-like string
|
||||
*/
|
||||
twig.extendFilter('urlify', function (string: string) {
|
||||
return urlify(string);
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse link as URL object
|
||||
*
|
||||
* @param {string} linkUrl - link to be processed
|
||||
* @returns {string} url — url data
|
||||
*/
|
||||
twig.extendFunction('parseLink', function (linkUrl: string): string {
|
||||
try {
|
||||
return new URL(linkUrl).toString();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
return '';
|
||||
}
|
||||
});
|
||||
}());
|
33
src/backend/utils/urlify.ts
Normal file
33
src/backend/utils/urlify.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import translateString from './translation';
|
||||
|
||||
/**
|
||||
* Convert text to URL-like string
|
||||
* Example: "What is <mark>clean data</mark>" -> "what-is-clean-data"
|
||||
*
|
||||
* @param {string} string - source string with HTML
|
||||
* @returns {string} alias-like string
|
||||
*/
|
||||
export default function urlify(string: string): string {
|
||||
// strip tags
|
||||
string = string.replace(/(<([^>]+)>)/ig, '');
|
||||
|
||||
// remove nbsp
|
||||
string = string.replace(/ /g, ' ');
|
||||
|
||||
// remove all symbols except chars
|
||||
string = string.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ');
|
||||
|
||||
// remove whitespaces
|
||||
string = string.replace(/ +/g, ' ').trim();
|
||||
|
||||
// lowercase
|
||||
string = string.toLowerCase();
|
||||
|
||||
// join words with hyphens
|
||||
string = string.split(' ').join('-');
|
||||
|
||||
// translate
|
||||
string = translateString(string);
|
||||
|
||||
return string;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue