post.js 6.5 KB

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