|
|
const Model = require('objection').Model const _ = require('lodash') const JSBinType = require('js-binary').Type const pageHelper = require('../helpers/page') const path = require('path') const fs = require('fs-extra') const yaml = require('js-yaml') const striptags = require('striptags') const emojiRegex = require('emoji-regex')
/* global WIKI */
const frontmatterRegex = { html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/, legacy: /^(<!-- TITLE: ?([\w\W]+?) -{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) -{2}>)?(?:\n|\r)*([\w\W]*)*/i, markdown: /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?(?:\n|\r)*([\w\W]*)*/ }
const punctuationRegex = /[!,:;/\\_+\-=()&#@<>$~%^*[\]{}"'|]+|(\.\s)|(\s\.)/ig const htmlEntitiesRegex = /(&#[0-9]{3};)|(&#x[a-zA-Z0-9]{2};)/ig
/** * Pages model */ module.exports = class Page extends Model { static get tableName() { return 'pages' }
static get jsonSchema () { return { type: 'object', required: ['path', 'title'],
properties: { id: {type: 'integer'}, path: {type: 'string'}, hash: {type: 'string'}, title: {type: 'string'}, description: {type: 'string'}, isPublished: {type: 'boolean'}, privateNS: {type: 'string'}, publishStartDate: {type: 'string'}, publishEndDate: {type: 'string'}, content: {type: 'string'}, contentType: {type: 'string'},
createdAt: {type: 'string'}, updatedAt: {type: 'string'} } } }
static get relationMappings() { return { tags: { relation: Model.ManyToManyRelation, modelClass: require('./tags'), join: { from: 'pages.id', through: { from: 'pageTags.pageId', to: 'pageTags.tagId' }, to: 'tags.id' } }, author: { relation: Model.BelongsToOneRelation, modelClass: require('./users'), join: { from: 'pages.authorId', to: 'users.id' } }, creator: { relation: Model.BelongsToOneRelation, modelClass: require('./users'), join: { from: 'pages.creatorId', to: 'users.id' } }, editor: { relation: Model.BelongsToOneRelation, modelClass: require('./editors'), join: { from: 'pages.editorKey', to: 'editors.key' } }, locale: { relation: Model.BelongsToOneRelation, modelClass: require('./locales'), join: { from: 'pages.localeCode', to: 'locales.code' } } } }
$beforeUpdate() { this.updatedAt = new Date().toISOString() } $beforeInsert() { this.createdAt = new Date().toISOString() this.updatedAt = new Date().toISOString() }
static get cacheSchema() { return new JSBinType({ id: 'uint', authorId: 'uint', authorName: 'string', createdAt: 'string', creatorId: 'uint', creatorName: 'string', description: 'string', isPrivate: 'boolean', isPublished: 'boolean', publishEndDate: 'string', publishStartDate: 'string', render: 'string', title: 'string', toc: 'string', updatedAt: 'string' }) }
/** * Inject page metadata into contents */ injectMetadata () { return pageHelper.injectPageMetadata(this) }
/** * Parse injected page metadata from raw content * * @param {String} raw Raw file contents * @param {String} contentType Content Type */ static parseMetadata (raw, contentType) { let result switch (contentType) { case 'markdown': result = frontmatterRegex.markdown.exec(raw) if (result[2]) { return { ...yaml.safeLoad(result[2]), content: result[3] } } else { // Attempt legacy v1 format
result = frontmatterRegex.legacy.exec(raw) if (result[2]) { return { title: result[2], description: result[4], content: result[5] } } } break case 'html': result = frontmatterRegex.html.exec(raw) if (result[2]) { return { ...yaml.safeLoad(result[2]), content: result[3] } } break } return { content: raw } }
static async createPage(opts) { await WIKI.models.pages.query().insert({ authorId: opts.authorId, content: opts.content, creatorId: opts.authorId, contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'), description: opts.description, editorKey: opts.editor, hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }), isPrivate: opts.isPrivate, isPublished: opts.isPublished, localeCode: opts.locale, path: opts.path, publishEndDate: opts.publishEndDate || '', publishStartDate: opts.publishStartDate || '', title: opts.title, toc: '[]' }) const page = await WIKI.models.pages.getPageFromDb({ path: opts.path, locale: opts.locale, userId: opts.authorId, isPrivate: opts.isPrivate })
// -> Render page to HTML
await WIKI.models.pages.renderPage(page)
// -> Add to Search Index
const pageContents = await WIKI.models.pages.query().findById(page.id).select('render') page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render) await WIKI.data.searchEngine.created(page)
// -> Add to Storage
if (!opts.skipStorage) { await WIKI.models.storage.pageEvent({ event: 'created', page }) }
return page }
static async updatePage(opts) { const ogPage = await WIKI.models.pages.query().findById(opts.id) if (!ogPage) { throw new Error('Invalid Page Id') } await WIKI.models.pageHistory.addVersion({ ...ogPage, isPublished: ogPage.isPublished === true || ogPage.isPublished === 1, action: 'updated' }) await WIKI.models.pages.query().patch({ authorId: opts.authorId, content: opts.content, description: opts.description, isPublished: opts.isPublished === true || opts.isPublished === 1, publishEndDate: opts.publishEndDate || '', publishStartDate: opts.publishStartDate || '', title: opts.title }).where('id', ogPage.id) const page = await WIKI.models.pages.getPageFromDb({ path: ogPage.path, locale: ogPage.localeCode, userId: ogPage.authorId, isPrivate: ogPage.isPrivate })
// -> Render page to HTML
await WIKI.models.pages.renderPage(page)
// -> Update Search Index
const pageContents = await WIKI.models.pages.query().findById(page.id).select('render') page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render) await WIKI.data.searchEngine.updated(page)
// -> Update on Storage
if (!opts.skipStorage) { await WIKI.models.storage.pageEvent({ event: 'updated', page }) } return page }
static async deletePage(opts) { let page if (_.has(opts, 'id')) { page = await WIKI.models.pages.query().findById(opts.id) } else { page = await await WIKI.models.pages.query().findOne({ path: opts.path, localeCode: opts.locale }) } if (!page) { throw new Error('Invalid Page Id') } await WIKI.models.pageHistory.addVersion({ ...page, action: 'deleted' }) await WIKI.models.pages.query().delete().where('id', page.id) await WIKI.models.pages.deletePageFromCache(page)
// -> Delete from Search Index
await WIKI.data.searchEngine.deleted(page)
// -> Delete from Storage
if (!opts.skipStorage) { await WIKI.models.storage.pageEvent({ event: 'deleted', page }) } }
static async renderPage(page) { const renderJob = await WIKI.scheduler.registerJob({ name: 'render-page', immediate: true, worker: true }, page.id) return renderJob.finished }
static async getPage(opts) { let page = await WIKI.models.pages.getPageFromCache(opts) if (!page) { page = await WIKI.models.pages.getPageFromDb(opts) if (page) { await WIKI.models.pages.savePageToCache(page) } } return page }
static async getPageFromDb(opts) { const queryModeID = _.isNumber(opts) return WIKI.models.pages.query() .column([ 'pages.*', { authorName: 'author.name', authorEmail: 'author.email', creatorName: 'creator.name', creatorEmail: 'creator.email' } ]) .joinRelation('author') .joinRelation('creator') .where(queryModeID ? { 'pages.id': opts } : { 'pages.path': opts.path, 'pages.localeCode': opts.locale }) .andWhere(builder => { if (queryModeID) return builder.where({ 'pages.isPublished': true }).orWhere({ 'pages.isPublished': false, 'pages.authorId': opts.userId }) }) .andWhere(builder => { if (queryModeID) return if (opts.isPrivate) { builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS }) } else { builder.where({ 'pages.isPrivate': false }) } }) .first() }
static async savePageToCache(page) { const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`) await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({ id: page.id, authorId: page.authorId, authorName: page.authorName, createdAt: page.createdAt, creatorId: page.creatorId, creatorName: page.creatorName, description: page.description, isPrivate: page.isPrivate === 1 || page.isPrivate === true, isPublished: page.isPublished === 1 || page.isPublished === true, publishEndDate: page.publishEndDate, publishStartDate: page.publishStartDate, render: page.render, title: page.title, toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc), updatedAt: page.updatedAt })) }
static async getPageFromCache(opts) { const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }) const cachePath = path.join(process.cwd(), `data/cache/${pageHash}.bin`)
try { const pageBuffer = await fs.readFile(cachePath) let page = WIKI.models.pages.cacheSchema.decode(pageBuffer) return { ...page, path: opts.path, localeCode: opts.locale, isPrivate: opts.isPrivate } } catch (err) { if (err.code === 'ENOENT') { return false } WIKI.logger.error(err) throw err } }
static async deletePageFromCache(page) { return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`)) }
static cleanHTML(rawHTML = '') { return striptags(rawHTML || '') .replace(emojiRegex(), '') .replace(htmlEntitiesRegex, '') .replace(punctuationRegex, ' ') .replace(/(\r\n|\n|\r)/gm, ' ') .replace(/\s\s+/g, ' ') .split(' ').filter(w => w.length > 1).join(' ').toLowerCase() } }
|