post.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. 'use strict';
  2. const { Schema } = require('warehouse');
  3. const moment = require('moment');
  4. const { extname, join, sep } = require('path');
  5. const Promise = require('bluebird');
  6. const Moment = require('./types/moment');
  7. const { full_url_for } = require('hexo-util');
  8. function pickID(data) {
  9. return data._id;
  10. }
  11. function removeEmptyTag(tags) {
  12. return tags.filter(tag => tag != null && tag !== '').map(tag => `${tag}`);
  13. }
  14. module.exports = ctx => {
  15. const Post = new Schema({
  16. id: String,
  17. title: {type: String, default: ''},
  18. date: {
  19. type: Moment,
  20. default: moment,
  21. language: ctx.config.languages,
  22. timezone: ctx.config.timezone
  23. },
  24. updated: {
  25. type: Moment,
  26. language: ctx.config.languages,
  27. timezone: ctx.config.timezone
  28. },
  29. comments: {type: Boolean, default: true},
  30. layout: {type: String, default: 'post'},
  31. _content: {type: String, default: ''},
  32. source: {type: String, required: true},
  33. slug: {type: String, required: true},
  34. photos: [String],
  35. link: {type: String, default: ''},
  36. raw: {type: String, default: ''},
  37. published: {type: Boolean, default: true},
  38. content: {type: String},
  39. excerpt: {type: String},
  40. more: {type: String}
  41. });
  42. Post.virtual('path').get(function() {
  43. const path = ctx.execFilterSync('post_permalink', this, {context: ctx});
  44. return typeof path === 'string' ? path : '';
  45. });
  46. Post.virtual('permalink').get(function() {
  47. return full_url_for.call(ctx, this.path);
  48. });
  49. Post.virtual('full_source').get(function() {
  50. return join(ctx.source_dir, this.source || '');
  51. });
  52. Post.virtual('asset_dir').get(function() {
  53. const src = this.full_source;
  54. return src.substring(0, src.length - extname(src).length) + sep;
  55. });
  56. Post.virtual('tags').get(function() {
  57. const PostTag = ctx.model('PostTag');
  58. const Tag = ctx.model('Tag');
  59. const ids = PostTag.find({post_id: this._id}, {lean: true}).map(item => item.tag_id);
  60. return Tag.find({_id: {$in: ids}});
  61. });
  62. Post.method('setTags', function(tags) {
  63. tags = removeEmptyTag(tags);
  64. const PostTag = ctx.model('PostTag');
  65. const Tag = ctx.model('Tag');
  66. const id = this._id;
  67. const existed = PostTag.find({post_id: id}, {lean: true}).map(pickID);
  68. return Promise.map(tags, tag => {
  69. // Find the tag by name
  70. const data = Tag.findOne({name: tag}, {lean: true});
  71. if (data) return data;
  72. // Insert the tag if not exist
  73. return Tag.insert({name: tag}).catch(err => {
  74. // Try to find the tag again. Throw the error if not found
  75. const data = Tag.findOne({name: tag}, {lean: true});
  76. if (data) return data;
  77. throw err;
  78. });
  79. }).map(tag => {
  80. // Find the reference
  81. const ref = PostTag.findOne({post_id: id, tag_id: tag._id}, {lean: true});
  82. if (ref) return ref;
  83. // Insert the reference if not exist
  84. return PostTag.insert({
  85. post_id: id,
  86. tag_id: tag._id
  87. });
  88. }).then(tags => {
  89. // Remove old tags
  90. const deleted = existed.filter(item => !tags.map(pickID).includes(item));
  91. return deleted;
  92. }).map(tag => PostTag.removeById(tag));
  93. });
  94. Post.virtual('categories').get(function() {
  95. const PostCategory = ctx.model('PostCategory');
  96. const Category = ctx.model('Category');
  97. const ids = PostCategory.find({post_id: this._id}, {lean: true}).map(item => item.category_id);
  98. return Category.find({_id: {$in: ids}});
  99. });
  100. Post.method('setCategories', function(cats) {
  101. // Remove empty categories, preserving hierarchies
  102. cats = cats.filter(cat => {
  103. return Array.isArray(cat) || (cat != null && cat !== '');
  104. }).map(cat => {
  105. return Array.isArray(cat) ? removeEmptyTag(cat) : `${cat}`;
  106. });
  107. const PostCategory = ctx.model('PostCategory');
  108. const Category = ctx.model('Category');
  109. const id = this._id;
  110. const allIds = [];
  111. const existed = PostCategory.find({post_id: id}, {lean: true}).map(pickID);
  112. const hasHierarchy = cats.filter(Array.isArray).length > 0;
  113. // Add a hierarchy of categories
  114. const addHierarchy = catHierarchy => {
  115. const parentIds = [];
  116. if (!Array.isArray(catHierarchy)) catHierarchy = [catHierarchy];
  117. // Don't use "Promise.map". It doesn't run in series.
  118. // MUST USE "Promise.each".
  119. return Promise.each(catHierarchy, (cat, i) => {
  120. // Find the category by name
  121. const data = Category.findOne({
  122. name: cat,
  123. parent: i ? parentIds[i - 1] : {$exists: false}
  124. }, {lean: true});
  125. if (data) {
  126. allIds.push(data._id);
  127. parentIds.push(data._id);
  128. return data;
  129. }
  130. // Insert the category if not exist
  131. const obj = {name: cat};
  132. if (i) obj.parent = parentIds[i - 1];
  133. return Category.insert(obj).catch(err => {
  134. // Try to find the category again. Throw the error if not found
  135. const data = Category.findOne({
  136. name: cat,
  137. parent: i ? parentIds[i - 1] : {$exists: false}
  138. }, {lean: true});
  139. if (data) return data;
  140. throw err;
  141. }).then(data => {
  142. allIds.push(data._id);
  143. parentIds.push(data._id);
  144. return data;
  145. });
  146. });
  147. };
  148. return (hasHierarchy ? Promise.each(cats, addHierarchy) : Promise.resolve(addHierarchy(cats))
  149. ).then(() => allIds).map(catId => {
  150. // Find the reference
  151. const ref = PostCategory.findOne({post_id: id, category_id: catId}, {lean: true});
  152. if (ref) return ref;
  153. // Insert the reference if not exist
  154. return PostCategory.insert({
  155. post_id: id,
  156. category_id: catId
  157. });
  158. }).then(postCats => // Remove old categories
  159. existed.filter(item => !postCats.map(pickID).includes(item))).map(cat => PostCategory.removeById(cat));
  160. });
  161. // Remove PostTag references
  162. Post.pre('remove', data => {
  163. const PostTag = ctx.model('PostTag');
  164. return PostTag.remove({post_id: data._id});
  165. });
  166. // Remove PostCategory references
  167. Post.pre('remove', data => {
  168. const PostCategory = ctx.model('PostCategory');
  169. return PostCategory.remove({post_id: data._id});
  170. });
  171. // Remove assets
  172. Post.pre('remove', data => {
  173. const PostAsset = ctx.model('PostAsset');
  174. return PostAsset.remove({post: data._id});
  175. });
  176. return Post;
  177. };