# app/design/site.coffee class SiteDesignDocuments constructor: -> @designDocs = { products: @getProductsDesignDoc() categories: @getCategoriesDesignDoc() orders: @getOrdersDesignDoc() validation: @getValidationDesignDoc() } 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 '💾 Сохранение дизайн-документов в базу...' 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 Promise.all(promises) module.exports = new SiteDesignDocuments()