site.coffee 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # app/design/site.coffee
  2. class SiteDesignDocuments
  3. constructor: ->
  4. @designDocs = {
  5. products: @getProductsDesignDoc()
  6. categories: @getCategoriesDesignDoc()
  7. orders: @getOrdersDesignDoc()
  8. validation: @getValidationDesignDoc()
  9. }
  10. getProductsDesignDoc: ->
  11. {
  12. _id: '_design/products'
  13. views:
  14. by_category:
  15. map: """
  16. function(doc) {
  17. if (doc.type === 'product' && doc.active !== false) {
  18. emit([doc.category, doc.name], {
  19. _id: doc._id,
  20. name: doc.name,
  21. price: doc.price,
  22. images: doc.images,
  23. inStock: doc.inStock,
  24. brand: doc.brand
  25. });
  26. }
  27. }
  28. """
  29. by_brand:
  30. map: """
  31. function(doc) {
  32. if (doc.type === 'product' && doc.active !== false) {
  33. emit([doc.brand, doc.category], {
  34. _id: doc._id,
  35. name: doc.name,
  36. price: doc.price,
  37. category: doc.category
  38. });
  39. }
  40. }
  41. """
  42. by_sku:
  43. map: """
  44. function(doc) {
  45. if (doc.type === 'product') {
  46. emit(doc.sku, {
  47. _id: doc._id,
  48. name: doc.name,
  49. price: doc.price
  50. });
  51. }
  52. }
  53. """
  54. search_index:
  55. map: """
  56. function(doc) {
  57. if (doc.type === 'product' && doc.active !== false) {
  58. var searchable = [
  59. doc.name,
  60. doc.brand,
  61. doc.category,
  62. doc.description
  63. ].join(' ').toLowerCase();
  64. var words = searchable.split(/\\\\s+/);
  65. words.forEach(function(word) {
  66. if (word.length > 2) {
  67. emit(word, 1);
  68. }
  69. });
  70. }
  71. }
  72. """
  73. reduce: "_sum"
  74. language: "javascript"
  75. }
  76. getCategoriesDesignDoc: ->
  77. {
  78. _id: '_design/categories'
  79. views:
  80. hierarchical:
  81. map: """
  82. function(doc) {
  83. if (doc.type === 'category') {
  84. // Для построения иерархии категорий
  85. var path = doc.parent ? [doc._id] : [doc.parent, doc._id];
  86. emit(path, {
  87. _id: doc._id,
  88. name: doc.name,
  89. parent: doc.parent,
  90. order: doc.order,
  91. image: doc.image
  92. });
  93. }
  94. }
  95. """
  96. by_slug:
  97. map: """
  98. function(doc) {
  99. if (doc.type === 'category') {
  100. emit(doc.slug, {
  101. _id: doc._id,
  102. name: doc.name,
  103. parent: doc.parent
  104. });
  105. }
  106. }
  107. """
  108. active_categories:
  109. map: """
  110. function(doc) {
  111. if (doc.type === 'category' && doc.active !== false) {
  112. emit(doc.order, {
  113. _id: doc._id,
  114. name: doc.name,
  115. image: doc.image
  116. });
  117. }
  118. }
  119. """
  120. language: "javascript"
  121. }
  122. getOrdersDesignDoc: ->
  123. {
  124. _id: '_design/orders'
  125. views:
  126. by_user:
  127. map: """
  128. function(doc) {
  129. if (doc.type === 'order') {
  130. emit([doc.userId, doc.createdAt], {
  131. _id: doc._id,
  132. total: doc.total,
  133. status: doc.status,
  134. items: doc.items.length
  135. });
  136. }
  137. }
  138. """
  139. by_status:
  140. map: """
  141. function(doc) {
  142. if (doc.type === 'order') {
  143. emit([doc.status, doc.createdAt], {
  144. _id: doc._id,
  145. userId: doc.userId,
  146. total: doc.total
  147. });
  148. }
  149. }
  150. """
  151. language: "javascript"
  152. }
  153. getValidationDesignDoc: ->
  154. {
  155. _id: '_design/validation'
  156. validate_doc_update: """
  157. function(newDoc, oldDoc, userCtx, secObj) {
  158. // Функция проверки документов при сохранении :cite[2]:cite[10]
  159. // Проверка типа документа
  160. if (newDoc.type) {
  161. var validTypes = [
  162. 'product', 'category', 'order', 'user',
  163. 'domain_settings', 'hero_slide', 'blog_article'
  164. ];
  165. if (validTypes.indexOf(newDoc.type) === -1) {
  166. throw({forbidden: 'Invalid document type: ' + newDoc.type});
  167. }
  168. }
  169. // Проверка обязательных полей для товаров :cite[2]
  170. if (newDoc.type === 'product') {
  171. if (!newDoc.name) {
  172. throw({forbidden: 'Product must have a name'});
  173. }
  174. if (!newDoc.sku) {
  175. throw({forbidden: 'Product must have SKU'});
  176. }
  177. if (typeof newDoc.price !== 'number' || newDoc.price < 0) {
  178. throw({forbidden: 'Product must have valid price'});
  179. }
  180. }
  181. // Проверка категорий
  182. if (newDoc.type === 'category') {
  183. if (!newDoc.name) {
  184. throw({forbidden: 'Category must have a name'});
  185. }
  186. if (!newDoc.slug) {
  187. throw({forbidden: 'Category must have a slug'});
  188. }
  189. }
  190. // Проверка заказов
  191. if (newDoc.type === 'order') {
  192. if (!newDoc.userId) {
  193. throw({forbidden: 'Order must have user ID'});
  194. }
  195. if (!Array.isArray(newDoc.items) || newDoc.items.length === 0) {
  196. throw({forbidden: 'Order must have items'});
  197. }
  198. }
  199. // Проверка неизменяемых полей
  200. if (oldDoc) {
  201. if (oldDoc.type !== newDoc.type) {
  202. throw({forbidden: 'Document type cannot be changed'});
  203. }
  204. if (oldDoc.createdAt !== newDoc.createdAt) {
  205. throw({forbidden: 'Creation date cannot be changed'});
  206. }
  207. }
  208. // Проверка прав доступа :cite[2]
  209. if (newDoc.type === 'order' || newDoc.type === 'user') {
  210. if (userCtx.roles.indexOf('_admin') === -1 &&
  211. userCtx.roles.indexOf('user') === -1) {
  212. throw({unauthorized: 'You are not authorized to modify this document'});
  213. }
  214. }
  215. }
  216. """
  217. language: "javascript"
  218. }
  219. saveDesignDocs: (pouchService) ->
  220. log '💾 Сохранение дизайн-документов в базу...'
  221. promises = []
  222. for name, doc of @designDocs
  223. promises.push(
  224. pouchService.saveDocument(doc)
  225. .then ->
  226. log "✅ Дизайн-документ сохранен: #{name}"
  227. .catch (error) ->
  228. if error.status == 409
  229. log "⚠️ Дизайн-документ уже существует: #{name}"
  230. else
  231. log "❌ Ошибка сохранения дизайн-документа #{name}:", error
  232. )
  233. return Promise.all(promises)
  234. module.exports = new SiteDesignDocuments()