pages.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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. static async createPage(opts) {
  109. await WIKI.models.pages.query().insert({
  110. authorId: opts.authorId,
  111. content: opts.content,
  112. creatorId: opts.authorId,
  113. contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),
  114. description: opts.description,
  115. editorKey: opts.editor,
  116. hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }),
  117. isPrivate: opts.isPrivate,
  118. isPublished: opts.isPublished,
  119. localeCode: opts.locale,
  120. path: opts.path,
  121. publishEndDate: opts.publishEndDate || '',
  122. publishStartDate: opts.publishStartDate || '',
  123. title: opts.title,
  124. toc: '[]'
  125. })
  126. const page = await WIKI.models.pages.getPageFromDb({
  127. path: opts.path,
  128. locale: opts.locale,
  129. userId: opts.authorId,
  130. isPrivate: opts.isPrivate
  131. })
  132. await WIKI.models.pages.renderPage(page)
  133. await WIKI.models.storage.pageEvent({
  134. event: 'created',
  135. page
  136. })
  137. return page
  138. }
  139. static async updatePage(opts) {
  140. const ogPage = await WIKI.models.pages.query().findById(opts.id)
  141. if (!ogPage) {
  142. throw new Error('Invalid Page Id')
  143. }
  144. await WIKI.models.pageHistory.addVersion({
  145. ...ogPage,
  146. action: 'updated'
  147. })
  148. await WIKI.models.pages.query().patch({
  149. authorId: opts.authorId,
  150. content: opts.content,
  151. description: opts.description,
  152. isPublished: opts.isPublished,
  153. publishEndDate: opts.publishEndDate || '',
  154. publishStartDate: opts.publishStartDate || '',
  155. title: opts.title
  156. }).where('id', ogPage.id)
  157. const page = await WIKI.models.pages.getPageFromDb({
  158. path: ogPage.path,
  159. locale: ogPage.localeCode,
  160. userId: ogPage.authorId,
  161. isPrivate: ogPage.isPrivate
  162. })
  163. await WIKI.models.pages.renderPage(page)
  164. await WIKI.models.storage.pageEvent({
  165. event: 'updated',
  166. page
  167. })
  168. return page
  169. }
  170. static async deletePage(opts) {
  171. const page = await WIKI.models.pages.query().findById(opts.id)
  172. if (!page) {
  173. throw new Error('Invalid Page Id')
  174. }
  175. await WIKI.models.pageHistory.addVersion({
  176. ...page,
  177. action: 'deleted'
  178. })
  179. await WIKI.models.pages.query().delete().where('id', page.id)
  180. await WIKI.models.pages.deletePageFromCache(page)
  181. await WIKI.models.storage.pageEvent({
  182. event: 'deleted',
  183. page
  184. })
  185. }
  186. static async renderPage(page) {
  187. const pipeline = await WIKI.models.renderers.getRenderingPipeline(page.contentType)
  188. const renderJob = await WIKI.queue.job.renderPage.add({
  189. page,
  190. pipeline
  191. }, {
  192. removeOnComplete: true,
  193. removeOnFail: true
  194. })
  195. return renderJob.finished()
  196. }
  197. static async getPage(opts) {
  198. let page = await WIKI.models.pages.getPageFromCache(opts)
  199. if (!page) {
  200. page = await WIKI.models.pages.getPageFromDb(opts)
  201. if (page) {
  202. await WIKI.models.pages.savePageToCache(page)
  203. }
  204. }
  205. return page
  206. }
  207. static async getPageFromDb(opts) {
  208. return WIKI.models.pages.query()
  209. .column([
  210. 'pages.*',
  211. {
  212. authorName: 'author.name',
  213. authorEmail: 'author.email',
  214. creatorName: 'creator.name',
  215. creatorEmail: 'creator.email'
  216. }
  217. ])
  218. .joinRelation('author')
  219. .joinRelation('creator')
  220. .where({
  221. 'pages.path': opts.path,
  222. 'pages.localeCode': opts.locale
  223. })
  224. .andWhere(builder => {
  225. builder.where({
  226. 'pages.isPublished': true
  227. }).orWhere({
  228. 'pages.isPublished': false,
  229. 'pages.authorId': opts.userId
  230. })
  231. })
  232. .andWhere(builder => {
  233. if (opts.isPrivate) {
  234. builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })
  235. } else {
  236. builder.where({ 'pages.isPrivate': false })
  237. }
  238. })
  239. .first()
  240. }
  241. static async savePageToCache(page) {
  242. const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`)
  243. await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
  244. id: page.id,
  245. authorId: page.authorId,
  246. authorName: page.authorName,
  247. createdAt: page.createdAt,
  248. creatorId: page.creatorId,
  249. creatorName: page.creatorName,
  250. description: page.description,
  251. isPrivate: page.isPrivate === 1 || page.isPrivate === true,
  252. isPublished: page.isPublished === 1 || page.isPublished === true,
  253. publishEndDate: page.publishEndDate,
  254. publishStartDate: page.publishStartDate,
  255. render: page.render,
  256. title: page.title,
  257. toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
  258. updatedAt: page.updatedAt
  259. }))
  260. }
  261. static async getPageFromCache(opts) {
  262. const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
  263. const cachePath = path.join(process.cwd(), `data/cache/${pageHash}.bin`)
  264. try {
  265. const pageBuffer = await fs.readFile(cachePath)
  266. let page = WIKI.models.pages.cacheSchema.decode(pageBuffer)
  267. return {
  268. ...page,
  269. path: opts.path,
  270. localeCode: opts.locale,
  271. isPrivate: opts.isPrivate
  272. }
  273. } catch (err) {
  274. if (err.code === 'ENOENT') {
  275. return false
  276. }
  277. WIKI.logger.error(err)
  278. throw err
  279. }
  280. }
  281. static async deletePageFromCache(page) {
  282. return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`))
  283. }
  284. }