24 December 2010

CouchApp: JavaScript приложения в CouchDB

NoSQL
Sandbox
Когда-то давно, когда я практиковался в написании хранимых процедур, триггеров, курсоров под MSSQL, мне не давала покоя мысль о приложении, где вся бизнес-логика крутится на уровне БД, а presentation tier просто дергает базу и отвечает за отрисовку полученных результатов. С тех пор прошло много моих девелоперских лет, но возможности для реализации данной идеи так и не встретилось… до тех пор, пока я не наткнулся на CouchDB.

Я думаю, что многие уже слышали о NoSQL базах данных и в том числе о Couch DB. Здесь я хочу рассказать о замечательной возможности встраивать JavaScript-приложения в CouchDB, название которым CouchApp.



CouchApp описывается на сайте CouchDB книги как «Javascript и HTML5 приложение, которое отдается напрямую в браузер из CouchDB». Я думаю, что такое определение не совсем точно, так как в браузер в этом случае отдается HTML, а уже какой HTML отдавать решает JavaScript, работающий на сервере. Схема с сайта несколько лучше иллюстрирует эту идею:

Browser (UI and links between pages)
-------------- HTTP ---------------
CouchDB (persistence, business logic, and templating)

CouchDB представляет собой (или включает в себя) веб-сервер. Он выполняет свою функцию по отдаче результатов в JSON-формате на REST-запросы, которые являются основным способом общения с базой при любых операциях (от CRUD операций над данными до создания и удаления самих БД). Данные между сервером и клиентом идут по протоколу HTTP, соответственно, ничего не мешает ему отдавать HTML. Для этого надо просто правильным образом ему сказать об этом.

Приложение в CouchDB начинается с «design document». Так как все, включая данные, в CouchDB называется документом и задается в JSON формате, то и дизайн документ ничем не отличается. Он конечно же должен по особенному называться и иметь определенную структуру. Сам документ и содержит код приложения.

Простейший документ базы выглядит так:
{
"_id": "my_doc",
"_rev": "1-7a8b01193c8798fa555243b354d1e9d7"
}

Дизайн документ должен иметь _id в виде: _design/app_name. JavaScript-код приложения размазан по полям JSON-объекта, задающего дизайн документ (с соответствующим экранированием). В нем же указываются и другие аттрибуты приложения, такие как ссылки на файлы (attachments), манифест приложения и т.п. Развертывание приложения заключается в загрузке дизайн документа и файловых ресурсов (attachments) в базу данных.

Чтобы иметь возможность нормально работать над JS кодом в любимом редакторе, не собирать по крупицам из папок приложения дизайн документ, существует утилита couchapp. Написана на Python'е и занимается тем, что собирает ресурсы приложения в один json-документ и отправляет на сервер, отдельно заливаются attachments. Также позволяет генерировать начальную структуру папок для приложения, откуда она будет потом собирать ресурсы.

Итак, первое с чего стоит начать — это установить себе утилиту couchapp. Я не буду описывать процесс установки, он хорошо написан в wiki.

Далее, для знакомства с базой, можно либо поставить базу себе, а можно зарегистрировать бесплатный CouchDB хостинг. Последним вариантом я и воспользовался. В нем все хорошо (про ограничения ничего не скажу), кроме того, что там нельзя посмотреть логи, а соответственно отлаживать приложение может быть затруднительно.

Теперь все готово, чтобы начать практиковаться.

Начало


Я специально хотел написать что-то большее чем «Hello world» и отличное от блога, который разрабатывают в каждом учебнике, но, в тоже время, на него похожее, чтобы иметь возможность смотреть за примерами в документации и не слишком сильно увлекаться переносом кода из учебника в приложение. Как показывает практика, это позволяет понять сразу нюансы обращения с новой технологией.

Приложение, которое я буду описывать — это, так называемые, карточки для изучения иностранных слов (flash cards). Прицип простой — в жизни, на одной стороне карточки написано слово на целевом языке, на другой — его перевод. В приложении пользователю, соответственно, требуется интерфейс для просмотра списка наборов карточек (sets), возможность редактировать набор, добавляя и изменяя слова на карточках, и сам интерфейс прогона карточек.

Документ набора карточек в базе будет иметь следующий вид:
{
"_id": "my set",
"_rev": "1-7a8b01193c8798fa555243b354d1e9d6",
"type": "set",
"cards": [
{
"front": "front_text",
"back": "back_text"
},
{
"front": "front_text second",
"back": "back_text second"
}
]
}

"_id" — уникальный идентификатор сета, по совместительству являющийся и его названием (для простоты)
"_rev" — номер ревизии сета. Его при сохранении или создании докумета можно не указывать, тогда оно заполнится автоматически.

Поля "_id" и "_rev" являются обязательными атрибутами любого документа.

«type» — тип докумета. Так как таблиц в CouchDB нет и все документы лежат в базе рядом друг с другом, то, для простого отличия их друг от друга при выборках, удобно использовать специальное поле.
«cards» — массив объектов, задающих карты. Можно каждую карту хранить отдельно в документе с типом «card», но для простого приложения я решил так не поступать.

Создадим страничку с формой для заведения нового или редактирования существующего набора.

Страница редактирования


В CouchApp существует понятие «show function». Эта функция отвечает за отображение документа базы, т.е. трансформирует его в другую структуру, например может выдавать HTML на выходе.

Show функции хранятся в js файле и должны располагаться в папке shows. Вызов функции «edit» произойдет, когда мы обратимся по адресу http:////_design/flashcards/_show/edit/ и http:////_design/flashcards/_show/edit/<document_id>.

Код файла \shows\edit.js для нашей странички:
function(doc, req) {
//!json shows._edit

var Mustache = require("vendor/couchapp/lib/mustache"),
path = require("vendor/couchapp/lib/path").init(req);

var data = {
assets : path.asset(),
indexPath : path.list('sets', 'all-sets', {descending:true, limit:10})
};

data.doc = doc || {};

return Mustache.to_html(shows._edit, data);
}


Show функция принимает в качетсве параметров запрошенный документ, если он был запрошен и найден, и объект, описывающий входящий реквест. В первом случае, doc будет отсутствовать.

Далее идет комментарий, который является директивой couchapp скрипту (называется macro) записать в переменную «shows._edit» содержание файла \shows\_edit.*. Это значит, что в папке не может лежать больше одного файла с названием _edit вне зависимости от расширения. В нашем случае там будет лежать файл _edit.html, который содержит код нашей страницы с формой. Вернемся к нему позже.

Далее идет импорт двух модулей. CouchDB следует конвенции CommonJS для разработки server-side javascript, поэтому функции для импорта могут быть знакомы разработчикам на Node.js. Я с Node.js не работал и какой-либо документации в CouchDB найти не смог по доступным функциям. Пришлось смотреть в коды СouchDB и разбираться по примерам.

Первый модуль — Mustache, он отвечает за рендеринг всего. Нам он нужен для рендеринга html страницы с данными, которые мы приготовим для нее. Второй — path, позволяет нам генерировать ссылки на другие страницы нашего приложения и ресурсы, такие как js скрипты для работы с базой (какие-то сгенерируются couchapp при инициализации проекта, а какие-то уже доступны на веб сервере базы).

Далее создается объект модели (из MVC-паттерна). Поле assets — это url-префикс директории где разложены скрипты, indexPath — URL страницы со списком наших наборов. Отображением списка документов занимаются list-функции, о которых будет сказано далее. Метод .list выдаст ссылку на на функцию sets, в которую, в свою очередь, будет передан результат view-функции all-sets.

Ниже идет добавление в модель самого документа, т.е. набора карточек. Как я уже писал, к функции edit мы можем обратиться либо с id документа, либо без него. В случае если документ найден, то он будет в параметре doc. Если его не нашлось, то мы создаем пустой объект для набора. Таким образом, когда открывается страница редактирования набора, то мы редактируем либо новый, либо существующий набор.

Часть html-кода страницы отвечающего за отрисовку набора:
<form id="new-post" method="post">
{{#doc}}
<label>Set name</label> <input type="text" size="20" name="_id" value="{{_id}}">
<ul class="cards">
{{#cards}}
<li>
<label>Front</label> <input type="text" size="20" name="front" value="{{front}}">
<label>Back</label> <input type="text" size="20" name="back" value="{{back}}">
</li>
{{/cards}}

<li>
<label>Front</label> <input type="text" size="20" name="front">
<label>Back</label> <input type="text" size="20" name="back">
</li>
</ul>

<button id="add">Add</button><br/><br/>
{{/doc}}

<input type="submit" value="Save &rarr;"/>
<span id="saved" style="display:none;">Saved</span>
<br/><br/>
</form>

Ничего особенного. Все в соотвествии с mustache.

После того как мы показали форму, теперь надо написать функционал по сохранению формы. Для этого надо послать POST-запрос с JSON-документом набора карточек. Для этого на страницу добавляются следующие скрипты: /_utils/script/jquery.js и /_utils/script/jquery.couch.js. Первый это jQuery, второй — плагин к jQuery, организующий запросы к CouchDB.

Теперь наш скрипт:
(function($) {
$('#new-post').submit(function() {
var self = $(this), setName = $('input[name=_id]', this).val(),
oldSet, newSet = {}, db = $.couch.db('flashcards');

function saveDoc(set) {
db.saveDoc(set), {
success: function(resp) {
window.location.reload(true);
}
});
}

//collect cards array
newSet.cards = $('ul.cards input').toArray().reduce(function(arr, el, idx) {
if ( !(idx%2) ) { arr[arr.length] = {}; }
arr[arr.length - 1][ !(idx%2) ? "front" : "back" ] = el.value;

return arr;
}, []);

//retrieve old set if found or create new
db.openDoc(setName, {
error: function(code) {
oldSet = {};
if (setName.length) {
oldSet._id = setName;
}
saveDoc($.extend(oldSet, newSet);

},
success : function(response) {
oldSet = response;
saveDoc($.extend(oldSet, newSet);
}
});

return false;
});
})(jQuery);

Основными операциями по работе с БД здесь являются:
1. var db = $.couch.db('flashcards') — получение объекта базы данных для вызова методов сохранения, открытия и др.
2. db.openDoc(id, callback) — получает документ из базы данных по id. Используется для получения последней версии документа. На сервер необходимо послать документ с последней ревизией, иначе он будет отвергнут. В CouchDB реализуется только такой принцип разделения совместного доступа к ресурсу. Если документ не найден, то подразумевается, что создается новый набор. Далее вызывается сохранение в базу всего объекта набора.
3. db.saveDoc(doc, callback) — сохранение документа в базу. Если все успешно, то страница перегружается. Случай, когда что-то пошло не так, а это может произойти, если послана была старая ревизия, я не рассматриваю.

Теперь у нас есть полноценная функциональность по созданию и редактированию наборов карт. Перейдем к отображению списка наборов.

Отображение списков


Прежде чем отобразить список, нужно сделать выборку. Выборка осуществляется при помощи view. В отличие от SQL, где запрос может строиться динамически и, потом, отрабатывать на данных в таблице, view работает несколько иначе. Нет возможности передать во view какие-либо параметры из HTTP-запроса, но их можно применить к результатам полученых от view.

View — это функция, применяемая к каждому документу базы. С помощью нее осуществляется фильтрация документов, чтобы на результаты этой фильтрации наложить индекс. View состоит из функции map и опциональной reduce. Для отображения всех наборов фунция \views\all-sets\map.js (all-sets будет называться view) выглядит следующим образом:
function(doc) {
if (doc.type === 'set' && doc.cards) {
emit(doc._id, null);
}
}

Здесь проверяется на соответствие документа типу set и наличие массива с карточками. В случае соответствия, вызывается функция emit принимающая два аргумента — ключ и значение. Для отображения списка набора, значение второго параметра роли не играет, т.е. нужен только ключ, представляющий имя набора. По нему также сортируются результаты. По полученному набору ключей и будут построены индексы.

Ко view можно делать запросы, сужающие выборку. Для некоторых запросов потребуется нетривиальная схема генерации ключей, для других дополнительная функция reduce. Я не буду фокусироваться на этом, так как про это в документации неплохо написано.

После того как у нас определен view, мы можем задать отображение его результатов. Если для отображения документов использовались show функции, то здесь понадобятся list-функции. Функция \lists\sets.js:
function(head, req) {
// !json lists._sets

var Mustache = require("vendor/couchapp/lib/mustache"),
path = require("vendor/couchapp/lib/path").init(req);

provides('html', function() {
var data = {
assets : path.asset(),
createNewLink : path.show('edit')
};

data.sets = (function(){
var row, set, arr = [];
while(row = getRow()) {
set = row.value;
arr.push({
title: row.id,
showView : path.show('set', row.id),
editLink : path.show('edit', row.id),
runsetLink : path.asset('flashcards_zeptojs.html') + '#' + row.id
});
}
return arr;
})();

return Mustache.to_html(lists._sets, data);
});
}

Отличительная особенность от show-функции состоит в том, что здесь доступна функция provides(content_type, callback), которая вызывает callback, если совпадает content-type запроса и getRow(), которая возвращает строки из view. Из какого view отображать строки определяется через url: http:////_design/flashcards/_list/sets/<view-name>?. В head параметре функции будут записаны параметры view, в req — многочисленные параметры HTTP-запроса. В остальном поступаем также, как в show-функции — берем шаблон и передаем туда получившуюся модель, а потом возвращаем результат.

За рамками статьи остались две страницы с предпросмотром набора карточек и сам интерфейс прогона, но они абсолютно тривиальны и реализуются тем же набором функций, который описывался выше.

Это, наверное, все, что я считаю нужным, чтобы иметь понимание о CouchApp. Дальше можно смело обращаться к документации и реализовывать что-то осмысленное.

Заключение


Что осталось в осадке от опыта изучения CouchDB с CouchApp:

1. Очень дырявая документация.
CouchDB Book, их даже две версии — 1.0 и draft, обе лежат в общем доступе, раскрывают в подробностях основные концепции самой базы данных, но описание couchApp очень сумбурное и с пробелами. Приходится ходить по разным записям в блогах, чтобы получить общую картину о том, как все работает. Я даже думаю, что прочтение целиком книги не гарантирует обретения полного понимания, хотя читать 300 страниц, чтобы иметь высокоуровневое понимание, мне кажется многовато. С другой стороны, есть небольшие helloworld статьи, которые выводят только сакральную надпись, но глубже не идут, что пользы приносит очень мало.

Забавная вещь — отсутствие описания JavaScript API в CouchApp. Причем референса по такому API я вообще не нашел ни на сайте couchone.com, ни на wiki.apache.org/couchdb, ни на официальном сайте couchapp.org.

В книге CouchDB предлагается смотреть в пример Sofa (блог), который лежит на github. Код этого приложения уехал настолько далеко, что только местами можно найти лишь отдаленное сходство. Плюс если код приложения в книге использует достаточно базовые вещи, то приложение в репозитории уже использует более высокоуровневые конструкции. Когда разберешься что к чему, уже не так страшно, но для человека только открывшего CouchApp по-моему слишком сложно. Может быть этому может способствовать наличие опыта в server-side JS, я не знаю, такого опыта у меня не было.

Также удручает отсутствие документации по поставляемым javascript библиотекам. Не понятно где они лежат (я не сразу осознал, что некоторые библиотеки предоставляются скриптом couchapp, а некоторые уже есть на сервере, но где — еще предстояло выяснить), какой у них API и что они делают. Например, есть в блоге жены лида CouchDB описание тесткейсов для javascript библиотек (одна plain js, другая jquery-based), осуществляющих ajax-запросы к базе. Но эти библиотеки не используются в Sofa приложении. Там используется другая библиотека, расширяющая их возможности, но которая зачем-то при первом запросе вытаскивает весь дизайн-документ приложения, чтобы из него правильно строить урлы. А размер дизайн-документа даже в моем маленьком приложении составляет около 1 мегабайта.

Вообщем, если судить по документации, то от проекта разит таким серьезным студенческим дипломом, но никак не коммерческим продуктом, несмотря на то, что продукт серьезен.

2. Security приложения.
Изначально абсолютно все пользователи могут делать любые запросы к базе данных. Это запрещается путем задания пользователей базы данных. Но так как CouchApp крутится в рамках одной базы, то если пользователь имеет доступ к CouchApp, то он имеет полный доступ и ко всем документам. И если писать можно запретить на уровне validation функций, то с чтением так не получится. Любой авторизованный пользователь сможет сделать встроенный запрос /_all_docs и получить все-все документы в базе.

Наверное, это можно обойти прикрутив поверх Apache, где закрыть все ненужные URLs, решив при этом попутно и вопрос pretty urls (не будет же пользователь ходить по /_lists/sets/...?...), но я так не пробовал, хотя тот же couchapp.org так и работает.

Из всего этого, могу сделать вывод, что спокойнее будет иметь CouchDB за каким-то приложением, которое возьмет на себя секьюрити и обработку POST запросов с форм. Но проект развивается и, если на нововведения, которые упрощают жизнь, будет написана вменяемая документация, то можно попробовать пересмотреть мои взгляды.

Спасибо, надеюсь, было интересно.
Tags:couchdbcouchappserver-side js
Hubs: NoSQL
+27
6k 34
Comments 7