Colibri - это фреймворк для Node.js, который упрощает задачу построения REST-сервисов для управления данными в MongoDB.
npm install colibri
Допустим, у нас в mongodb есть коллекция статей Articles
и мы хотим управлять ей через REST-ресурс.
Также у нас есть веб-сервер на Express, в который мы хотим добавить наш ресурс. Допустим, это - веб-сервер:
express = require 'express'
app = express.createServer()
app.use express.bodyParser()
app.use express.static "#{__dirname}/static"
app.listen 3000
Кроме того, мы создали модель ArticleModel с помощью Mongoose:
ArticleSchema = new mongoose.Schema
title : String
body : String
ArticleModel = mongoose.model 'Articles', ArticleSchema
Наш REST-ресурс должен обслуживать следующие запросы:
GET /articles - получить список статей
POST /articles - создать статью
GET /articles/1234 - получить статью с _id = 1234
PUT /articles/1234 - изменить статью с _id = 1234
DELETE /articles/1234 - удалить статью с _id = 1234
Нас ждёт куча монотоной, кропотливой работы! Придётся написать множество маршрутов app.get(...)
, app.post(...)
и т.д., в которых предстоит реализовать разбор запросов, выборку сущностей, операции с ними, обработку ошибок, и наконец, возврат результата на клиент.
Colibri упрощает эту задачу. Вот как с его помощью строится REST-сервис:
#создаём ресурс
resource = colibri.createResource
model : ArticleModel
#с помощью ресурса создаём маршруты на сервере app
resource.express app
Теперь у нас есть готовый бэкенд, к которому мы можем, например, подключиться из браузера с помощью Backbone или других клиентских библиотек, поддерживающих REST.
Пример ToDo-приложения на Backbone показан в папке examples.
Создает REST-ресурс.
options:
mtimeField
- поле, в котором сохраняется время последнего изменения документа (по умолчанию false)ctimeField
- поле, в котором сохраняется время создания документа (по умолчанию false)upsert
- создавать ли документ в методе PUT, если документ с заданным _id
не найденhooks
- набор пользовательских прослоек. Устаревшая опция, вместо неё используйте Resource#use().plainOutput
- по умолчанию результат запроса (req.rest.result
) передаётся в свойстве result
JSON-объекта. Это сделано для того, чтобы в остальных полях объекта можно было передать поля мета-данных из req.rest.meta
. Если plainOutput = true
, то результат запроса передаётся в корне JSON-объекта, без мета-данных. Если в качестве клиента вы используете Backbone
(в базовом виде), то, скорее всего, вы захотите установить опцию plainOutput
в true
. Однако тогда вам будут недоступны мета-данные (которые могут содержать полезную информацию, переданную из плагинов, например поле totalCount
из плагина count).возвращаемое значение:
объект класса Resource
Добавляет плагин. Плагин - это набор прослоек (route middleware) Express, которые навешиваются после шагов/методов Colibri для изменения их функциональности.
Colibri на каждый запрос создаёт объект req.rest
, через свойства которого можно управлять менять поведение REST-метода. Например, добавлять условия в запросы, проверять авторизацию, делать постраничную навигацию, скрывать/добавлять поля в результирующие объекты, выполнять логгирование, обновление связанных записей, и т.д.
Бизнес-логику приложения удобно оформлять в виде таких плагинов.
plugin:
Двухуровневый объект, в котором задаются прослойки, навешиваемые на определённые методы и шаги ресурса.
Имена свойств первого уровня соответствует именам методов, используемых в Colibri: get
, put
, post
, del
или list
.
Имена свойств второго уровня - именам шагов соответсвующего метода: begin
, input
, query
, serialize
, output
и пр.
Значения свойств второго уровня - это функции или массивы функций, представляющие собой стандартные прослойки маршрутов (route middleware) Express - функции или массивы функций, выполняющиеся после соответствующих шагов соответствующего метода/шага.
Так, например, если мы хотим добавить сортировку списка по определённому полю, мы должны написать такой плагин:
resource.use({
//list - имя метода
list : {
//query - имя шага.
query : function (req, res, next) { //эта функция выполнится после шага query
//В шаге list.query доступен объект req.rest.query,
//представляющий собой Mongoose-запрос.
//Добавляем параметр сортировки.
req.rest.query.sort({my_field_name : 1});
//Передаем управление дальше.
next(null);
//Следующим выполнится шаг load, который использует
//req.rest.query для выборки документов
}
}
});
Список доступных шагов, а также свойств req.rest.*
, доступных для модификации, (TODO).
Добавляет созданный ресурс в Express-приложение.
Вызывайте этот метод после всех вызовов Resource#use()
.
Допустим у нас есть приложение такого вида:
express = require 'express'
app = express.createServer()
app.use express.bodyParser()
app.use express.static "#{__dirname}/static"
app.listen 3000
ArticleSchema = new mongoose.Schema
title : String
body : String
ArticleModel = mongoose.model 'Articles', ArticleSchema
resource = colibri.createResource
model : ArticleModel
Мы создали “голый” REST-ресурс по адресу /articles/
. Он умеет отдавать статьи и списки статей, редактировать и удалять статьи.
Отлично, но мы не хотим, чтобы неавторизованные пользователи могли изменять наши статьи.
Допустим, система авторизации уже есть (ни Colibri, ни Express не предоставляют таковой “из коробки”, но есть отличные сторонние решения), а также предположим, что эта система добавляет переменные req.isAuthorized
и req.currentUser
.
Тогда мы должны написать плагин для нашего ресурса, который бы проверял req.isAuthorized
и отвечал HTTP-кодом 403
, если пользователь не авторизован.
Проверять авторизацию будет обычная прослойка (middleware) для Express:
isAuthorized = (req, res, next)->
return res.send 403 unless req.isAuthorized
next null
Теперь надо добавить её во все HTTP-методы нашего ресурса, которые мы хотим защитить:
resource.use
post:
begin: isAuthorized
del:
begin: isAuthorized
put:
begin: isAuthorized
Что всё это значит? У каждого HTTP-метода в Colibri-ресурсе есть последовательность фаз (шагов). Например, шаги для метода post
таковы:
'begin' - начало
'input' - разбор HTTP-запроса
'create' - создание и заполнение Mongoose-документа
'save' - сохранение Mongoose-документа
'output' - выдача Mongoose-документа на клиент
Colibri строит из всех этих шагов цепочку прослоек для Express, которые выполняются как асинхронный конвейер.
К счастью, в Colibri имеются средства для вмешательства в этот процесс. Это делается с помощью метода Resource#use
. В него передается объект, в котором определяется в какой HTTP-метод и после какого из его шагов мы хотим вставить собственные функции-прослойки.
Именно это мы сделали, когда добавили нашу функцию isAuthorized
в шаг begin
методов post
, put
и delete
.
Мы написали простейший плагин, который проверяет авторизацию.
Но с помощью API плагинов можно делать много других вещей. Например, мы можем добавить сортировку списка документов в зависимости от входных параметров GET-запроса. Можем добавить автоматически заполняемые поля (например, ID автора статьи) в обработчик POST-запроса. Можем также реализовать более гибкую проверку правд доступа (например, сделать так, чтобы пользователь мог редактировать только свои статьи, а не чужие). И так далее.
Colibri по умолчанию создаёт простейший незащищённый, но уже работоспособный REST-ресурс. Задача разработчика приложения - отсечь лишние возможности и добавить свои плюшки с помощью плагинов.
Документация по плагинам - TODO.
Готовые плагины - тоже пока TODO. В разработке - плагины range
и paginate
.
Документация по добавлению кастомных методов - TODO.
ALL_METHODS.begin
adds req.rest.meta:
ALL_METHODS.begin
adds req.rest.model:
ALL_METHODS.begin
adds req.rest.currentTime:
ALL_METHODS.output
uses req.rest.result:
ALL_METHODS.output
uses req.rest.meta:
get.input
adds req.rest._id:
req.param('_id')
get.load
uses req.rest._id:
get.load
adds req.rest.document:
req.rest.model.findById(req.rest._id)
get.serialize
adds req.rest.result:
req.res.document.toObject()
del.input
adds req.rest._id:
req.param('_id')
del.load
uses req.rest._id:
del.load
adds req.rest.document:
req.rest.model.findById(req.rest._id)
del.remove
uses req.rest.document:
list.query
adds req.rest.query:
rest.model.find()
list.load
uses req.rest.query:
list.load
adds req.rest.documents:
req.rest.query.exec()
list.serialize
adds req.rest.result:
req.res.documents.toObject()
post.input
adds req.rest.fieldValues:
req.body
post.create
adds req.rest.document:
new req.rest.model
with fields populated from req.rest.fieldValues
post.save
uses req.rest.document:
post.serialize
adds req.rest.result:
req.res.document.toObject()
put.input
adds req.rest._id:
req.param('_id')
put.input
adds req.rest.fieldValues:
req.body
put.load
uses req.rest._id:
put.load
uses req.rest.currentTime:
options.ctimeField
put.load
adds req.rest.document:
findById
or newly created (if upsert
option is on)put.update
uses req.rest.fieldValues:
req.res.document
put.update
uses req.rest.document:
req.res.document
put.save
uses req.rest.document:
put.serialize
adds req.rest.result:
req.res.document.toObject()