|
|
@@ -1,85 +1,250 @@
|
|
|
-module.exports = {
|
|
|
- _id: '_design/site',
|
|
|
- version: '1.0.0',
|
|
|
- appVersion: '1.0.0',
|
|
|
- hash: 'site_v1_0_0_' + Date.now(),
|
|
|
-
|
|
|
- views: {
|
|
|
- # Активные товары для каталога
|
|
|
- active_products: {
|
|
|
- map: ((doc) ->
|
|
|
- if doc.type == 'product' && doc.active == true
|
|
|
- emit([doc.category, doc.name], {
|
|
|
- _id: doc._id,
|
|
|
- name: doc.name,
|
|
|
- price: doc.price,
|
|
|
- oldPrice: doc.oldPrice,
|
|
|
- sku: doc.sku,
|
|
|
- image: doc.image,
|
|
|
- category: doc.category,
|
|
|
- description: doc.description,
|
|
|
- attributes: doc.attributes
|
|
|
- })
|
|
|
- ).toString()
|
|
|
- },
|
|
|
-
|
|
|
- # Опубликованные статьи блога
|
|
|
- published_articles: {
|
|
|
- map: ((doc) ->
|
|
|
- if doc.type == 'blog_article' && doc.published == true
|
|
|
- emit([doc.createdAt], {
|
|
|
- _id: doc._id,
|
|
|
- title: doc.title,
|
|
|
- slug: doc.slug,
|
|
|
- excerpt: doc.excerpt,
|
|
|
- image: doc.image,
|
|
|
- author: doc.author,
|
|
|
- createdAt: doc.createdAt,
|
|
|
- content: doc.content
|
|
|
- })
|
|
|
- ).toString()
|
|
|
- },
|
|
|
-
|
|
|
- # Активные слайды
|
|
|
- active_slides: {
|
|
|
- map: ((doc) ->
|
|
|
- if doc.type == 'hero_slide' && doc.active == true
|
|
|
- emit(doc.order, {
|
|
|
- _id: doc._id,
|
|
|
- title: doc.title,
|
|
|
- subtitle: doc.subtitle,
|
|
|
- image: doc.image,
|
|
|
- buttonText: doc.buttonText,
|
|
|
- buttonLink: doc.buttonLink
|
|
|
- })
|
|
|
- ).toString()
|
|
|
+# app/design/site.coffee
|
|
|
+class SiteDesignDocuments
|
|
|
+ constructor: ->
|
|
|
+ @designDocs = {
|
|
|
+ products: @getProductsDesignDoc()
|
|
|
+ categories: @getCategoriesDesignDoc()
|
|
|
+ orders: @getOrdersDesignDoc()
|
|
|
+ validation: @getValidationDesignDoc()
|
|
|
}
|
|
|
- },
|
|
|
-
|
|
|
- validates_doc_update: ((newDoc, oldDoc, userCtx) ->
|
|
|
- # Базовая валидация документов
|
|
|
-
|
|
|
- # Запрещаем изменение design документов
|
|
|
- if newDoc._id && newDoc._id.startsWith('_design/')
|
|
|
- if oldDoc # existing document
|
|
|
- throw { forbidden: 'Design documents can only be updated by admins' }
|
|
|
-
|
|
|
- # Валидация товаров
|
|
|
- if newDoc.type == 'product'
|
|
|
- if !newDoc.name
|
|
|
- throw { forbidden: 'Product must have a name' }
|
|
|
- if !newDoc.price || isNaN(parseFloat(newDoc.price))
|
|
|
- throw { forbidden: 'Product must have a valid price' }
|
|
|
- if !newDoc.sku
|
|
|
- throw { forbidden: 'Product must have SKU' }
|
|
|
+
|
|
|
+ getProductsDesignDoc: ->
|
|
|
+ {
|
|
|
+ _id: '_design/products'
|
|
|
+ views:
|
|
|
+ by_category:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'product' && doc.active !== false) {
|
|
|
+ emit([doc.category, doc.name], {
|
|
|
+ _id: doc._id,
|
|
|
+ name: doc.name,
|
|
|
+ price: doc.price,
|
|
|
+ images: doc.images,
|
|
|
+ inStock: doc.inStock,
|
|
|
+ brand: doc.brand
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ by_brand:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'product' && doc.active !== false) {
|
|
|
+ emit([doc.brand, doc.category], {
|
|
|
+ _id: doc._id,
|
|
|
+ name: doc.name,
|
|
|
+ price: doc.price,
|
|
|
+ category: doc.category
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ by_sku:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'product') {
|
|
|
+ emit(doc.sku, {
|
|
|
+ _id: doc._id,
|
|
|
+ name: doc.name,
|
|
|
+ price: doc.price
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ search_index:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'product' && doc.active !== false) {
|
|
|
+ var searchable = [
|
|
|
+ doc.name,
|
|
|
+ doc.brand,
|
|
|
+ doc.category,
|
|
|
+ doc.description
|
|
|
+ ].join(' ').toLowerCase();
|
|
|
+
|
|
|
+ var words = searchable.split(/\\\\s+/);
|
|
|
+ words.forEach(function(word) {
|
|
|
+ if (word.length > 2) {
|
|
|
+ emit(word, 1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ reduce: "_sum"
|
|
|
+ language: "javascript"
|
|
|
+ }
|
|
|
+
|
|
|
+ getCategoriesDesignDoc: ->
|
|
|
+ {
|
|
|
+ _id: '_design/categories'
|
|
|
+ views:
|
|
|
+ hierarchical:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'category') {
|
|
|
+ // Для построения иерархии категорий
|
|
|
+ var path = doc.parent ? [doc._id] : [doc.parent, doc._id];
|
|
|
+ emit(path, {
|
|
|
+ _id: doc._id,
|
|
|
+ name: doc.name,
|
|
|
+ parent: doc.parent,
|
|
|
+ order: doc.order,
|
|
|
+ image: doc.image
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ by_slug:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'category') {
|
|
|
+ emit(doc.slug, {
|
|
|
+ _id: doc._id,
|
|
|
+ name: doc.name,
|
|
|
+ parent: doc.parent
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ active_categories:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'category' && doc.active !== false) {
|
|
|
+ emit(doc.order, {
|
|
|
+ _id: doc._id,
|
|
|
+ name: doc.name,
|
|
|
+ image: doc.image
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ language: "javascript"
|
|
|
+ }
|
|
|
+
|
|
|
+ getOrdersDesignDoc: ->
|
|
|
+ {
|
|
|
+ _id: '_design/orders'
|
|
|
+ views:
|
|
|
+ by_user:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'order') {
|
|
|
+ emit([doc.userId, doc.createdAt], {
|
|
|
+ _id: doc._id,
|
|
|
+ total: doc.total,
|
|
|
+ status: doc.status,
|
|
|
+ items: doc.items.length
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ by_status:
|
|
|
+ map: """
|
|
|
+ function(doc) {
|
|
|
+ if (doc.type === 'order') {
|
|
|
+ emit([doc.status, doc.createdAt], {
|
|
|
+ _id: doc._id,
|
|
|
+ userId: doc.userId,
|
|
|
+ total: doc.total
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ language: "javascript"
|
|
|
+ }
|
|
|
+
|
|
|
+ getValidationDesignDoc: ->
|
|
|
+ {
|
|
|
+ _id: '_design/validation'
|
|
|
+ validate_doc_update: """
|
|
|
+ function(newDoc, oldDoc, userCtx, secObj) {
|
|
|
+ // Функция проверки документов при сохранении :cite[2]:cite[10]
|
|
|
+
|
|
|
+ // Проверка типа документа
|
|
|
+ if (newDoc.type) {
|
|
|
+ var validTypes = [
|
|
|
+ 'product', 'category', 'order', 'user',
|
|
|
+ 'domain_settings', 'hero_slide', 'blog_article'
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (validTypes.indexOf(newDoc.type) === -1) {
|
|
|
+ throw({forbidden: 'Invalid document type: ' + newDoc.type});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Проверка обязательных полей для товаров :cite[2]
|
|
|
+ if (newDoc.type === 'product') {
|
|
|
+ if (!newDoc.name) {
|
|
|
+ throw({forbidden: 'Product must have a name'});
|
|
|
+ }
|
|
|
+ if (!newDoc.sku) {
|
|
|
+ throw({forbidden: 'Product must have SKU'});
|
|
|
+ }
|
|
|
+ if (typeof newDoc.price !== 'number' || newDoc.price < 0) {
|
|
|
+ throw({forbidden: 'Product must have valid price'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Проверка категорий
|
|
|
+ if (newDoc.type === 'category') {
|
|
|
+ if (!newDoc.name) {
|
|
|
+ throw({forbidden: 'Category must have a name'});
|
|
|
+ }
|
|
|
+ if (!newDoc.slug) {
|
|
|
+ throw({forbidden: 'Category must have a slug'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Проверка заказов
|
|
|
+ if (newDoc.type === 'order') {
|
|
|
+ if (!newDoc.userId) {
|
|
|
+ throw({forbidden: 'Order must have user ID'});
|
|
|
+ }
|
|
|
+ if (!Array.isArray(newDoc.items) || newDoc.items.length === 0) {
|
|
|
+ throw({forbidden: 'Order must have items'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Проверка неизменяемых полей
|
|
|
+ if (oldDoc) {
|
|
|
+ if (oldDoc.type !== newDoc.type) {
|
|
|
+ throw({forbidden: 'Document type cannot be changed'});
|
|
|
+ }
|
|
|
+ if (oldDoc.createdAt !== newDoc.createdAt) {
|
|
|
+ throw({forbidden: 'Creation date cannot be changed'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Проверка прав доступа :cite[2]
|
|
|
+ if (newDoc.type === 'order' || newDoc.type === 'user') {
|
|
|
+ if (userCtx.roles.indexOf('_admin') === -1 &&
|
|
|
+ userCtx.roles.indexOf('user') === -1) {
|
|
|
+ throw({unauthorized: 'You are not authorized to modify this document'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ language: "javascript"
|
|
|
+ }
|
|
|
+
|
|
|
+ saveDesignDocs: (pouchService) ->
|
|
|
+ log '💾 Сохранение дизайн-документов в базу...'
|
|
|
|
|
|
- # Валидация статей блога
|
|
|
- if newDoc.type == 'blog_article'
|
|
|
- if !newDoc.title
|
|
|
- throw { forbidden: 'Blog article must have a title' }
|
|
|
- if !newDoc.slug
|
|
|
- throw { forbidden: 'Blog article must have a slug' }
|
|
|
+ promises = []
|
|
|
+ for name, doc of @designDocs
|
|
|
+ promises.push(
|
|
|
+ pouchService.saveDocument(doc)
|
|
|
+ .then ->
|
|
|
+ log "✅ Дизайн-документ сохранен: #{name}"
|
|
|
+ .catch (error) ->
|
|
|
+ if error.status == 409
|
|
|
+ log "⚠️ Дизайн-документ уже существует: #{name}"
|
|
|
+ else
|
|
|
+ log "❌ Ошибка сохранения дизайн-документа #{name}:", error
|
|
|
+ )
|
|
|
|
|
|
- return true
|
|
|
- ).toString()
|
|
|
-}
|
|
|
+ return Promise.all(promises)
|
|
|
+
|
|
|
+module.exports = new SiteDesignDocuments()
|