1
0
Fork 0
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:
DorofeevMark 2019-01-25 02:23:00 +03:00 committed by Peter Savchenko
parent ed69336481
commit d872e78339
28 changed files with 807 additions and 45 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

View file

@ -1,4 +1,5 @@
const Model = require('../models/page');
const Alias = require('../models/alias');
/**
* @class Pages
@ -85,7 +86,18 @@ class Pages {
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) {
throw new Error(validationError);
}
@ -132,13 +144,33 @@ class Pages {
*/
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;
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;
}
/**

View file

@ -29,6 +29,7 @@ export default class Writing {
saveButton: null,
parentIdSelector: null,
putAboveIdSelector: null,
uriInput: null
};
}
@ -63,6 +64,7 @@ export default class Writing {
});
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
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 firstBlock = editorData.blocks.length ? editorData.blocks[0] : 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) {
throw new Error('Entry should start with Header');
@ -100,6 +111,7 @@ export default class Writing {
return {
parent: this.nodes.parentIdSelector.value,
putAbovePageId: putAbovePageId,
uri: uri,
body: editorData
};
}
@ -124,7 +136,7 @@ export default class Writing {
response = await response.json();
if (response.success) {
document.location = '/page/' + response.result._id;
window.location.pathname = response.result.uri ? response.result.uri : '/page/' + response.result._id;
} else {
alert(response.error);
console.log('Validation failed:', response.error);

View file

@ -1,13 +1,13 @@
.docs-header {
font-size: 15.8px;
border-bottom: 1px solid var(--color-line-gray);
line-height: 50px;
display: flex;
padding: 0 var(--layout-padding-horisontal);
border-bottom: 1px solid var(--color-line-gray);
font-size: 15.8px;
line-height: 50px;
a {
text-decoration: none;
display: inline-block;
text-decoration: none;
}
&__logo {

View file

@ -66,8 +66,8 @@
*/
.block-code {
padding: 20px !important;
font-size: 13px;
border-radius: 3px;
font-size: 13px;
border: 1px solid var(--color-line-gray);
font-family: Menlo,Monaco,Consolas,Courier New,monospace;
line-height: 1.7em;

View file

@ -1,12 +1,12 @@
.writing-header {
display: flex;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
position: sticky;
top: 0;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
background: #fff;
z-index: 2;
box-shadow: 0 3px 10px #fff;
z-index: 2;
&__save {
@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;
}

View file

@ -3,6 +3,7 @@
--color-text-second: #7B7E89;
--color-line-gray: #E8E8EB;
--color-link-active: #388AE5;
--color-gray-border: rgba(var(--color-line-gray), 0.48);
/**
* Site layout sizes
@ -14,10 +15,10 @@
--button {
display: inline-block;
padding: 9px 15px;
border-radius: 3px;
background: var(--color-link-active);
color: #fff;
border-radius: 3px;
padding: 9px 15px;
font-size: 14px;
line-height: 1em;
text-decoration: none;

129
src/models/alias.js Normal file
View 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;

View file

@ -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
* @property {string} _id - page id
* @property {string} title - page title
* @property {string} uri - page uri
* @property {*} body - page body
* @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} title - page title
* @property {string} uri - page uri
* @property {*} body - page body
* @property {string} _parent - id of parent page
*/
@ -24,7 +27,18 @@ class Page {
* @returns {Promise<Page>}
*/
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);
}
@ -36,7 +50,7 @@ class Page {
* @returns {Promise<Page[]>}
*/
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)));
}
@ -64,10 +78,11 @@ class Page {
* @param {PageData} pageData
*/
set data(pageData) {
const {body, parent} = pageData;
const {body, parent, uri} = pageData;
this.body = body || this.body;
this.title = this.extractTitleFromBody();
this.uri = uri || '';
this._parent = parent || this._parent;
}
@ -80,6 +95,7 @@ class Page {
return {
_id: this._id,
title: this.title,
uri: this.uri,
body: this.body,
parent: this._parent
};
@ -95,6 +111,21 @@ class Page {
return headerBlock ? headerBlock.data.text : '';
}
/**
* Transform title for uri
* @return {string}
*/
transformTitleToUri() {
return translateString(this.title
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
}
/**
* Link given page as parent
*
@ -110,7 +141,7 @@ class Page {
* @returns {Promise<Page>}
*/
get parent() {
return db.findOne({_id: this._parent})
return pagesDb.findOne({_id: this._parent})
.then(data => new Page(data));
}
@ -120,7 +151,7 @@ class Page {
* @returns {Promise<Page[]>}
*/
get children() {
return db.find({parent: this._id})
return pagesDb.find({parent: this._id})
.then(data => data.map(page => new Page(page)));
}
@ -130,12 +161,14 @@ class Page {
* @returns {Promise<Page>}
*/
async save() {
this.uri = await this.composeUri(this.uri);
if (!this._id) {
const insertedRow = await db.insert(this.data);
const insertedRow = await pagesDb.insert(this.data);
this._id = insertedRow._id;
} else {
await db.update({_id: this._id}, this.data);
await pagesDb.update({_id: this._id}, this.data);
}
return this;
@ -147,13 +180,37 @@ class Page {
* @returns {Promise<Page>}
*/
async destroy() {
await db.remove({_id: this._id});
await pagesDb.remove({_id: this._id});
delete this._id;
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
*

35
src/routes/aliases.js Normal file
View 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;

View file

@ -80,7 +80,7 @@ router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params;
try {
const {title, body, parent, putAbovePageId} = req.body;
const {title, body, parent, putAbovePageId, uri} = req.body;
let page = await Pages.get(id);
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({
success: true,
result: page

View file

@ -3,6 +3,7 @@ const router = express.Router();
const home = require('./home');
const pages = require('./pages');
const aliases = require('./aliases');
const api = require('./api');
const pagesMiddleware = require('./middlewares/pages');
@ -10,5 +11,6 @@ const pagesMiddleware = require('./middlewares/pages');
router.use('/', pagesMiddleware, home);
router.use('/', pagesMiddleware, pages);
router.use('/api', api);
router.use('/', aliases);
module.exports = router;

12
src/utils/crypto.js Normal file
View 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');
};

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

View file

@ -1,4 +1,5 @@
const pages = require('./pages');
const aliases = require('./aliases');
const pagesOrder = require('./pagesOrder');
/**
@ -144,5 +145,6 @@ class Database {
module.exports = {
class: Database,
pages: new Database(pages),
aliases: new Database(aliases),
pagesOrder: new Database(pagesOrder)
};

78
src/utils/translation.js Normal file
View 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);
};

View file

@ -1,14 +1,24 @@
<div class="docs-aside">
{% for firstLevelPage in menu %}
<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 }}
</a>
{% if firstLevelPage.children is not empty %}
<ul class="docs-aside__section-list">
{% for child in firstLevelPage.children %}
<li>
<a href="/page/{{ child._id }}">
<a
{% if child.uri %}
href="/{{ child.uri }}"
{% else %}
href="/page/{{ child._id }}"
{% endif %}>
{{ child.title }}
</a>
</li>

View file

@ -11,7 +11,12 @@
</li>
{% for option in config.menu %}
<li>
<a href="{{option.uri}}">
<a
{% if child.uri %}
href="{{ option.uri }}"
{% else %}
href="/page/{{ option._id }}"
{% endif %}>
{{ option.title }}
</a>
</li>

View file

@ -48,5 +48,10 @@
Save
</span>
</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>
{% endblock %}

View file

@ -7,7 +7,12 @@
Documentation
</a>
{% 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 }}
</a>
{% endif %}

139
test/models/alias.js Normal file
View 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);
});
});

View file

@ -4,8 +4,21 @@ 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');
describe('Page model', () => {
const transformToUri = (string) => {
return translateString(string
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
};
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
@ -23,6 +36,7 @@ describe('Page model', () => {
expect(data._id).to.be.undefined;
expect(data.title).to.be.empty;
expect(data.uri).to.be.empty;
expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined;
@ -32,6 +46,7 @@ describe('Page model', () => {
expect(data._id).to.be.undefined;
expect(data.title).to.be.empty;
expect(data.uri).to.be.empty;
expect(data.body).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.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.parent).to.be.undefined;
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.body).to.deep.equal(initialData.body);
expect(json.parent).to.be.undefined;
@ -85,6 +102,7 @@ describe('Page model', () => {
expect(data._id).to.equal(initialData._id);
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.parent).to.be.undefined;
});
@ -96,7 +114,7 @@ describe('Page model', () => {
{
type: 'header',
data: {
text: 'Page header'
text: 'New page header'
}
}
]
@ -108,6 +126,7 @@ describe('Page model', () => {
expect(savedPage._id).not.be.undefined;
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(page._id).not.be.undefined;
@ -115,6 +134,7 @@ describe('Page model', () => {
expect(insertedPage._id).to.equal(page._id);
expect(insertedPage.title).to.equal(page.title);
expect(insertedPage.uri).to.equal(page.uri);
expect(insertedPage.body).to.deep.equal(page.body);
const updateData = {
@ -127,7 +147,8 @@ describe('Page model', () => {
}
}
]
}
},
uri: 'updated-uri'
};
page.data = updateData;
@ -139,6 +160,7 @@ describe('Page model', () => {
expect(updatedPage._id).to.equal(savedPage._id);
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);
await page.destroy();
@ -150,6 +172,39 @@ describe('Page model', () => {
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 () => {
const initialData = {
body: {
@ -173,6 +228,7 @@ describe('Page model', () => {
expect(data._id).to.equal(savedPage._id);
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);
await page.destroy();
@ -213,6 +269,7 @@ describe('Page model', () => {
expect(foundPages.length).to.equal(2);
foundPages.forEach((page, i) => {
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);
});
});
@ -257,6 +314,7 @@ describe('Page model', () => {
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;
@ -267,6 +325,7 @@ describe('Page model', () => {
expect(testedChild._id).to.equal(childId);
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._parent).to.equal(child._parent);
expect(testedChild._parent).to.equal(parent._id);

58
test/rest/aliases.js Normal file
View 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);
});
});

View file

@ -1,6 +1,7 @@
const {app} = require('../../bin/www');
const model = require('../../src/models/page');
const PageOrder = require('../../src/models/pageOrder');
const translateString = require('../../src/utils/translation');
const fs = require('fs');
const path = require('path');
@ -13,6 +14,16 @@ chai.use(chaiHTTP);
describe('Pages REST: ', () => {
let agent;
const transformToUri = (string) => {
return translateString(string
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
};
before(async () => {
agent = chai.request.agent(app);
@ -35,7 +46,7 @@ describe('Pages REST: ', () => {
text: 'Page header'
}
}
],
]
};
const parent = 0;
const res = await agent
@ -50,6 +61,7 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
expect(result._id).to.be.a('string');
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);
const createdPage = await model.get(result._id);
@ -57,13 +69,14 @@ describe('Pages REST: ', () => {
expect(createdPage).not.be.null;
expect(createdPage._id).to.equal(result._id);
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);
const pageOrder = await PageOrder.get('' + (createdPage.data.parent || 0));
expect(pageOrder.order).to.be.an('array');
await createdPage.destroy();
await pageOrder.destroy()
await pageOrder.destroy();
});
it('Page data validation on create', async () => {
@ -115,6 +128,7 @@ describe('Pages REST: ', () => {
expect(foundPage._id).to.equal(_id);
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);
await pageOrder.destroy();
@ -164,10 +178,11 @@ describe('Pages REST: ', () => {
}
]
};
const updatedUri = 'updated-uri';
res = await agent
.post(`/api/page/${_id}`)
.send({body: updatedBody});
.send({body: updatedBody, uri: updatedUri});
expect(res).to.have.status(200);
expect(res).to.be.json;
@ -178,6 +193,8 @@ describe('Pages REST: ', () => {
expect(result._id).to.equal(_id);
expect(result.title).not.equal(body.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).to.deep.equal(updatedBody);
@ -187,6 +204,8 @@ describe('Pages REST: ', () => {
expect(updatedPage._id).to.equal(_id);
expect(updatedPage.title).not.equal(body.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).to.deep.equal(updatedBody);
@ -194,6 +213,65 @@ describe('Pages REST: ', () => {
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 () => {
const res = await agent
.post('/api/page/not-existing-id')
@ -223,7 +301,7 @@ describe('Pages REST: ', () => {
{
type: 'header',
data: {
text: 'Page header'
text: 'Page header to be deleted'
}
}
]
@ -249,6 +327,7 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
expect(result._id).to.be.undefined;
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);
const deletedPage = await model.get(_id);