mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-19 05:09:41 +02:00
Beautiful urls (#18)
* Added uri property to Page model * Added aliases collection * Added routing form aliases * Fixed redirect after page creation * Added abiltity to support few pages with same title * Added ability to change uri manually * Changed hash function * Changed uri parsing * Removed pages controller promise * Modified page's tests * Added tests for alias model * Added tests for aliases * Escaping special characters * Added missed files * Fixed bugs related to translation * Fixed parent page link * Added server validation for uri * Changed css properties order * Made uri property of page be optional * Prevented alias creation from empty uri * Moved alias types to model
This commit is contained in:
parent
ed69336481
commit
d872e78339
28 changed files with 807 additions and 45 deletions
2
public/dist/code-styling.bundle.js
vendored
2
public/dist/code-styling.bundle.js
vendored
File diff suppressed because one or more lines are too long
8
public/dist/editor.bundle.js
vendored
8
public/dist/editor.bundle.js
vendored
File diff suppressed because one or more lines are too long
4
public/dist/main.bundle.js
vendored
4
public/dist/main.bundle.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/main.css
vendored
2
public/dist/main.css
vendored
File diff suppressed because one or more lines are too long
26
src/controllers/aliases.js
Normal file
26
src/controllers/aliases.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
const Alias = require('../models/alias');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class Aliases
|
||||||
|
* @classdesc Aliases controller
|
||||||
|
*/
|
||||||
|
class Aliases {
|
||||||
|
/**
|
||||||
|
* @static
|
||||||
|
* Find and return entity with given alias
|
||||||
|
*
|
||||||
|
* @param {string} aliasName - alias name of entity
|
||||||
|
* @returns {Promise<Alias>}
|
||||||
|
*/
|
||||||
|
static async get(aliasName) {
|
||||||
|
const alias = await Alias.get(aliasName);
|
||||||
|
|
||||||
|
if (!alias.id) {
|
||||||
|
throw new Error('Entity with given alias does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Aliases;
|
|
@ -1,4 +1,5 @@
|
||||||
const Model = require('../models/page');
|
const Model = require('../models/page');
|
||||||
|
const Alias = require('../models/alias');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Pages
|
* @class Pages
|
||||||
|
@ -85,7 +86,18 @@ class Pages {
|
||||||
|
|
||||||
const page = new Model(data);
|
const page = new Model(data);
|
||||||
|
|
||||||
return page.save();
|
const insertedPage = await page.save();
|
||||||
|
|
||||||
|
if (insertedPage.uri) {
|
||||||
|
const alias = new Alias({
|
||||||
|
id: insertedPage._id,
|
||||||
|
type: Alias.types.PAGE
|
||||||
|
}, insertedPage.uri);
|
||||||
|
|
||||||
|
alias.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertedPage;
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
throw new Error(validationError);
|
throw new Error(validationError);
|
||||||
}
|
}
|
||||||
|
@ -132,13 +144,33 @@ class Pages {
|
||||||
*/
|
*/
|
||||||
static async update(id, data) {
|
static async update(id, data) {
|
||||||
const page = await Model.get(id);
|
const page = await Model.get(id);
|
||||||
|
const previousUri = page.uri;
|
||||||
|
|
||||||
if (!page._id) {
|
if (!page._id) {
|
||||||
throw new Error('Page with given id does not exist');
|
throw new Error('Page with given id does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.uri && !data.uri.match(/^[a-z0-9'-]+$/i)) {
|
||||||
|
throw new Error('Uri has unexpected characters');
|
||||||
|
}
|
||||||
|
|
||||||
page.data = data;
|
page.data = data;
|
||||||
return page.save();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,6 +29,7 @@ export default class Writing {
|
||||||
saveButton: null,
|
saveButton: null,
|
||||||
parentIdSelector: null,
|
parentIdSelector: null,
|
||||||
putAboveIdSelector: null,
|
putAboveIdSelector: null,
|
||||||
|
uriInput: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ export default class Writing {
|
||||||
});
|
});
|
||||||
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
|
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
|
||||||
this.nodes.putAboveIdSelector = moduleEl.querySelector('[name="above"]');
|
this.nodes.putAboveIdSelector = moduleEl.querySelector('[name="above"]');
|
||||||
|
this.nodes.uriInput = moduleEl.querySelector('[name="uri-input"]');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,6 +88,15 @@ export default class Writing {
|
||||||
const editorData = await this.editor.save();
|
const editorData = await this.editor.save();
|
||||||
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
|
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
|
||||||
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
|
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
|
||||||
|
let uri = '';
|
||||||
|
|
||||||
|
if (this.nodes.uriInput && this.nodes.uriInput.value) {
|
||||||
|
if (this.nodes.uriInput.value.match(/^[a-z0-9'-]+$/i)) {
|
||||||
|
uri = this.nodes.uriInput.value;
|
||||||
|
} else {
|
||||||
|
throw new Error('Uri has unexpected characters');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
throw new Error('Entry should start with Header');
|
throw new Error('Entry should start with Header');
|
||||||
|
@ -100,6 +111,7 @@ export default class Writing {
|
||||||
return {
|
return {
|
||||||
parent: this.nodes.parentIdSelector.value,
|
parent: this.nodes.parentIdSelector.value,
|
||||||
putAbovePageId: putAbovePageId,
|
putAbovePageId: putAbovePageId,
|
||||||
|
uri: uri,
|
||||||
body: editorData
|
body: editorData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -124,7 +136,7 @@ export default class Writing {
|
||||||
response = await response.json();
|
response = await response.json();
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
document.location = '/page/' + response.result._id;
|
window.location.pathname = response.result.uri ? response.result.uri : '/page/' + response.result._id;
|
||||||
} else {
|
} else {
|
||||||
alert(response.error);
|
alert(response.error);
|
||||||
console.log('Validation failed:', response.error);
|
console.log('Validation failed:', response.error);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
.docs-header {
|
.docs-header {
|
||||||
font-size: 15.8px;
|
|
||||||
border-bottom: 1px solid var(--color-line-gray);
|
|
||||||
line-height: 50px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 var(--layout-padding-horisontal);
|
padding: 0 var(--layout-padding-horisontal);
|
||||||
|
border-bottom: 1px solid var(--color-line-gray);
|
||||||
|
font-size: 15.8px;
|
||||||
|
line-height: 50px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__logo {
|
&__logo {
|
||||||
|
|
|
@ -66,8 +66,8 @@
|
||||||
*/
|
*/
|
||||||
.block-code {
|
.block-code {
|
||||||
padding: 20px !important;
|
padding: 20px !important;
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
border: 1px solid var(--color-line-gray);
|
border: 1px solid var(--color-line-gray);
|
||||||
font-family: Menlo,Monaco,Consolas,Courier New,monospace;
|
font-family: Menlo,Monaco,Consolas,Courier New,monospace;
|
||||||
line-height: 1.7em;
|
line-height: 1.7em;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
.writing-header {
|
.writing-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 15px 0;
|
|
||||||
margin-top: calc(-1 * var(--layout-padding-vertical));
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
padding: 15px 0;
|
||||||
|
margin-top: calc(-1 * var(--layout-padding-vertical));
|
||||||
background: #fff;
|
background: #fff;
|
||||||
z-index: 2;
|
|
||||||
box-shadow: 0 3px 10px #fff;
|
box-shadow: 0 3px 10px #fff;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
&__save {
|
&__save {
|
||||||
@apply --button;
|
@apply --button;
|
||||||
|
@ -22,3 +22,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uri-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(201, 201, 204, 0.48);
|
||||||
|
box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
--color-text-second: #7B7E89;
|
--color-text-second: #7B7E89;
|
||||||
--color-line-gray: #E8E8EB;
|
--color-line-gray: #E8E8EB;
|
||||||
--color-link-active: #388AE5;
|
--color-link-active: #388AE5;
|
||||||
|
--color-gray-border: rgba(var(--color-line-gray), 0.48);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site layout sizes
|
* Site layout sizes
|
||||||
|
@ -14,10 +15,10 @@
|
||||||
|
|
||||||
--button {
|
--button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
padding: 9px 15px;
|
||||||
|
border-radius: 3px;
|
||||||
background: var(--color-link-active);
|
background: var(--color-link-active);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 3px;
|
|
||||||
padding: 9px 15px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
129
src/models/alias.js
Normal file
129
src/models/alias.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
const {aliases: aliasesDb} = require('../utils/database/index');
|
||||||
|
const binaryMD5 = require('../utils/crypto');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} AliasData
|
||||||
|
* @property {string} _id - alias id
|
||||||
|
* @property {string} hash - alias binary hash
|
||||||
|
* @property {string} type - entity type
|
||||||
|
* @property {boolean} deprecated - indicate if alias deprecated
|
||||||
|
* @property {string} id - entity id
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class Alias
|
||||||
|
* @property {string} _id - alias id
|
||||||
|
* @property {string} hash - alias binary hash
|
||||||
|
* @property {string} type - entity type
|
||||||
|
* @property {boolean} deprecated - indicate if alias deprecated
|
||||||
|
* @property {string} id - entity title
|
||||||
|
*/
|
||||||
|
class Alias {
|
||||||
|
/**
|
||||||
|
* Return Alias types
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
static get types() {
|
||||||
|
return {
|
||||||
|
PAGE: 'page'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and return alias with given alias
|
||||||
|
* @param {string} aliasName - alias of entity
|
||||||
|
* @returns {Promise<Alias>}
|
||||||
|
*/
|
||||||
|
static async get(aliasName) {
|
||||||
|
const hash = binaryMD5(aliasName);
|
||||||
|
let data = await aliasesDb.findOne({hash: hash, deprecated: false});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
data = await aliasesDb.findOne({hash: hash});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Alias(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* @param {AliasData} data
|
||||||
|
* @param {string} aliasName - alias of entity
|
||||||
|
*/
|
||||||
|
constructor(data = {}, aliasName = '') {
|
||||||
|
if (data === null) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
if (data._id) {
|
||||||
|
this._id = data._id;
|
||||||
|
}
|
||||||
|
if (aliasName) {
|
||||||
|
this.hash = binaryMD5(aliasName);
|
||||||
|
}
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update alias data in the database
|
||||||
|
*
|
||||||
|
* @returns {Promise<Alias>}
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
if (!this._id) {
|
||||||
|
const insertedRow = await aliasesDb.insert(this.data);
|
||||||
|
|
||||||
|
this._id = insertedRow._id;
|
||||||
|
} else {
|
||||||
|
await aliasesDb.update({_id: this._id}, this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set AliasData object fields to internal model fields
|
||||||
|
*
|
||||||
|
* @param {AliasData} aliasData
|
||||||
|
*/
|
||||||
|
set data(aliasData) {
|
||||||
|
const {id, type, hash, deprecated} = aliasData;
|
||||||
|
|
||||||
|
this.id = id || this.id;
|
||||||
|
this.type = type || this.type;
|
||||||
|
this.hash = hash || this.hash;
|
||||||
|
this.deprecated = deprecated || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return AliasData object
|
||||||
|
*
|
||||||
|
* @returns {AliasData}
|
||||||
|
*/
|
||||||
|
get data() {
|
||||||
|
return {
|
||||||
|
_id: this._id,
|
||||||
|
id: this.id,
|
||||||
|
type: this.type,
|
||||||
|
hash: this.hash,
|
||||||
|
deprecated: this.deprecated
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Alias;
|
|
@ -1,9 +1,11 @@
|
||||||
const {pages: db} = require('../utils/database/index');
|
const {pages: pagesDb} = require('../utils/database/index');
|
||||||
|
const translateString = require('../utils/translation');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PageData
|
* @typedef {Object} PageData
|
||||||
* @property {string} _id - page id
|
* @property {string} _id - page id
|
||||||
* @property {string} title - page title
|
* @property {string} title - page title
|
||||||
|
* @property {string} uri - page uri
|
||||||
* @property {*} body - page body
|
* @property {*} body - page body
|
||||||
* @property {string} parent - id of parent page
|
* @property {string} parent - id of parent page
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +16,7 @@ const {pages: db} = require('../utils/database/index');
|
||||||
*
|
*
|
||||||
* @property {string} _id - page id
|
* @property {string} _id - page id
|
||||||
* @property {string} title - page title
|
* @property {string} title - page title
|
||||||
|
* @property {string} uri - page uri
|
||||||
* @property {*} body - page body
|
* @property {*} body - page body
|
||||||
* @property {string} _parent - id of parent page
|
* @property {string} _parent - id of parent page
|
||||||
*/
|
*/
|
||||||
|
@ -24,7 +27,18 @@ class Page {
|
||||||
* @returns {Promise<Page>}
|
* @returns {Promise<Page>}
|
||||||
*/
|
*/
|
||||||
static async get(_id) {
|
static async get(_id) {
|
||||||
const data = await db.findOne({_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);
|
return new Page(data);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +50,7 @@ class Page {
|
||||||
* @returns {Promise<Page[]>}
|
* @returns {Promise<Page[]>}
|
||||||
*/
|
*/
|
||||||
static async getAll(query = {}) {
|
static async getAll(query = {}) {
|
||||||
const docs = await db.find(query);
|
const docs = await pagesDb.find(query);
|
||||||
|
|
||||||
return Promise.all(docs.map(doc => new Page(doc)));
|
return Promise.all(docs.map(doc => new Page(doc)));
|
||||||
}
|
}
|
||||||
|
@ -64,10 +78,11 @@ class Page {
|
||||||
* @param {PageData} pageData
|
* @param {PageData} pageData
|
||||||
*/
|
*/
|
||||||
set data(pageData) {
|
set data(pageData) {
|
||||||
const {body, parent} = pageData;
|
const {body, parent, uri} = pageData;
|
||||||
|
|
||||||
this.body = body || this.body;
|
this.body = body || this.body;
|
||||||
this.title = this.extractTitleFromBody();
|
this.title = this.extractTitleFromBody();
|
||||||
|
this.uri = uri || '';
|
||||||
this._parent = parent || this._parent;
|
this._parent = parent || this._parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +95,7 @@ class Page {
|
||||||
return {
|
return {
|
||||||
_id: this._id,
|
_id: this._id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
|
uri: this.uri,
|
||||||
body: this.body,
|
body: this.body,
|
||||||
parent: this._parent
|
parent: this._parent
|
||||||
};
|
};
|
||||||
|
@ -95,6 +111,21 @@ class Page {
|
||||||
return headerBlock ? headerBlock.data.text : '';
|
return headerBlock ? headerBlock.data.text : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform title for uri
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
transformTitleToUri() {
|
||||||
|
return translateString(this.title
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
|
||||||
|
.replace(/ +/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.join('-'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link given page as parent
|
* Link given page as parent
|
||||||
*
|
*
|
||||||
|
@ -110,7 +141,7 @@ class Page {
|
||||||
* @returns {Promise<Page>}
|
* @returns {Promise<Page>}
|
||||||
*/
|
*/
|
||||||
get parent() {
|
get parent() {
|
||||||
return db.findOne({_id: this._parent})
|
return pagesDb.findOne({_id: this._parent})
|
||||||
.then(data => new Page(data));
|
.then(data => new Page(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +151,7 @@ class Page {
|
||||||
* @returns {Promise<Page[]>}
|
* @returns {Promise<Page[]>}
|
||||||
*/
|
*/
|
||||||
get children() {
|
get children() {
|
||||||
return db.find({parent: this._id})
|
return pagesDb.find({parent: this._id})
|
||||||
.then(data => data.map(page => new Page(page)));
|
.then(data => data.map(page => new Page(page)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,12 +161,14 @@ class Page {
|
||||||
* @returns {Promise<Page>}
|
* @returns {Promise<Page>}
|
||||||
*/
|
*/
|
||||||
async save() {
|
async save() {
|
||||||
|
this.uri = await this.composeUri(this.uri);
|
||||||
|
|
||||||
if (!this._id) {
|
if (!this._id) {
|
||||||
const insertedRow = await db.insert(this.data);
|
const insertedRow = await pagesDb.insert(this.data);
|
||||||
|
|
||||||
this._id = insertedRow._id;
|
this._id = insertedRow._id;
|
||||||
} else {
|
} else {
|
||||||
await db.update({_id: this._id}, this.data);
|
await pagesDb.update({_id: this._id}, this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
@ -147,13 +180,37 @@ class Page {
|
||||||
* @returns {Promise<Page>}
|
* @returns {Promise<Page>}
|
||||||
*/
|
*/
|
||||||
async destroy() {
|
async destroy() {
|
||||||
await db.remove({_id: this._id});
|
await pagesDb.remove({_id: this._id});
|
||||||
|
|
||||||
delete this._id;
|
delete this._id;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and return available uri
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async composeUri(uri) {
|
||||||
|
let pageWithSameUriCount = 0;
|
||||||
|
|
||||||
|
if (!this._id) {
|
||||||
|
uri = this.transformTitleToUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri) {
|
||||||
|
let pageWithSameUri = await Page.getByUri(uri);
|
||||||
|
|
||||||
|
while (pageWithSameUri._id && pageWithSameUri._id !== this._id) {
|
||||||
|
pageWithSameUriCount++;
|
||||||
|
pageWithSameUri = await Page.getByUri(uri + `-${pageWithSameUriCount}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageWithSameUriCount ? uri + `-${pageWithSameUriCount}` : uri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return readable page data
|
* Return readable page data
|
||||||
*
|
*
|
||||||
|
|
35
src/routes/aliases.js
Normal file
35
src/routes/aliases.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const Aliases = require('../controllers/aliases');
|
||||||
|
const Pages = require('../controllers/pages');
|
||||||
|
const Alias = require('../models/alias');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /*
|
||||||
|
*
|
||||||
|
* Return document with given alias
|
||||||
|
*/
|
||||||
|
router.get('*', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const alias = await Aliases.get(req.originalUrl.slice(1)); // Cuts first '/' character
|
||||||
|
|
||||||
|
switch (alias.type) {
|
||||||
|
case Alias.types.PAGE: {
|
||||||
|
let page = await Pages.get(alias.id);
|
||||||
|
|
||||||
|
let pageParent = await page.parent;
|
||||||
|
|
||||||
|
res.render('pages/page', {
|
||||||
|
page, pageParent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -80,7 +80,7 @@ router.post('/page/:id', multer.any(), async (req, res) => {
|
||||||
const {id} = req.params;
|
const {id} = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {title, body, parent, putAbovePageId} = req.body;
|
const {title, body, parent, putAbovePageId, uri} = req.body;
|
||||||
let page = await Pages.get(id);
|
let page = await Pages.get(id);
|
||||||
|
|
||||||
if (page._parent !== parent) {
|
if (page._parent !== parent) {
|
||||||
|
@ -91,7 +91,7 @@ router.post('/page/:id', multer.any(), async (req, res) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
page = await Pages.update(id, {title, body, parent});
|
page = await Pages.update(id, {title, body, parent, uri});
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: page
|
result: page
|
||||||
|
|
|
@ -3,6 +3,7 @@ const router = express.Router();
|
||||||
|
|
||||||
const home = require('./home');
|
const home = require('./home');
|
||||||
const pages = require('./pages');
|
const pages = require('./pages');
|
||||||
|
const aliases = require('./aliases');
|
||||||
const api = require('./api');
|
const api = require('./api');
|
||||||
|
|
||||||
const pagesMiddleware = require('./middlewares/pages');
|
const pagesMiddleware = require('./middlewares/pages');
|
||||||
|
@ -10,5 +11,6 @@ const pagesMiddleware = require('./middlewares/pages');
|
||||||
router.use('/', pagesMiddleware, home);
|
router.use('/', pagesMiddleware, home);
|
||||||
router.use('/', pagesMiddleware, pages);
|
router.use('/', pagesMiddleware, pages);
|
||||||
router.use('/api', api);
|
router.use('/api', api);
|
||||||
|
router.use('/', aliases);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
12
src/utils/crypto.js
Normal file
12
src/utils/crypto.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create binary md5
|
||||||
|
* @param stringToHash - string to hash
|
||||||
|
* @returns {string} - binary hash of argument
|
||||||
|
*/
|
||||||
|
module.exports = function binaryMD5(stringToHash) {
|
||||||
|
return crypto.createHash('md5')
|
||||||
|
.update(stringToHash)
|
||||||
|
.digest('binary');
|
||||||
|
};
|
6
src/utils/database/aliases.js
Normal file
6
src/utils/database/aliases.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const Datastore = require('nedb');
|
||||||
|
const config = require('../../../config');
|
||||||
|
|
||||||
|
const db = new Datastore({filename: `./${config.database}/aliases.db`, autoload: true});
|
||||||
|
|
||||||
|
module.exports = db;
|
|
@ -1,4 +1,5 @@
|
||||||
const pages = require('./pages');
|
const pages = require('./pages');
|
||||||
|
const aliases = require('./aliases');
|
||||||
const pagesOrder = require('./pagesOrder');
|
const pagesOrder = require('./pagesOrder');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -144,5 +145,6 @@ class Database {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
class: Database,
|
class: Database,
|
||||||
pages: new Database(pages),
|
pages: new Database(pages),
|
||||||
|
aliases: new Database(aliases),
|
||||||
pagesOrder: new Database(pagesOrder)
|
pagesOrder: new Database(pagesOrder)
|
||||||
};
|
};
|
||||||
|
|
78
src/utils/translation.js
Normal file
78
src/utils/translation.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
const translationTable = {
|
||||||
|
'а': '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
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function translateString(string) {
|
||||||
|
return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char);
|
||||||
|
};
|
|
@ -1,14 +1,24 @@
|
||||||
<div class="docs-aside">
|
<div class="docs-aside">
|
||||||
{% for firstLevelPage in menu %}
|
{% for firstLevelPage in menu %}
|
||||||
<section class="docs-aside__section">
|
<section class="docs-aside__section">
|
||||||
<a class="docs-aside__section-title" href="/page/{{ firstLevelPage._id }}">
|
<a class="docs-aside__section-title"
|
||||||
|
{% if firstLevelPage.uri %}
|
||||||
|
href="/{{ firstLevelPage.uri }}"
|
||||||
|
{% else %}
|
||||||
|
href="/page/{{ firstLevelPage._id }}"
|
||||||
|
{% endif %}>
|
||||||
{{ firstLevelPage.title }}
|
{{ firstLevelPage.title }}
|
||||||
</a>
|
</a>
|
||||||
{% if firstLevelPage.children is not empty %}
|
{% if firstLevelPage.children is not empty %}
|
||||||
<ul class="docs-aside__section-list">
|
<ul class="docs-aside__section-list">
|
||||||
{% for child in firstLevelPage.children %}
|
{% for child in firstLevelPage.children %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/page/{{ child._id }}">
|
<a
|
||||||
|
{% if child.uri %}
|
||||||
|
href="/{{ child.uri }}"
|
||||||
|
{% else %}
|
||||||
|
href="/page/{{ child._id }}"
|
||||||
|
{% endif %}>
|
||||||
{{ child.title }}
|
{{ child.title }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -11,7 +11,12 @@
|
||||||
</li>
|
</li>
|
||||||
{% for option in config.menu %}
|
{% for option in config.menu %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{option.uri}}">
|
<a
|
||||||
|
{% if child.uri %}
|
||||||
|
href="{{ option.uri }}"
|
||||||
|
{% else %}
|
||||||
|
href="/page/{{ option._id }}"
|
||||||
|
{% endif %}>
|
||||||
{{ option.title }}
|
{{ option.title }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% set currentPageId = page._id %}
|
{% set currentPageId = page._id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<select name="parent">
|
<select name="parent">
|
||||||
<option value="0">Root</option>
|
<option value="0">Root</option>
|
||||||
{% for _page in pagesAvailable %}
|
{% for _page in pagesAvailable %}
|
||||||
{% if _page._id != currentPageId %}
|
{% if _page._id != currentPageId %}
|
||||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
</option>
|
</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if parentsChildrenOrdered is not empty %}
|
{% if parentsChildrenOrdered is not empty %}
|
||||||
|
@ -48,5 +48,10 @@
|
||||||
Save
|
Save
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
{% if page is not empty %}
|
||||||
|
<main>
|
||||||
|
<input type="text" class="uri-input" name="uri-input" placeholder="Uri(Optional)" value="{{ page.uri }}">
|
||||||
|
</main>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
Documentation
|
Documentation
|
||||||
</a>
|
</a>
|
||||||
{% if page._parent %}
|
{% if page._parent %}
|
||||||
<a href="/page/{{ page._parent }}" class="page__header-nav">
|
<a class="page__header-nav"
|
||||||
|
{% if pageParent.uri %}
|
||||||
|
href="/{{ pageParent.uri }}"
|
||||||
|
{% else %}
|
||||||
|
href="/page/{{ pageParent._id }}"
|
||||||
|
{% endif %}>
|
||||||
Parent {{ pageParent.title }}
|
Parent {{ pageParent.title }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
139
test/models/alias.js
Normal file
139
test/models/alias.js
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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');
|
||||||
|
|
||||||
|
describe('Alias model', () => {
|
||||||
|
after(() => {
|
||||||
|
const pathToDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
|
||||||
|
|
||||||
|
if (fs.existsSync(pathToDB)) {
|
||||||
|
fs.unlinkSync(pathToDB);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Working with empty model', async () => {
|
||||||
|
let alias = new Alias();
|
||||||
|
|
||||||
|
expect(alias.data).to.be.a('object');
|
||||||
|
|
||||||
|
let {data} = alias;
|
||||||
|
|
||||||
|
expect(data._id).to.be.undefined;
|
||||||
|
expect(data.hash).to.be.undefined;
|
||||||
|
expect(data.type).to.be.undefined;
|
||||||
|
expect(data.deprecated).to.be.false;
|
||||||
|
expect(data.id).to.be.undefined;
|
||||||
|
|
||||||
|
alias = new Alias();
|
||||||
|
|
||||||
|
data = alias.data;
|
||||||
|
|
||||||
|
expect(data._id).to.be.undefined;
|
||||||
|
expect(data.hash).to.be.undefined;
|
||||||
|
expect(data.type).to.be.undefined;
|
||||||
|
expect(data.deprecated).to.be.false;
|
||||||
|
expect(data.id).to.be.undefined;
|
||||||
|
|
||||||
|
const initialData = {
|
||||||
|
_id: 'alias_id',
|
||||||
|
type: Alias.types.PAGE,
|
||||||
|
id: 'page_id'
|
||||||
|
};
|
||||||
|
const aliasName = 'alias name';
|
||||||
|
|
||||||
|
alias = new Alias(initialData, aliasName);
|
||||||
|
data = alias.data;
|
||||||
|
|
||||||
|
expect(data._id).to.equal(initialData._id);
|
||||||
|
expect(data.hash).to.equal(binaryMD5(aliasName));
|
||||||
|
expect(data.type).to.equal(initialData.type);
|
||||||
|
expect(data.deprecated).to.equal(false);
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
type: Alias.types.PAGE,
|
||||||
|
id: 'page_id',
|
||||||
|
hash: binaryMD5('another test hash'),
|
||||||
|
deprecated: true
|
||||||
|
};
|
||||||
|
|
||||||
|
alias.data = update;
|
||||||
|
|
||||||
|
data = alias.data;
|
||||||
|
|
||||||
|
expect(data._id).to.equal(initialData._id);
|
||||||
|
expect(data.type).to.equal(update.type);
|
||||||
|
expect(data.hash).to.equal(update.hash);
|
||||||
|
expect(data.deprecated).to.equal(update.deprecated);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Static get method', async () => {
|
||||||
|
const initialData = {
|
||||||
|
type: Alias.types.PAGE,
|
||||||
|
id: 'page_id'
|
||||||
|
};
|
||||||
|
const aliasName = 'alias name';
|
||||||
|
|
||||||
|
const alias = new Alias(initialData, aliasName);
|
||||||
|
|
||||||
|
const savedAlias = await alias.save();
|
||||||
|
|
||||||
|
const foundAlias = await Alias.get(aliasName);
|
||||||
|
|
||||||
|
const {data} = foundAlias;
|
||||||
|
|
||||||
|
expect(data._id).to.equal(savedAlias._id);
|
||||||
|
expect(data.hash).to.equal(binaryMD5(aliasName));
|
||||||
|
expect(data.type).to.equal(initialData.type);
|
||||||
|
expect(data.deprecated).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Saving, updating and deleting model in the database', async () => {
|
||||||
|
const initialData = {
|
||||||
|
type: Alias.types.PAGE,
|
||||||
|
id: 'page_id'
|
||||||
|
};
|
||||||
|
const aliasName = 'alias name';
|
||||||
|
|
||||||
|
const alias = new Alias(initialData, aliasName);
|
||||||
|
|
||||||
|
const savedAlias = await alias.save();
|
||||||
|
|
||||||
|
expect(savedAlias._id).not.be.undefined;
|
||||||
|
expect(savedAlias.hash).to.equal(binaryMD5(aliasName));
|
||||||
|
expect(savedAlias.type).to.equal(initialData.type);
|
||||||
|
expect(savedAlias.id).to.equal(initialData.id);
|
||||||
|
expect(savedAlias.deprecated).to.equal(false);
|
||||||
|
|
||||||
|
const insertedAlias = await aliases.findOne({_id: savedAlias._id});
|
||||||
|
|
||||||
|
expect(insertedAlias._id).to.equal(savedAlias._id);
|
||||||
|
expect(insertedAlias.hash).to.equal(savedAlias.hash);
|
||||||
|
expect(insertedAlias.type).to.equal(savedAlias.type);
|
||||||
|
expect(insertedAlias.id).to.equal(savedAlias.id);
|
||||||
|
expect(insertedAlias.deprecated).to.equal(savedAlias.deprecated);
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
type: Alias.types.PAGE,
|
||||||
|
id: 'page_id',
|
||||||
|
hash: binaryMD5('another test hash'),
|
||||||
|
deprecated: true
|
||||||
|
};
|
||||||
|
|
||||||
|
alias.data = updateData;
|
||||||
|
await alias.save();
|
||||||
|
|
||||||
|
expect(alias._id).to.equal(insertedAlias._id);
|
||||||
|
|
||||||
|
const updatedAlias = await aliases.findOne({_id: alias._id});
|
||||||
|
|
||||||
|
expect(updatedAlias._id).to.equal(savedAlias._id);
|
||||||
|
expect(updatedAlias.hash).to.equal(updateData.hash);
|
||||||
|
expect(updatedAlias.type).to.equal(updateData.type);
|
||||||
|
expect(updatedAlias.id).to.equal(updateData.id);
|
||||||
|
expect(updatedAlias.deprecated).to.equal(updateData.deprecated);
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,8 +4,21 @@ const path = require('path');
|
||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
const Page = require('../../src/models/page');
|
const Page = require('../../src/models/page');
|
||||||
const {pages} = require('../../src/utils/database');
|
const {pages} = require('../../src/utils/database');
|
||||||
|
const translateString = require('../../src/utils/translation');
|
||||||
|
|
||||||
describe('Page model', () => {
|
describe('Page model', () => {
|
||||||
|
|
||||||
|
const transformToUri = (string) => {
|
||||||
|
return translateString(string
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
|
||||||
|
.replace(/ +/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.join('-'));
|
||||||
|
};
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
|
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
|
||||||
|
|
||||||
|
@ -23,6 +36,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(data._id).to.be.undefined;
|
expect(data._id).to.be.undefined;
|
||||||
expect(data.title).to.be.empty;
|
expect(data.title).to.be.empty;
|
||||||
|
expect(data.uri).to.be.empty;
|
||||||
expect(data.body).to.be.undefined;
|
expect(data.body).to.be.undefined;
|
||||||
expect(data.parent).to.be.undefined;
|
expect(data.parent).to.be.undefined;
|
||||||
|
|
||||||
|
@ -32,6 +46,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(data._id).to.be.undefined;
|
expect(data._id).to.be.undefined;
|
||||||
expect(data.title).to.be.empty;
|
expect(data.title).to.be.empty;
|
||||||
|
expect(data.uri).to.be.empty;
|
||||||
expect(data.body).to.be.undefined;
|
expect(data.body).to.be.undefined;
|
||||||
expect(data.parent).to.be.undefined;
|
expect(data.parent).to.be.undefined;
|
||||||
|
|
||||||
|
@ -57,11 +72,13 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(data._id).to.equal(initialData._id);
|
expect(data._id).to.equal(initialData._id);
|
||||||
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
|
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
|
||||||
|
expect(data.uri).to.be.empty;
|
||||||
expect(data.body).to.deep.equal(initialData.body);
|
expect(data.body).to.deep.equal(initialData.body);
|
||||||
expect(data.parent).to.be.undefined;
|
expect(data.parent).to.be.undefined;
|
||||||
|
|
||||||
expect(json._id).to.equal(initialData._id);
|
expect(json._id).to.equal(initialData._id);
|
||||||
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
|
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
|
||||||
|
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
|
||||||
expect(json.body).to.deep.equal(initialData.body);
|
expect(json.body).to.deep.equal(initialData.body);
|
||||||
expect(json.parent).to.be.undefined;
|
expect(json.parent).to.be.undefined;
|
||||||
|
|
||||||
|
@ -85,6 +102,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(data._id).to.equal(initialData._id);
|
expect(data._id).to.equal(initialData._id);
|
||||||
expect(data.title).to.equal(update.body.blocks[0].data.text);
|
expect(data.title).to.equal(update.body.blocks[0].data.text);
|
||||||
|
expect(data.uri).to.be.empty;
|
||||||
expect(data.body).to.equal(update.body);
|
expect(data.body).to.equal(update.body);
|
||||||
expect(data.parent).to.be.undefined;
|
expect(data.parent).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
@ -96,7 +114,7 @@ describe('Page model', () => {
|
||||||
{
|
{
|
||||||
type: 'header',
|
type: 'header',
|
||||||
data: {
|
data: {
|
||||||
text: 'Page header'
|
text: 'New page header'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -108,6 +126,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(savedPage._id).not.be.undefined;
|
expect(savedPage._id).not.be.undefined;
|
||||||
expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text);
|
expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text);
|
||||||
|
expect(savedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
|
||||||
expect(savedPage.body).to.equal(initialData.body);
|
expect(savedPage.body).to.equal(initialData.body);
|
||||||
expect(page._id).not.be.undefined;
|
expect(page._id).not.be.undefined;
|
||||||
|
|
||||||
|
@ -115,6 +134,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(insertedPage._id).to.equal(page._id);
|
expect(insertedPage._id).to.equal(page._id);
|
||||||
expect(insertedPage.title).to.equal(page.title);
|
expect(insertedPage.title).to.equal(page.title);
|
||||||
|
expect(insertedPage.uri).to.equal(page.uri);
|
||||||
expect(insertedPage.body).to.deep.equal(page.body);
|
expect(insertedPage.body).to.deep.equal(page.body);
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
@ -127,7 +147,8 @@ describe('Page model', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
uri: 'updated-uri'
|
||||||
};
|
};
|
||||||
|
|
||||||
page.data = updateData;
|
page.data = updateData;
|
||||||
|
@ -139,6 +160,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(updatedPage._id).to.equal(savedPage._id);
|
expect(updatedPage._id).to.equal(savedPage._id);
|
||||||
expect(updatedPage.title).to.equal(updateData.body.blocks[0].data.text);
|
expect(updatedPage.title).to.equal(updateData.body.blocks[0].data.text);
|
||||||
|
expect(updatedPage.uri).to.equal(updateData.uri);
|
||||||
expect(updatedPage.body).to.deep.equal(updateData.body);
|
expect(updatedPage.body).to.deep.equal(updateData.body);
|
||||||
|
|
||||||
await page.destroy();
|
await page.destroy();
|
||||||
|
@ -150,6 +172,39 @@ describe('Page model', () => {
|
||||||
expect(removedPage).to.be.null;
|
expect(removedPage).to.be.null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Handle multiple page creation with the same uri', async () => {
|
||||||
|
const initialData = {
|
||||||
|
body: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: 'header',
|
||||||
|
data: {
|
||||||
|
text: 'New page header'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const firstPage = new Page(initialData);
|
||||||
|
let firstSavedPage = await firstPage.save();
|
||||||
|
const secondPage = new Page(initialData);
|
||||||
|
let secondSavedPage = await secondPage.save();
|
||||||
|
|
||||||
|
expect(secondSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text) + '-1');
|
||||||
|
|
||||||
|
const newUri = 'new-uri';
|
||||||
|
|
||||||
|
firstPage.data = {...firstPage.data, uri: newUri};
|
||||||
|
firstSavedPage = await firstPage.save();
|
||||||
|
|
||||||
|
expect(firstSavedPage.uri).to.equal(newUri);
|
||||||
|
|
||||||
|
const thirdPage = new Page(initialData);
|
||||||
|
let thirdSavedPage = await thirdPage.save();
|
||||||
|
|
||||||
|
expect(thirdSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
|
||||||
|
});
|
||||||
|
|
||||||
it('Static get method', async () => {
|
it('Static get method', async () => {
|
||||||
const initialData = {
|
const initialData = {
|
||||||
body: {
|
body: {
|
||||||
|
@ -173,6 +228,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(data._id).to.equal(savedPage._id);
|
expect(data._id).to.equal(savedPage._id);
|
||||||
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
|
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
|
||||||
|
expect(data.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
|
||||||
expect(data.body).to.deep.equal(initialData.body);
|
expect(data.body).to.deep.equal(initialData.body);
|
||||||
|
|
||||||
await page.destroy();
|
await page.destroy();
|
||||||
|
@ -213,6 +269,7 @@ describe('Page model', () => {
|
||||||
expect(foundPages.length).to.equal(2);
|
expect(foundPages.length).to.equal(2);
|
||||||
foundPages.forEach((page, i) => {
|
foundPages.forEach((page, i) => {
|
||||||
expect(page.title).to.equal(pagesToSave[i].body.blocks[0].data.text);
|
expect(page.title).to.equal(pagesToSave[i].body.blocks[0].data.text);
|
||||||
|
expect(page.uri).to.equal(transformToUri(pagesToSave[i].body.blocks[0].data.text));
|
||||||
expect(page.body).to.deep.equal(pagesToSave[i].body);
|
expect(page.body).to.deep.equal(pagesToSave[i].body);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -257,6 +314,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(testedParent._id).to.equal(parentId);
|
expect(testedParent._id).to.equal(parentId);
|
||||||
expect(testedParent.title).to.equal(parent.body.blocks[0].data.text);
|
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.body).to.deep.equal(parent.body);
|
||||||
|
|
||||||
const children = await parent.children;
|
const children = await parent.children;
|
||||||
|
@ -267,6 +325,7 @@ describe('Page model', () => {
|
||||||
|
|
||||||
expect(testedChild._id).to.equal(childId);
|
expect(testedChild._id).to.equal(childId);
|
||||||
expect(testedChild.title).to.equal(child.body.blocks[0].data.text);
|
expect(testedChild.title).to.equal(child.body.blocks[0].data.text);
|
||||||
|
expect(testedChild.uri).to.equal(transformToUri(child.body.blocks[0].data.text));
|
||||||
expect(testedChild.body).to.deep.equal(child.body);
|
expect(testedChild.body).to.deep.equal(child.body);
|
||||||
expect(testedChild._parent).to.equal(child._parent);
|
expect(testedChild._parent).to.equal(child._parent);
|
||||||
expect(testedChild._parent).to.equal(parent._id);
|
expect(testedChild._parent).to.equal(parent._id);
|
||||||
|
|
58
test/rest/aliases.js
Normal file
58
test/rest/aliases.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
const {app} = require('../../bin/www');
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('../../config');
|
||||||
|
const chai = require('chai');
|
||||||
|
const chaiHTTP = require('chai-http');
|
||||||
|
const {expect} = chai;
|
||||||
|
|
||||||
|
chai.use(chaiHTTP);
|
||||||
|
|
||||||
|
describe('Aliases REST: ', () => {
|
||||||
|
let agent;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
agent = chai.request.agent(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
|
||||||
|
|
||||||
|
if (fs.existsSync(pathToDB)) {
|
||||||
|
fs.unlinkSync(pathToDB);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathToAliasDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
|
||||||
|
|
||||||
|
if (fs.existsSync(pathToAliasDB)) {
|
||||||
|
fs.unlinkSync(pathToAliasDB);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Finding page with alias', async () => {
|
||||||
|
const body = {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: 'header',
|
||||||
|
data: {
|
||||||
|
text: 'Test header'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const put = await agent
|
||||||
|
.put('/api/page')
|
||||||
|
.send({body});
|
||||||
|
|
||||||
|
expect(put).to.have.status(200);
|
||||||
|
expect(put).to.be.json;
|
||||||
|
|
||||||
|
const {result: {uri}} = put.body;
|
||||||
|
|
||||||
|
const get = await agent.get(`/${uri}`);
|
||||||
|
|
||||||
|
expect(get).to.have.status(200);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
const {app} = require('../../bin/www');
|
const {app} = require('../../bin/www');
|
||||||
const model = require('../../src/models/page');
|
const model = require('../../src/models/page');
|
||||||
const PageOrder = require('../../src/models/pageOrder');
|
const PageOrder = require('../../src/models/pageOrder');
|
||||||
|
const translateString = require('../../src/utils/translation');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
@ -13,6 +14,16 @@ chai.use(chaiHTTP);
|
||||||
|
|
||||||
describe('Pages REST: ', () => {
|
describe('Pages REST: ', () => {
|
||||||
let agent;
|
let agent;
|
||||||
|
const transformToUri = (string) => {
|
||||||
|
return translateString(string
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
|
||||||
|
.replace(/ +/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.join('-'));
|
||||||
|
};
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
agent = chai.request.agent(app);
|
agent = chai.request.agent(app);
|
||||||
|
@ -35,9 +46,9 @@ describe('Pages REST: ', () => {
|
||||||
text: 'Page header'
|
text: 'Page header'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
const parent = 0;
|
const parent = 0;
|
||||||
const res = await agent
|
const res = await agent
|
||||||
.put('/api/page')
|
.put('/api/page')
|
||||||
.send({body, parent});
|
.send({body, parent});
|
||||||
|
@ -50,6 +61,7 @@ describe('Pages REST: ', () => {
|
||||||
expect(success).to.be.true;
|
expect(success).to.be.true;
|
||||||
expect(result._id).to.be.a('string');
|
expect(result._id).to.be.a('string');
|
||||||
expect(result.title).to.equal(body.blocks[0].data.text);
|
expect(result.title).to.equal(body.blocks[0].data.text);
|
||||||
|
expect(result.uri).to.equal(transformToUri(body.blocks[0].data.text));
|
||||||
expect(result.body).to.deep.equal(body);
|
expect(result.body).to.deep.equal(body);
|
||||||
|
|
||||||
const createdPage = await model.get(result._id);
|
const createdPage = await model.get(result._id);
|
||||||
|
@ -57,13 +69,14 @@ describe('Pages REST: ', () => {
|
||||||
expect(createdPage).not.be.null;
|
expect(createdPage).not.be.null;
|
||||||
expect(createdPage._id).to.equal(result._id);
|
expect(createdPage._id).to.equal(result._id);
|
||||||
expect(createdPage.title).to.equal(body.blocks[0].data.text);
|
expect(createdPage.title).to.equal(body.blocks[0].data.text);
|
||||||
|
expect(createdPage.uri).to.equal(transformToUri(body.blocks[0].data.text));
|
||||||
expect(createdPage.body).to.deep.equal(body);
|
expect(createdPage.body).to.deep.equal(body);
|
||||||
|
|
||||||
const pageOrder = await PageOrder.get('' + (createdPage.data.parent || 0));
|
const pageOrder = await PageOrder.get('' + (createdPage.data.parent || 0));
|
||||||
expect(pageOrder.order).to.be.an('array');
|
expect(pageOrder.order).to.be.an('array');
|
||||||
|
|
||||||
await createdPage.destroy();
|
await createdPage.destroy();
|
||||||
await pageOrder.destroy()
|
await pageOrder.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Page data validation on create', async () => {
|
it('Page data validation on create', async () => {
|
||||||
|
@ -115,6 +128,7 @@ describe('Pages REST: ', () => {
|
||||||
|
|
||||||
expect(foundPage._id).to.equal(_id);
|
expect(foundPage._id).to.equal(_id);
|
||||||
expect(foundPage.title).to.equal(body.blocks[0].data.text);
|
expect(foundPage.title).to.equal(body.blocks[0].data.text);
|
||||||
|
expect(foundPage.uri).to.equal(transformToUri(body.blocks[0].data.text));
|
||||||
expect(foundPage.body).to.deep.equal(body);
|
expect(foundPage.body).to.deep.equal(body);
|
||||||
|
|
||||||
await pageOrder.destroy();
|
await pageOrder.destroy();
|
||||||
|
@ -164,10 +178,11 @@ describe('Pages REST: ', () => {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
const updatedUri = 'updated-uri';
|
||||||
|
|
||||||
res = await agent
|
res = await agent
|
||||||
.post(`/api/page/${_id}`)
|
.post(`/api/page/${_id}`)
|
||||||
.send({body: updatedBody});
|
.send({body: updatedBody, uri: updatedUri});
|
||||||
|
|
||||||
expect(res).to.have.status(200);
|
expect(res).to.have.status(200);
|
||||||
expect(res).to.be.json;
|
expect(res).to.be.json;
|
||||||
|
@ -178,6 +193,8 @@ describe('Pages REST: ', () => {
|
||||||
expect(result._id).to.equal(_id);
|
expect(result._id).to.equal(_id);
|
||||||
expect(result.title).not.equal(body.blocks[0].data.text);
|
expect(result.title).not.equal(body.blocks[0].data.text);
|
||||||
expect(result.title).to.equal(updatedBody.blocks[0].data.text);
|
expect(result.title).to.equal(updatedBody.blocks[0].data.text);
|
||||||
|
expect(result.uri).not.equal(transformToUri(body.blocks[0].data.text));
|
||||||
|
expect(result.uri).to.equal(updatedUri);
|
||||||
expect(result.body).not.equal(body);
|
expect(result.body).not.equal(body);
|
||||||
expect(result.body).to.deep.equal(updatedBody);
|
expect(result.body).to.deep.equal(updatedBody);
|
||||||
|
|
||||||
|
@ -187,6 +204,8 @@ describe('Pages REST: ', () => {
|
||||||
expect(updatedPage._id).to.equal(_id);
|
expect(updatedPage._id).to.equal(_id);
|
||||||
expect(updatedPage.title).not.equal(body.blocks[0].data.text);
|
expect(updatedPage.title).not.equal(body.blocks[0].data.text);
|
||||||
expect(updatedPage.title).to.equal(updatedBody.blocks[0].data.text);
|
expect(updatedPage.title).to.equal(updatedBody.blocks[0].data.text);
|
||||||
|
expect(updatedPage.uri).not.equal(transformToUri(body.blocks[0].data.text));
|
||||||
|
expect(updatedPage.uri).to.equal(updatedUri);
|
||||||
expect(updatedPage.body).not.equal(body);
|
expect(updatedPage.body).not.equal(body);
|
||||||
expect(updatedPage.body).to.deep.equal(updatedBody);
|
expect(updatedPage.body).to.deep.equal(updatedBody);
|
||||||
|
|
||||||
|
@ -194,6 +213,65 @@ describe('Pages REST: ', () => {
|
||||||
await updatedPage.destroy();
|
await updatedPage.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Handle multiple page creation with the same uri', async () => {
|
||||||
|
const body = {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: 'header',
|
||||||
|
data: {
|
||||||
|
text: 'Page header'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = await agent
|
||||||
|
.put('/api/page')
|
||||||
|
.send({body});
|
||||||
|
|
||||||
|
expect(res).to.have.status(200);
|
||||||
|
expect(res).to.be.json;
|
||||||
|
|
||||||
|
const {result: {_id}} = res.body;
|
||||||
|
|
||||||
|
res = await agent
|
||||||
|
.put('/api/page')
|
||||||
|
.send({body: body});
|
||||||
|
|
||||||
|
expect(res).to.have.status(200);
|
||||||
|
expect(res).to.be.json;
|
||||||
|
|
||||||
|
const {success: secondPageSuccess, result: secondPageResult} = res.body;
|
||||||
|
|
||||||
|
expect(secondPageSuccess).to.be.true;
|
||||||
|
expect(secondPageResult.title).to.equal(body.blocks[0].data.text);
|
||||||
|
expect(secondPageResult.uri).to.equal(transformToUri(body.blocks[0].data.text) + '-1');
|
||||||
|
expect(secondPageResult.body).to.deep.equal(body);
|
||||||
|
|
||||||
|
const newFirstPageUri = 'New-uri';
|
||||||
|
|
||||||
|
res = await agent
|
||||||
|
.post(`/api/page/${_id}`)
|
||||||
|
.send({body: body, uri: newFirstPageUri});
|
||||||
|
|
||||||
|
expect(res).to.have.status(200);
|
||||||
|
expect(res).to.be.json;
|
||||||
|
|
||||||
|
res = await agent
|
||||||
|
.put('/api/page')
|
||||||
|
.send({body: body});
|
||||||
|
|
||||||
|
expect(res).to.have.status(200);
|
||||||
|
expect(res).to.be.json;
|
||||||
|
|
||||||
|
const {success: thirdPageSuccess, result: thirdPageResult} = res.body;
|
||||||
|
|
||||||
|
expect(thirdPageSuccess).to.be.true;
|
||||||
|
expect(thirdPageResult.title).to.equal(body.blocks[0].data.text);
|
||||||
|
expect(thirdPageResult.uri).to.equal(transformToUri(body.blocks[0].data.text));
|
||||||
|
expect(thirdPageResult.body).to.deep.equal(body);
|
||||||
|
});
|
||||||
|
|
||||||
it('Updating page with not existing id', async () => {
|
it('Updating page with not existing id', async () => {
|
||||||
const res = await agent
|
const res = await agent
|
||||||
.post('/api/page/not-existing-id')
|
.post('/api/page/not-existing-id')
|
||||||
|
@ -223,7 +301,7 @@ describe('Pages REST: ', () => {
|
||||||
{
|
{
|
||||||
type: 'header',
|
type: 'header',
|
||||||
data: {
|
data: {
|
||||||
text: 'Page header'
|
text: 'Page header to be deleted'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -249,6 +327,7 @@ describe('Pages REST: ', () => {
|
||||||
expect(success).to.be.true;
|
expect(success).to.be.true;
|
||||||
expect(result._id).to.be.undefined;
|
expect(result._id).to.be.undefined;
|
||||||
expect(result.title).to.equal(body.blocks[0].data.text);
|
expect(result.title).to.equal(body.blocks[0].data.text);
|
||||||
|
expect(result.uri).to.equal(transformToUri(body.blocks[0].data.text));
|
||||||
expect(result.body).to.deep.equal(body);
|
expect(result.body).to.deep.equal(body);
|
||||||
|
|
||||||
const deletedPage = await model.get(_id);
|
const deletedPage = await model.get(_id);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue