You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

323 lines
8.7 KiB

6 years ago
6 years ago
  1. const Model = require('objection').Model
  2. const _ = require('lodash')
  3. const JSBinType = require('js-binary').Type
  4. const pageHelper = require('../helpers/page')
  5. const path = require('path')
  6. const fs = require('fs-extra')
  7. /* global WIKI */
  8. /**
  9. * Pages model
  10. */
  11. module.exports = class Page extends Model {
  12. static get tableName() { return 'pages' }
  13. static get jsonSchema () {
  14. return {
  15. type: 'object',
  16. required: ['path', 'title'],
  17. properties: {
  18. id: {type: 'integer'},
  19. path: {type: 'string'},
  20. hash: {type: 'string'},
  21. title: {type: 'string'},
  22. description: {type: 'string'},
  23. isPublished: {type: 'boolean'},
  24. privateNS: {type: 'string'},
  25. publishStartDate: {type: 'string'},
  26. publishEndDate: {type: 'string'},
  27. content: {type: 'string'},
  28. contentType: {type: 'string'},
  29. createdAt: {type: 'string'},
  30. updatedAt: {type: 'string'}
  31. }
  32. }
  33. }
  34. static get relationMappings() {
  35. return {
  36. tags: {
  37. relation: Model.ManyToManyRelation,
  38. modelClass: require('./tags'),
  39. join: {
  40. from: 'pages.id',
  41. through: {
  42. from: 'pageTags.pageId',
  43. to: 'pageTags.tagId'
  44. },
  45. to: 'tags.id'
  46. }
  47. },
  48. author: {
  49. relation: Model.BelongsToOneRelation,
  50. modelClass: require('./users'),
  51. join: {
  52. from: 'pages.authorId',
  53. to: 'users.id'
  54. }
  55. },
  56. creator: {
  57. relation: Model.BelongsToOneRelation,
  58. modelClass: require('./users'),
  59. join: {
  60. from: 'pages.creatorId',
  61. to: 'users.id'
  62. }
  63. },
  64. editor: {
  65. relation: Model.BelongsToOneRelation,
  66. modelClass: require('./editors'),
  67. join: {
  68. from: 'pages.editorKey',
  69. to: 'editors.key'
  70. }
  71. },
  72. locale: {
  73. relation: Model.BelongsToOneRelation,
  74. modelClass: require('./locales'),
  75. join: {
  76. from: 'pages.localeCode',
  77. to: 'locales.code'
  78. }
  79. }
  80. }
  81. }
  82. $beforeUpdate() {
  83. this.updatedAt = new Date().toISOString()
  84. }
  85. $beforeInsert() {
  86. this.createdAt = new Date().toISOString()
  87. this.updatedAt = new Date().toISOString()
  88. }
  89. static get cacheSchema() {
  90. return new JSBinType({
  91. id: 'uint',
  92. authorId: 'uint',
  93. authorName: 'string',
  94. createdAt: 'string',
  95. creatorId: 'uint',
  96. creatorName: 'string',
  97. description: 'string',
  98. isPrivate: 'boolean',
  99. isPublished: 'boolean',
  100. publishEndDate: 'string',
  101. publishStartDate: 'string',
  102. render: 'string',
  103. title: 'string',
  104. toc: 'string',
  105. updatedAt: 'string'
  106. })
  107. }
  108. /**
  109. * Inject page metadata into contents
  110. */
  111. injectMetadata () {
  112. let meta = [
  113. ['title', this.title],
  114. ['description', this.description],
  115. ['published', this.isPublished.toString()],
  116. ['date', this.updatedAt],
  117. ['tags', '']
  118. ]
  119. switch (this.contentType) {
  120. case 'markdown':
  121. return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + this.content
  122. case 'html':
  123. return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + this.content
  124. default:
  125. return this.content
  126. }
  127. }
  128. static async createPage(opts) {
  129. await WIKI.models.pages.query().insert({
  130. authorId: opts.authorId,
  131. content: opts.content,
  132. creatorId: opts.authorId,
  133. contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),
  134. description: opts.description,
  135. editorKey: opts.editor,
  136. hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }),
  137. isPrivate: opts.isPrivate,
  138. isPublished: opts.isPublished,
  139. localeCode: opts.locale,
  140. path: opts.path,
  141. publishEndDate: opts.publishEndDate || '',
  142. publishStartDate: opts.publishStartDate || '',
  143. title: opts.title,
  144. toc: '[]'
  145. })
  146. const page = await WIKI.models.pages.getPageFromDb({
  147. path: opts.path,
  148. locale: opts.locale,
  149. userId: opts.authorId,
  150. isPrivate: opts.isPrivate
  151. })
  152. await WIKI.models.pages.renderPage(page)
  153. await WIKI.models.storage.pageEvent({
  154. event: 'created',
  155. page
  156. })
  157. return page
  158. }
  159. static async updatePage(opts) {
  160. const ogPage = await WIKI.models.pages.query().findById(opts.id)
  161. if (!ogPage) {
  162. throw new Error('Invalid Page Id')
  163. }
  164. await WIKI.models.pageHistory.addVersion({
  165. ...ogPage,
  166. action: 'updated'
  167. })
  168. await WIKI.models.pages.query().patch({
  169. authorId: opts.authorId,
  170. content: opts.content,
  171. description: opts.description,
  172. isPublished: opts.isPublished,
  173. publishEndDate: opts.publishEndDate || '',
  174. publishStartDate: opts.publishStartDate || '',
  175. title: opts.title
  176. }).where('id', ogPage.id)
  177. const page = await WIKI.models.pages.getPageFromDb({
  178. path: ogPage.path,
  179. locale: ogPage.localeCode,
  180. userId: ogPage.authorId,
  181. isPrivate: ogPage.isPrivate
  182. })
  183. await WIKI.models.pages.renderPage(page)
  184. await WIKI.models.storage.pageEvent({
  185. event: 'updated',
  186. page
  187. })
  188. return page
  189. }
  190. static async deletePage(opts) {
  191. const page = await WIKI.models.pages.query().findById(opts.id)
  192. if (!page) {
  193. throw new Error('Invalid Page Id')
  194. }
  195. await WIKI.models.pageHistory.addVersion({
  196. ...page,
  197. action: 'deleted'
  198. })
  199. await WIKI.models.pages.query().delete().where('id', page.id)
  200. await WIKI.models.pages.deletePageFromCache(page)
  201. await WIKI.models.storage.pageEvent({
  202. event: 'deleted',
  203. page
  204. })
  205. }
  206. static async renderPage(page) {
  207. const pipeline = await WIKI.models.renderers.getRenderingPipeline(page.contentType)
  208. const renderJob = await WIKI.queue.job.renderPage.add({
  209. page,
  210. pipeline
  211. }, {
  212. removeOnComplete: true,
  213. removeOnFail: true
  214. })
  215. return renderJob.finished()
  216. }
  217. static async getPage(opts) {
  218. let page = await WIKI.models.pages.getPageFromCache(opts)
  219. if (!page) {
  220. page = await WIKI.models.pages.getPageFromDb(opts)
  221. if (page) {
  222. await WIKI.models.pages.savePageToCache(page)
  223. }
  224. }
  225. return page
  226. }
  227. static async getPageFromDb(opts) {
  228. return WIKI.models.pages.query()
  229. .column([
  230. 'pages.*',
  231. {
  232. authorName: 'author.name',
  233. authorEmail: 'author.email',
  234. creatorName: 'creator.name',
  235. creatorEmail: 'creator.email'
  236. }
  237. ])
  238. .joinRelation('author')
  239. .joinRelation('creator')
  240. .where({
  241. 'pages.path': opts.path,
  242. 'pages.localeCode': opts.locale
  243. })
  244. .andWhere(builder => {
  245. builder.where({
  246. 'pages.isPublished': true
  247. }).orWhere({
  248. 'pages.isPublished': false,
  249. 'pages.authorId': opts.userId
  250. })
  251. })
  252. .andWhere(builder => {
  253. if (opts.isPrivate) {
  254. builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })
  255. } else {
  256. builder.where({ 'pages.isPrivate': false })
  257. }
  258. })
  259. .first()
  260. }
  261. static async savePageToCache(page) {
  262. const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`)
  263. await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
  264. id: page.id,
  265. authorId: page.authorId,
  266. authorName: page.authorName,
  267. createdAt: page.createdAt,
  268. creatorId: page.creatorId,
  269. creatorName: page.creatorName,
  270. description: page.description,
  271. isPrivate: page.isPrivate === 1 || page.isPrivate === true,
  272. isPublished: page.isPublished === 1 || page.isPublished === true,
  273. publishEndDate: page.publishEndDate,
  274. publishStartDate: page.publishStartDate,
  275. render: page.render,
  276. title: page.title,
  277. toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
  278. updatedAt: page.updatedAt
  279. }))
  280. }
  281. static async getPageFromCache(opts) {
  282. const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
  283. const cachePath = path.join(process.cwd(), `data/cache/${pageHash}.bin`)
  284. try {
  285. const pageBuffer = await fs.readFile(cachePath)
  286. let page = WIKI.models.pages.cacheSchema.decode(pageBuffer)
  287. return {
  288. ...page,
  289. path: opts.path,
  290. localeCode: opts.locale,
  291. isPrivate: opts.isPrivate
  292. }
  293. } catch (err) {
  294. if (err.code === 'ENOENT') {
  295. return false
  296. }
  297. WIKI.logger.error(err)
  298. throw err
  299. }
  300. }
  301. static async deletePageFromCache(page) {
  302. return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`))
  303. }
  304. }