diff --git a/package.json b/package.json index c7f7e9e..d0a643c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@babel/polyfill": "^7.0.0", + "blueimp-md5": "^2.10.0", "body-parser": "latest", "codex.editor": "^2.1.3", "codex.editor.header": "^2.0.5", diff --git a/src/controllers/pages.js b/src/controllers/pages.js index d0c97fc..e9e2263 100644 --- a/src/controllers/pages.js +++ b/src/controllers/pages.js @@ -1,7 +1,7 @@ const Model = require('../models/page'); const Alias = require('../models/alias'); const aliasTypes = require('../constants/aliasTypes'); -const getHashFromString = require('../utils/hash'); +const md5 = require('blueimp-md5'); /** * @class Pages @@ -15,7 +15,7 @@ class Pages { * @returns {['title', 'body']} */ static get REQUIRED_FIELDS() { - return [ 'body' ]; + return ['body']; } /** @@ -53,21 +53,19 @@ class Pages { static async insert(data) { try { Pages.validate(data); - console.log('data', data); const page = new Model(data); const pagePromise = page.save(); + const updatedPage = await pagePromise; - pagePromise.then(() => { - const alias = new Alias({ - id: page._id, - type: aliasTypes.PAGE, - hash: getHashFromString(page.uri).toString() - }); - - alias.save(); + const alias = new Alias({ + id: updatedPage._id, + type: aliasTypes.PAGE, + hash: md5(updatedPage.uri) }); + alias.save(); + return pagePromise; } catch (validationError) { throw new Error(validationError); @@ -119,10 +117,22 @@ class Pages { if (!page._id) { throw new Error('Page with given id does not exist'); } + const oldAlias = page.uri; page.data = data; + const pagePromise = page.save(); + const updatedPage = await pagePromise; - return page.save(); + Alias.markAsDeprecated(oldAlias); + + const alias = new Alias({ + id: updatedPage._id, + type: aliasTypes.PAGE, + hash: md5(updatedPage.uri) + }); + + alias.save(); + return pagePromise; } /** diff --git a/src/models/alias.js b/src/models/alias.js index 619d69e..9bfc34e 100644 --- a/src/models/alias.js +++ b/src/models/alias.js @@ -1,11 +1,11 @@ const {aliases: aliasesDb} = require('../utils/database/index'); -const getHashFromString = require('../utils/hash'); - +const md5 = require('blueimp-md5'); /** * @typedef {Object} AliasData * @property {string} _id - alias id * @property {string} hash - alias hash * @property {string} type - entity type + * @property {boolean} deprecated - indicate if alias deprecated * @property {string} id - entity id * */ @@ -15,6 +15,7 @@ const getHashFromString = require('../utils/hash'); * @property {string} _id - alias id * @property {string} hash - alias hash * @property {string} type - entity type + * @property {boolean} deprecated - indicate if alias deprecated * @property {string} id - entity title */ class Alias { @@ -24,7 +25,7 @@ class Alias { * @returns {Promise} */ static async get(aliasName) { - const hash = getHashFromString(aliasName).toString(); + const hash = md5(aliasName); const data = await aliasesDb.findOne({hash}); return new Alias(data); @@ -42,25 +43,65 @@ class Alias { if (data._id) { this._id = data._id; } - this.id = data.id; - this.type = data.type; - this.hash = data.hash; + this.data = data; } /** - * Save or update page data in the database + * Save or update alias data in the database * * @returns {Promise} */ async save() { if (!this._id) { - const insertedRow = await aliasesDb.insert({id: this.id, type: this.type, hash: this.hash}); + const insertedRow = await aliasesDb.insert(this.data); } 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} + */ + static async markAsDeprecated(aliasName) { + const alias = await Alias.get(aliasName); + + alias.deprecated = true; + + return alias.save(); + } } module.exports = Alias; diff --git a/src/models/page.js b/src/models/page.js index 2cbc1a3..4430af7 100644 --- a/src/models/page.js +++ b/src/models/page.js @@ -1,5 +1,6 @@ const {pages: pagesDb} = require('../utils/database/index'); const {aliases: aliasesDb} = require('../utils/database/index'); +const translateString = require('../utils/translation'); /** * @typedef {Object} PageData @@ -33,6 +34,17 @@ class Page { return new Page(data); } + /** + * Find and return model of page with given uri + * @param {string} uri - page uri + * @returns {Promise} + */ + static async getByUri(uri) { + const data = await pagesDb.findOne({uri}); + + return new Page(data); + } + /** * Find all pages which match passed query object * @@ -72,7 +84,7 @@ class Page { this.body = body || this.body; this.title = this.extractTitleFromBody(); - this.uri = uri || this.title.toLowerCase().split(' ').join('-'); + this.uri = uri || translateString(this.title.toLowerCase()).split(' ').join('-'); this._parent = parent || this._parent; } @@ -136,15 +148,29 @@ class Page { * @returns {Promise} */ async save() { + let newUri = translateString(this.title.toLowerCase()).split(' ').join('-'); + let pageWithSameUri = await Page.getByUri(newUri); + let pageWithSameUriCount = 0; + + while (pageWithSameUri._id) { + pageWithSameUriCount++; + pageWithSameUri = await Page.getByUri(newUri + `-${pageWithSameUriCount}`); + } + + this.uri = pageWithSameUriCount ? newUri + `-${pageWithSameUriCount}` : newUri; + if (!this._id) { const insertedRow = await pagesDb.insert({ ...this.data, - uri: this.title.toLowerCase().split(' ').join('-') + uri: this.uri }); this._id = insertedRow._id; } else { - await pagesDb.update({_id: this._id}, this.data); + await pagesDb.update({_id: this._id}, { + ...this.data, + uri: this.uri + }); } return this; diff --git a/src/utils/hash.js b/src/utils/hash.js deleted file mode 100644 index fbbea55..0000000 --- a/src/utils/hash.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Function for getting hash from stringToHash - * - * @param stringToHash - stringToHash to encode - * @returns {string} - hash index - */ -module.exports = function getHashFromString(stringToHash) { - return stringToHash.split('').reduce((previousHashValue, currentChar) => - (((previousHashValue << 5) - previousHashValue) + currentChar.charCodeAt(0)) | 0, 0); -}; diff --git a/src/utils/translation.js b/src/utils/translation.js new file mode 100644 index 0000000..66a5413 --- /dev/null +++ b/src/utils/translation.js @@ -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); +}; diff --git a/src/views/components/aside.twig b/src/views/components/aside.twig index 6f4d69d..2c096f0 100644 --- a/src/views/components/aside.twig +++ b/src/views/components/aside.twig @@ -8,7 +8,7 @@
    {% for child in firstLevelPage.children %}
  • - + {{ child.title }}
  • diff --git a/yarn.lock b/yarn.lock index de40920..526f9fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1162,6 +1162,11 @@ bluebird@^3.5.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== +blueimp-md5@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.10.0.tgz#02f0843921f90dca14f5b8920a38593201d6964d" + integrity sha512-EkNUOi7tpV68TqjpiUz9D9NcT8um2+qtgntmMbi5UKssVX2m/2PLqotcric0RE63pB3HPN/fjf3cKHN2ufGSUQ== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"