Commit 07ee148d authored by Patiphan Marak's avatar Patiphan Marak

ขอ 2 คะแนน

parents
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'สวัสดีคุณ'
});
rl.prompt();
rl.on('line', (line) => {
switch (line.trim()) {
case 'hello':
console.log('world!');
break;
default:
console.log(`สวัสดีคุณ '${line.trim()}'`);
break;
}
rl.prompt();
})
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
node_modules
npm-debug.log
sudo: false
language: node_js
node_js:
- "5"
- "4"
- "0.12"
- "0.10"
script: "npm test"
services:
- mongodb
version 0.4.0 - 2015-01-14
* Warning: probably incompatible with 0.3.x because of updated dependencies
* Complete move to express 4.x API
* Move to mongodb 2.x
* Move to mongoose 4.x
* Add 'unauthorized' http status, as mentioned in docs
* Add 'status()' helper to hooks 'next' callback, as mentioned in docs
version 0.3.7 - 2014-05-05
* Enable overriding mongoose collection paths (fixes #1)
version 0.3.6 - 2014-04-23
* Enable creating separate yarm instances
version 0.3.5 - 2014-03-06
* Fixed not being able to POST to mongoose scalar array fields
version 0.3.4 - 2014-03-01
* Fixed a wrong implementation of mongoose and native hooks that prevented
overriding subresources
* Fixed error when requesting aggregate collections with a limit of 0
version 0.3.3 - 2014-02-14
* Add 'postResponse' option to mongoose and native resources
* Allow using a custom request handler with cb.custom(function(req, res) { ... });
* Add readonly helper to all resources
version 0.3.2 - 2014-01-30
* Fixed mongoose helper directly passing yarm callback to mongoose
* URL-decode wildcard-matched parameters (except for "*")
version 0.3.0 - 2014-01-24
* Full rewrite and API change
version 0.2.0 - 2013-08-11
* Added 'key' option to mongoose resources
* Added mongoose aggregate support
* Added custom queries to DocumentArrays
version 0.1.1 - 2013-08-03
* Improved mongoose resource handling
* Fixed bugs with .get/.list precedence
* Added custom queries to mongoose collections
* Rewrote native resources implementation
* Added new tests
version 0.0.6 - 2013-08-02
* Fixed a bug with readable streams
* Added tests
version 0.0.5 - 2013-08-01
* Initial release
The MIT License (MIT)
Copyright (c) 2013-2016 Nicolas Joyard
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
yarm
====
*Yet Another REST Middleware for node.js, Express and mongoose.*
Master branch: [![Build Status](https://travis-ci.org/njoyard/yarm.png?branch=master)](https://travis-ci.org/njoyard/yarm)
Development branch: [![Build Status](https://travis-ci.org/njoyard/yarm.png?branch=devel)](https://travis-ci.org/njoyard/yarm)
## Installation
Use npm to install yarm, or add yarm to your package.json dependencies.
```
$ npm install yarm
```
yarm has no dependencies, however it is intended to be used with Express and will have additional features if mongoose is present.
## Usage
Below is a short introduction to yarm usage, see the [complete documentation][doc] for more information.
### Basics
Use yarm as any other Express middleware.
```javascript
var app = require("express")();
var yarm = require("yarm");
app.use("/rest", yarm());
app.listen(80);
```
### Serving native javascript resources
Use `yarm.native()` to serve native Javascript objects or arrays.
```javascript
var app = require("express")();
var yarm = require("yarm");
app.use("/rest", yarm());
yarm.native("me", {
name: "Alice",
age: 30
});
yarm.native("friends", [
"Bob",
"Charlie"
]);
app.listen(80);
```
```
$ curl http://localhost/rest/me
{
"name": "Alice",
"age": 30
}
$ curl http://localhost/rest/me/name
Alice
$ curl http://localhost/rest/friends
{
"_count": 2,
"_items": [ "Bob", "Charlie" ]
}
$ curl http://localhost/rest/friends/1
Charlie
```
Head on to the documentation on [Native resources][doc-native] for more details.
### Serving mongoose models and aggregates
When mongoose is available, you can use `yarm.mongoose()` to serve models.
```javascript
var app = require("express")();
var yarm = require("yarm");
var mongoose = require("mongoose");
app.use("/rest", yarm());
var postSchema = new mongoose.Schema({
title: String,
text: String,
comments: [{
author: String,
text: String
}]
});
var Post = mongoose.model("post", postSchema);
yarm.mongoose("posts", Post);
app.listen(80);
```
```
$ curl http://localhost/rest/posts?skip=10&limit=1
{
"_count": 42,
"_items": [
{
"_id": "507f191e810c19729de860ea",
"title": "My 11th post",
"text": "Hello, World",
"comments": [
{
"author": "Bob",
"text": "First !"
}
]
}
]
}
$ curl http://localhost/rest/posts/507f191e810c19729de860ea
{
"_id": "507f191e810c19729de860ea",
"title": "My 11th post",
"text": "Hello, World",
"comments": [
{
"author": "Bob",
"text": "First !"
}
]
}
$ curl http://localhost/rest/posts/507f191e810c19729de860ea/comments/0/text
First !
```
Head on to the documentation on [Mongoose resources][doc-mongoose] for more details.
### Serving custom resources
Use `yarm.resource` to define resources with custom handlers.
```javascript
var app = require("express")(),
var yarm = require("yarm");
yarm.resource("greeting")
.get(function(req, cb) {
cb(null, { hello: "world" });
})
.sub("french")
.get(function(req, cb) {
cb(null, { bonjour: "tout le monde" });
});
yarm.resource("greeting/pirate")
.get(function(req, cb) {
cb(null, { arrrrr: "arrrrrr" });
});
app.use("/rest", yarm());
app.listen(80);
```
```
$ curl http://localhost/rest/greeting
{
"hello": "world"
}
$ curl http://localhost/rest/greeting/french
{
"bonjour": "tout le monde"
}
$ curl http://localhost/rest/greeting/pirate
{
"arrrrr": "arrrrrr"
}
```
Head on to the documentation on [Custom resources][doc-custom] for more details.
### Extending served resources
yarm allows adding and replacing handlers for any resource or sub-resource. This enables restricting or extending the behaviour of the default native and mongoose resource handlers, as well as defining very complex custom resource hierarchies.
```javascript
yarm.<whatever>()
.get(function(req, cb) {
// Override GET handler here
});
function notAllowed(req, cb) {
cb(null, "Nope, sorry :(");
}
yarm.native("readonly", myObject)
.put(notAllowed)
.post(notAllowed)
.del(notAllowed)
.sub("*")
.put(notAllowed)
.post(notAllowed)
.del(notAllowed);
yarm.resource("already/defined/path")
.get(function(req, cb) {
// Will not alter 'already' nor 'already/defined' handlers,
// nor those for 'already/defined/other' if they are defined
});
```
Head on to the documentation on [Extending resources][doc-extend] for more details.
## Contributing
yarm is published under the terms of the MIT license. Feel free to report bugs or send pull requests.
[doc]: http://yarm.njoyard.fr
[doc-native]: http://yarm.njoyard.fr/doc-native-resources.html
[doc-mongoose]: http://yarm.njoyard.fr/doc-mongoose-resources.html
[doc-custom]: http://yarm.njoyard.fr/doc-custom-resources.html
[doc-extend]: http://yarm.njoyard.fr/doc-extending-resources.html
## Table of contents
- [Installation](#installation)
- [Usage](#usage)
- [Basics](#basics)
- [Serving native javascript resources](#serving-native-javascript-resources)
- [Serving mongoose models and aggregates](#serving-mongoose-models-and-aggregates)
- [Serving custom resources](#serving-custom-resources)
- [Extending served resources](#extending-served-resources)
- [Native resources](#native-resources)
- [Definition](#definition)
- [Modification](#modification)
- [DELETE](#delete)
- [PUT](#put)
- [PATCH](#patch)
- [POST](#post)
- [Options](#options)
- [Mongoose resources](#mongoose-resources)
- [Definition](#definition-1)
- [Serving collections](#serving-collections)
- [GET: retrieving multiple documents](#get-retrieving-multiple-documents)
- [Getting specific parts of a collection](#getting-specific-parts-of-a-collection)
- [Searching for documents](#searching-for-documents)
- [Using a custom query](#using-a-custom-query)
- [Sorting collections](#sorting-collections)
- [POST: adding new documents](#post-adding-new-documents)
- [Serving documents](#serving-documents)
- [GET: retrieving single documents](#get-retrieving-single-documents)
- [DELETE: removing documents](#delete-removing-documents)
- [PUT and PATCH: updating documents](#put-and-patch-updating-documents)
- [Serving document properties](#serving-document-properties)
- [GET: retrieving document properties](#get-retrieving-document-properties)
- [DELETE: removing document properties](#delete-removing-document-properties)
- [PUT and PATCH: updating document properties](#put-and-patch-updating-document-properties)
- [POST: adding sub-documents to document arrays](#post-adding-sub-documents-to-document-arrays)
- [Custom resources](#custom-resources)
- [Extending resources](#extending-resources)
- [Overriding handlers](#overriding-handlers)
- [Sub-resources](#sub-resources)
- [Setting options](#setting-options)
- [Hooks](#hooks)
- [Extending yarm](#extending-yarm)
- [Using multiple instances](#using-multiple-instances)
## Installation
Use npm to install yarm, or add yarm to your package.json dependencies.
```
$ npm install yarm
```
yarm has no dependencies, however it is intended to be used with Express and will have additional features if mongoose is present.
## Usage
### Basics
Use yarm as any other Express middleware.
```javascript
var app = require("express")();
var yarm = require("yarm");
app.use("/rest", yarm());
app.listen(80);
```
### Serving native javascript resources
Use `yarm.native()` to serve native Javascript objects or arrays.
```javascript
var app = require("express")();
var yarm = require("yarm");
app.use("/rest", yarm());
yarm.native("me", {
name: "Alice",
age: 30
});
yarm.native("friends", [
"Bob",
"Charlie"
]);
app.listen(80);
```
```
$ curl http://localhost/rest/me
{
"name": "Alice",
"age": 30
}
$ curl http://localhost/rest/me/name
Alice
$ curl http://localhost/rest/friends
{
"_count": 2,
"_items": [ "Bob", "Charlie" ]
}
$ curl http://localhost/rest/friends/1
Charlie
```
Head on to the [Native resources](#native-resources) chapter for more details.
### Serving mongoose models and aggregates
When mongoose is available, you can use `yarm.mongoose()` to serve models.
```javascript
var app = require("express")();
var yarm = require("yarm");
var mongoose = require("mongoose");
app.use("/rest", yarm());
var postSchema = new mongoose.Schema({
title: String,
text: String,
comments: [{
author: String,
text: String
}]
});
var Post = mongoose.model("post", postSchema);
yarm.mongoose("posts", Post);
app.listen(80);
```
```
$ curl http://localhost/rest/posts?skip=10&limit=1
{
"_count": 42,
"_items": [
{
"_id": "507f191e810c19729de860ea",
"title": "My 11th post",
"text": "Hello, World",
"comments": [
{
"author": "Bob",
"text": "First !"
}
]
}
]
}
$ curl http://localhost/rest/posts/507f191e810c19729de860ea
{
"_id": "507f191e810c19729de860ea",
"title": "My 11th post",
"text": "Hello, World",
"comments": [
{
"author": "Bob",
"text": "First !"
}
]
}
$ curl http://localhost/rest/posts/507f191e810c19729de860ea/comments/0/text
First !
```
Head on to the [Mongoose resources](#mongoose-resources) chapter for more details.
### Serving custom resources
Use `yarm.resource` to define resources with custom handlers.
```javascript
var app = require("express")(),
var yarm = require("yarm");
yarm.resource("greeting")
.get(function(req, cb) {
cb(null, { hello: "world" });
})
.sub("french")
.get(function(req, cb) {
cb(null, { bonjour: "tout le monde" });
});
yarm.resource("greeting/pirate")
.get(function(req, cb) {
cb(null, { arrrrr: "arrrrrr" });
});
app.use("/rest", yarm());
app.listen(80);
```
```
$ curl http://localhost/rest/greeting
{
"hello": "world"
}
$ curl http://localhost/rest/greeting/french
{
"bonjour": "tout le monde"
}
$ curl http://localhost/rest/greeting/pirate
{
"arrrrr": "arrrrrr"
}
```
Head on to the [Custom resources](#custom-resources) chapter for more details.
### Extending served resources
yarm allows adding and replacing handlers for any resource or sub-resource. This enables restricting or extending the behaviour of the default native and mongoose resource handlers, as well as defining very complex custom resource hierarchies.
```javascript
yarm.<whatever>()
.get(function(req, cb) {
// Override GET handler here
});
function notAllowed(req, cb) {
cb(null, "Nope, sorry :(");
}
yarm.native("readonly", myObject)
.put(notAllowed)
.post(notAllowed)
.del(notAllowed)
.sub("*")
.put(notAllowed)
.post(notAllowed)
.del(notAllowed);
yarm.resource("already/defined/path")
.get(function(req, cb) {
// Will not alter 'already' nor 'already/defined' handlers,
// nor those for 'already/defined/other' if they are defined
});
```
Head on to the [Extending resources](#extending-resources) chapter for more details.
## Native resources
### Definition
The `yarm.native()` helper allows serving plain Javascript objects and arrays. Served object and arrays will allow access to any property path, including array indices).
```javascript
yarm.native("object", {
foo: "bar",
sub: {
array: [1, 2, 3, 4, 5],
property: "baz"
}
});
```
```
$ curl http://localhost/rest/object
{
"foo": "bar",
"sub": {
"array": [1, 2, 3, 4, 5],
"property": "baz"
}
}
$ curl http://localhost/rest/object/sub/property
baz
$ curl http://localhost/rest/object/sub/array/2
3
```
Arrays are served as collections, i.e. yarm will respond with a JSON object containing the total item count and a subset of the array items.
```
$ curl http://localhost/rest/object/sub/array
{
"_count": 5,
"_items": [1, 2, 3, 4, 5]
}
```
By default, yarm returns at most 10 items in collection responses. You can change this default by passing a `defaultLimit` option to the middleware.
```javascript
app.use(yarm({ defaultLimit: 100 }));
```
Clients can also specify an offset and limit when requesting collections. The requested limit will override the default value, and requesting a limit of 0 will make yarm return all items from the collection, starting at the specified offset. In any case, the "_count" property will always return the total item count in the collection.
```
$ curl http://localhost/rest/object/sub/array?limit=1
{
"_count": 5,
"_items": [1]
}
$ curl http://localhost/rest/object/sub/array?skip=2&limit=0
{
"_count": 5,
"_items": [3, 4, 5]
}
```
### Modification
Native yarm resources can be modified using PUT, PATCH, POST and DELETE HTTP methods.
Note that the examples below assume you have set up middleware to parse JSON request bodies (such as `express.json()` or `express.bodyParser()`).
#### DELETE
The DELETE method allows removing object properties or array items.
```
$ curl -X DELETE http://localhost/rest/object/sub/array/2
$ curl http://localhost/rest/object/sub
{
"array": [1, 2, 4, 5],
"property": "baz"
}
$ curl -X DELETE http://localhost/rest/object/sub/property
$ curl http://localhost/rest/object/sub
{
"array": [1, 2, 3, 4, 5]
}
```
Note that clients cannot DELETE the root resource itself.
```
$ curl -i -X DELETE http://localhost/rest/object
HTTP/1.1 405 Method not allowed
```
#### PUT
The PUT method allows replacing object properties or array items.
```
$ curl -X PUT -d '{ "newArray": [1, 2, 3] }' http://localhost/rest/object/sub
$ curl http://localhost/rest/object/sub
{
"newArray": [1, 2, 3]
}
```
If a `_value` key is present in the request body, its value will be used instead. This allows passing values that are not valid JSON (eg. strings, numbers or booleans).
```
$ curl -X PUT -d '{ "_value": "foo" }' \
http://localhost/rest/object/sub/newArray/0
$ curl http://localhost/rest/object/sub
{
"newArray": ["foo", 2, 3]
}
```
As with the DELETE method, clients cannot PUT the root resource itself.
```
$ curl -i -X PUT -d '{}' http://localhost/rest/object
HTTP/1.1 405 Method not allowed
```
#### PATCH
The PATCH method allows adding and changing properties in an object.
```
$ curl -X PATCH -d '{"foo":"bar"}' http://localhost/rest/object/sub
$ curl http://localhost/rest/object/sub
{
"newArray": ["foo", 2, 3],
"foo": "bar"
}
$ curl -X PATCH -d '{"newArray":[],"num":42}' http://localhost/rest/object/sub
$ curl http://localhost/rest/object/sub
{
"newArray": [],
"foo": "bar",
"num": 42
}
```
The PATCH method is only available on object sub-resources. Attempting to PATCH the root resource or a non-object sub-resource will result in a "405 Method not allowed" response.
#### POST
The POST method allows adding items to arrays or properties to objects.
When adding items to arrays, as with the PUT method, the `_value` key in the request body will be used when it is present.
```
$ curl -X POST -d '{"name":"Alice"}' http://localhost/rest/object/sub/newArray
$ curl http://localhost/rest/object/sub/newArray
{
"_count": 1,
"_items": [
{ "name": "Alice"}
]
}
$ curl -X POST -d '{"_value":"Bob"}' http://localhost/rest/object/sub/newArray
$ curl http://localhost/rest/object/sub/newArray
{
"_count": 2,
"_items": [
{ "name": "Alice" },
"Bob"
]
}
```
When adding properties to objects, both a `_key` and a `_value` keys must be present in the request body or yarm will respond with "400 Bad request".
```
$ curl -X POST -d '{"_key":"age","_value":30}' \
http://localhost/rest/object/sub/newArray/0
$ curl http://localhost/rest/object/sub/newArray
{
"_count": 2,
"_items": [
{
"name": "Alice",
"age": 30
},
"Bob"
]
}
```
### Options
As with any other yarm resource, you can set options by using `resource.set(option, value)`.
By default, options apply both to the resource and to all sub-resources, but you can prevent the option to apply to sub-resources with `resource.set(option, value, true)`. You can also set options only on sub-resources using `resource.sub("path/to/subresource").set(...)`. For more information on how options work, see [Setting options](#setting-options).
The following options are supported by native resources:
* `rawArrays` (default `false`): when `true`, serve arrays as is instead of collections. The whole array content will be returned to clients, instead of an object with `_count` and `_items` keys. Note that clients cannot use `skip` or `limit` request parameters on raw arrays.
```javascript
yarm.native("array", [1, 2, 3])
.set("rawArrays", true);
```
```
$ curl http://localhost/rest/array
[1, 2, 3]
$ curl http://localhost/rest/array?skip=1&limit=1
[1, 2, 3]
```
* `objectCollections` (default `false`): when `true`, serve objects as collections of their keys. Properties can still be accessed the same way.
```javascript
yarm.native("object", {
"foo": "bar",
"sub": {
"array": [1, 2, 3, 4, 5],
"property": "baz"
}
}).set("objectCollections", true);
```
```
$ curl http://localhost/rest/object
{
"_count": 2,
"_items": [ "foo", "sub" ]
}
$ curl http://localhost/rest/object/sub/property
baz
```
* `sparseArrays` (default `false`): when `true`, DELETE requests on array items will leave an `undefined` hole in the array instead of splicing the array.
```javascript
yarm.native("array", [1, 2, 3])
.set("sparseArrays", true);
```
```
$ curl -X DELETE http://localhost/rest/array/1
$ curl http://localhost/rest/array
{
"_count": 3,
"_items": [1, undefined, 3]
}
```
* `postResponse` (default `false`): when `true`, responses to POST requests will include the POSTed entity instead of being empty HTTP 201 Created responses.
## Mongoose resources
### Definition
When mongoose is present, you can use `yarm.mongoose` to serve models as resources.
```javascript
var Post = mongoose.model(PostSchema)
yarm.mongoose("posts", Post);
```
You can also use `yarm.aggregate` to serve aggregates as resources. Contrary to models, aggregates are read-only: only GET requests are supported on aggregate (and sub-property) URLs.
```javascript
var Post = mongoose.model(PostSchema)
// MongoDB aggregate pipeline
var pipeline = [
{ $project : {
author : 1,
tags : 1,
} },
{ $unwind : "$tags" },
{ $group : {
_id : "$tags",
authors : { $addToSet : "$author" }
} }
];
yarm.aggregate("authorsByTag", Post, pipeline);
```
### Serving collections
#### GET: retrieving multiple documents
Models and aggregates are served as collections, i.e. yarm will respond with a JSON object containing the total item count and a subset of the collection items.
```
$ curl http://localhost/rest/posts
{
"_count": 50,
"_items": [
{
"_id": "507f191e810c19729de860ea",
"title": "My first post",
"text": "Hello, World"
},
{
"_id": "507f191e810c19729de62fc7",
"title": "My first post",
"text": "Hello again, World"
}
...
]
}
```
##### Getting specific parts of a collection
By default, yarm returns at most 10 items in collection responses. You can change this default by passing a `defaultLimit` option to the middleware.
```javascript
app.use(yarm({ defaultLimit: 100 }));
```
Clients can also specify an offset and limit when requesting collections. The requested limit will override the default value, and requesting a limit of 0 will make yarm return all items from the collection, starting at the specified offset. In any case, the "_count" property will always return the total item count in the collection.
```
$ curl http://localhost/rest/posts?limit=1
{
"_count": 50,
"_items": [
{
"_id": "507f191e810c19729de860ea",
"title": "My first post",
"text": "Hello, World"
}
]
}
$ curl http://localhost/rest/posts?skip=49&limit=0
{
"_count": 50,
"_items": [
{
"_id": "507f191e810c19729d5362b8",
"title": "My 50th post",
"text": "This is getting boring..."
}
]
}
```
##### Searching for documents
Clients can request documents matching a specific query in a collection using the `query` request parameter. Here are a few examples overviewing what you can do with queries.
```
# All posts with title equal to "First post"
curl http://localhost/rest/posts?query=title:First Post
# All posts not written by me
curl http://localhost/rest/posts?query=author!me
# All posts matching the regexp /post/ (make sure the client URL-encodes the
# query parameter)
curl http://localhost/rest/posts?query=title:/post/
# All posts not written by the nsa
curl http://localhost/rest/posts?query=author!/\.nsa\.gov$/
# Regexps can be made case-insensitive
curl http://localhost/rest/posts?query=title:/post/i
# Logical expression, AND operators have priority over OR operators
curl http://localhost/rest/posts?query=title:/post/ OR text:/hello/i AND isPublic:1
```
Clients can mix document queries with the `skip` and `limit` parameters. The `_count` property in the returned object will always be the total number of documents matching the query in the collection.
##### Using a custom query
When serving model resources, you can alter the query used to retrieve documents by setting the `query` option on a model resource.
```javascript
yarm.mongoose("posts", Post)
.set("query", function() {
return Post.find({ isPublic: true });
});
```
Aggregate resources don't support custom queries, as you can already customize the aggregation pipeline.
##### Sorting collections
When serving model resources, you could use a custom query to sort collections, but you may prefer using the `sort` option instead.
```javascript
// Instead of using this:
yarm.mongoose("posts", Post)
.set("query", function() {
return Post.find({ isPublic: true }).sort({ date: -1 });
});
// It is easier to use the sort option
yarm.mongoose("posts", Post)
.set("query", function() {
return Post.find({ isPublic: true });
})
.set("sort", { date: -1 });
```
Aggregate resources don't support a `sort` option, as you can already sort documents in the aggregation pipeline.
#### POST: adding new documents
Clients can POST new documents to model collections.
```
$ curl -X POST -d '{"title":"New Post","text":"Whatever..."}' \
http://localhost/rest/posts
$ curl http://localhost/rest/posts?query=title:New Post
{
"_count": 1,
"_items": [
{
"_id": "507f191e810c1972967fd7c3",
"title": "New Post",
"text": "Whatever..."
}
]
}
```
By default, a "201 Created" HTTP response is sent to the client when POSTing new documents. This behaviour can be changed by setting the `postResponse` option to a truthy value; in this case, the created document will be returned to the client.
### Serving documents
#### GET: retrieving single documents
By default, collection documents are accessible by adding the document ObjectID value to the collection URL.
```
$ curl http://localhost/rest/posts/507f191e810c19729de860ea
{
"_id": "507f191e810c19729de860ea",
"title": "My first post",
"text": "Hello, World"
}
```
If your documents have a more user-friendly identifier property, you can use the `key` option to tell `yarm.mongoose` to use this property instead.
Note that this option is not available for aggregate resources as the aggregation pipeline already allows you to map the `_id` property to whatever value you want.
```javascript
yarm.mongoose("posts", Post)
.set("key", "postId");
```
```
$ curl http://localhost/rest/posts/my-first-post
{
"_id": "507f191e810c19729de860ea",
"postId": "my-first-post",
"title": "My first post",
"text": "Hello, World"
}
```
You can change the way yarm returns documents by using mongoose toObject option. This option can be set on the model resource directly. Refer to the [mongoose documentation][mongoose-toobject] for more information on how this option works.
Again, this opton is not available for aggregate resources, as the aggregation pipeline already allows you to tailor documents the way you want.
```javascript
yarm.mongoose("posts", Post)
.set("toObject", {
// Include virtual properties in output
virtuals: true,
// Hide _id property
toObject: function(doc, ret, options) {
delete ret._id;
}
});
```
When the `toObject` option is set on the model resource, it will apply to responses to both collection requests and document requests. You can specify a different toObject option for sub-resources, refer to [Setting options](#setting-options) for more information.
Before returning documents, yarm adds a `_request` property to them with the Express request object. This allows using the request for example in a virtual property in your model.
#### DELETE: removing documents
Clients can remove documents by sending DELETE requests on the document URL.
```
$ curl -X DELETE http://localhost/rest/posts/my-first-post
$ curl -i http://localhost/rest/posts/my-first-post
HTTP/1.1 404 Not found
```
#### PUT and PATCH: updating documents
Clients can update documents by sending PUT or PATCH requests on the document URL. For now, both methods behave as a PATCH request, that is, they update all fields that are present in the request body, without touching other fields.
```
$ curl -X PATCH -d '{"title":"New title"}' \
http://localhost/rest/posts/507f191e810c19729de860ea
$ curl http://localhost/rest/posts/507f191e810c19729de860ea
{
"_id": "507f191e810c19729de860ea",
"title": "New title",
"text": "Hello, World"
}
```
### Serving document properties
#### GET: retrieving document properties
As with native resources, clients can request any document property (or subproperty).
```
$ curl http://localhost/rest/posts/507f191e810c19729de860ea/title
My first post
$ curl http://localhost/rest/posts/507f191e810c19729de860ea/tags
["homepage", "public", "hello"]
```
When your model schema includes document arrays, they are served as collections. Clients can use the `skip`, `limit` and `query` request parameters with those collections as well.
```javascript
var PostSchema = new mongoose.Schema({
title: String,
text: String,
comments: [{
author: String,
text: String
}]
})
var Post = mongoose.model("posts", PostSchema);
yarm.mongoose("posts", Post);
```
```
$ curl http://localhost/rest/posts/my-post/comments
{
"_count": 3,
"_items": [
{
_id: "507f191e810c19729f526a7",
author: "Alice",
text: "First !"
},
...
]
}
$ curl http://localhost/rest/posts/my-post/comments?query=author:Alice
{
"_count": 1,
"_items": [
{
_id: "507f191e810c19729f526a7",
author: "Alice",
text: "First !"
}
]
}
$ curl http://localhost/rest/posts/my-post/comments/507f191e810c19729f526a7
{
_id: "507f191e810c19729f526a7",
author: "Alice",
text: "First !"
}
$ curl http://localhost/rest/posts/my-post/comments/507f191e810c19729f526a7/text
First !
```
When your model schema contains references to other collections, you may want to adjust the `query` option on the mongoose resource so that mongoose populates those references.
```javascript
var PersonSchema = new mongoose.Schema({ ... });
var Person = mongoose.model("person", PersonSchema);
var CommentSchema = new mongoose.Schema({ ... });
var Comment = mongoose.model("comment", CommentSchema);
var PostSchema = new mongoose.Schema({
author: { type: mongoose.Schema.types.ObjectId, ref: "person" },
comments: [{ type: mongoose.Schema.types.ObjectId, ref: "comment" }]
});
var Post = mongoose.model("post", PostSchema);
yarm.mongoose("posts", Post)
.set("query", function() {
return Post.find().populate("author comments");
});
```
#### DELETE: removing document properties
Clients can remove document properties or sub-properties by sending a DELETE request on the property URL.
```
$ curl -X DELETE http://localhost/posts/my-first-post/comments/507f191e810c19729f526a7
```
#### PUT and PATCH: updating document properties
Clients can update document properties or sub-properties by sending PUT or PATCH requests on the property URL. If the request body contains a `_value` field, it will be used instead. This allows passing values that would otherwise not be valid JSON (strings, numbers, booleans, ...).
```
$ curl -X PATCH -d '{"_value":"New title"}' \
http://localhost/rest/posts/507f191e810c19729de860ea/title
$ curl http://localhost/rest/posts/507f191e810c19729de860ea
{
"_id": "507f191e810c19729de860ea",
"title": "New title",
"text": "Hello, World"
}
```
#### POST: adding sub-documents to document arrays
When your schema contains a document array, clients can add new sub-documents by sending POST requests on the document array URL.
```
$ curl -X POST -d '{"author":"Bob","text":"This is a nice post !"}' \
http://localhost/rest/posts/507f191e810c19729de860ea/comments
```
By default, a "201 Created" HTTP response is sent to the client when POSTing new sub-documents. This behaviour can be changed by setting the `postResponse` option to a truthy value; in this case, the created sub-document will be returned to the client.
## Custom resources
You can define bare resources (that is, resources without any default method handlers) using `yarm.resource()`.
```javascript
var resource = yarm.resource("myResource");
var deepResource = yarm.resource("path/to/deep/resource");
```
The whole point of defining bare resource is to define custom handlers, which is described in the [next chapter](#extending-resources).
## Extending resources
All yarm resources share the same methods and can all be extended the same way, whether you start with a native resource, a mongoose resource, a bare resource or some resource defined using a custom extension. Methods calls on resources can be chained, which is why any function defining a resource (including the built-in helpers) return the resource.
### Overriding handlers
Defining method handlers is just a matter of calling one of the `.get()`, `.put()`, `.post()` or `.delete()` methods on a resource. All those methods expect a handler function as a parameter, and can be chained as they all return the resource.
```javascript
resource
.get(function(req, cb) {
// GET handler
})
.put(function(req, isPatch, cb) {
// PUT and PATCH handler
})
.post(function(req, cb) {
// POST handler
})
.del(function(req, cb) {
// DELETE handler
});
```
yarm always chooses the last defined handler for a resource, which enables overriding method handlers defined by built-in resource definition helpers.
```javascript
yarm.mongoose("posts", Post)
.get(function(req, cb) {
cb(null, "Overriden !");
});
```
```
$ curl http://localhost/rest/posts
Overriden !
```
You can also remove any write handler (POST, PUT, PATCH and DELETE) using the `.readonly()` method on a resource. This is mainly useful for resources defined using helpers (like `yarm.mongose` and `yarm.native`).
All method handlers receive the Express request object as their first parameter (with all facilities enabled by Express or any middleware used before yarm), and a callback as their last parameter. The PUT and PATCH handler receives an additional boolean argument which indicates whether the request is a PATCH request (the handler is common because both methods work in a very similar way).
Calling the callback with an Error object as its first argument will make yarm send a HTTP 500 response, with the error message as the response body.
```javascript
yarm.resource("error")
.get(function(req, cb) {
cb(new Error("Oh noes !"));
});
```
```
$ curl -i http://localhost/rest/error
HTTP/1.1 500 Internal server error
Oh noes !
```
There are several ways to call the callback with a valid response. You can call `cb(null, body[, mimetype]);` to send the response body with an optional mimetype, where `body` can be any of:
* A string
* A Buffer instance
* A readable stream
* A plain object (which will be JSON.stringify-ed by Express)
* `null` or `undefined`, in which case yarm will send a "204 No content" response
The callback also has built-in helpers for other kinds of responses:
* `cb.file(error, filepath[, mimetype])` to send the content of a file (yarm will use Express' `res.sendfile()`)
* `cb.created()` to send a "201 Created" response
* `cb.noContent()` to send a "204 No content" response
* `cb.badRequest()` to send a "400 Bad request" response
* `cb.notFound()` to send a "404 Not found" response
* `cb.methodNotAllowed()` to send a "405 Method not allowed" response
* `cb.notImplemented()` to send a "501 Not implemented" response
* `cb.status(code[, body])` to send a custom HTTP status code and response body.
* `cb.custom(handler)` to use a custom request handler. The handler will receive the request object, the response object and Express' `next` callback as any Express handler.
To serve a resource as a collection, you must call both its `.count()` and `.list()` methods.
```javascript
resource
.count(function(req, cb) {
cb(null, totalCollectionCount);
})
.list(function(req, offset, limit, cb) {
cb(null, collectionItems(offset, limit));
});
```
The handlers for the `.count()` and `.list()` methods work the same way as other handlers, except the list handler receives additional `offset` and `limit` arguments. The offset defaults to 0 if not specified by the client, and the limit defaults to yarm defaultLimit option (default 10). A limit of 0 indicates a request for all items until the end of the collection. The count handler is expected to call the callback with the total item count in the collection, and the limit handler should pass an array of collection items matching the specified offset and limit.
Count/list and get handlers override each other. You can also decide to serve the resource as a collection inside a GET handler by calling `cb.list()`.
```javascript
resource.get(function(req, cb) {
if (req.params("asCollection")) {
cb.list(countHandler, listHandler);
} else {
cb(null, "Not a collection, as requested.");
}
});
```
### Sub-resources
There are several ways of defining handlers on a sub-resource. You can pass the full path to `yarm.resource()`, pass the sub-path to the `.sub()` method of a resource, or chain several `.sub()` calls. The following examples are all equivalent.
```javascript
yarm.resource("path/to/resource")
.get(function(req, cb) {
cb(null, "Hey !");
});
yarm.resource("path/to")
.sub("resource")
.get(function(req, cb) {
cb(null, "Hey !");
});
yarm.resource("path")
.sub("to/resource")
.get(function(req, cb) {
cb(null, "Hey !");
});
yarm.resource("path")
.sub("to")
.sub("resource")
.get(function(req, cb) {
cb(null, "Hey !");
});
```
yarm examines all defined handlers for the requested URL before choosing the last one. To define (or override) a handler on a resource, you can either chain calls from the original resource definition, or restart from scratch with a new `yarm.resource()` call.
```javascript
yarm.mongoose("posts", Post)
.get(function(req, cb) {
cb(null, "GET /posts has been overriden");
})
.sub("subresource")
.get(function(req, cb) {
cb(null, "GET /posts/subresource has been overriden")
});
/* yarm.resource() does not define any handlers, so any other method
handlers will still be present */
yarm.resource("posts")
.get(function(req, cb) {
cb(null, "GET /posts has been overriden again");
});
yarm.resource("posts/subresource")
.get(function(req, cb) {
cb(null, "GET /posts/subresource has been overriden again");
});
yarm.resource("posts")
.sub("subresource")
.get(function(req, cb) {
cb(null, "Yet another GET /posts/subresource override...");
});
```
```
$ curl http://localhost/rest/posts
GET /posts has been overriden again
$ curl http://localhost/rest/posts/subresource
Yet another GET /posts/subresource override...
```
Paths passed to `yarm.resource()` or a resource `.sub()` method can contain parameter matching wildcards and catchall wildcards. They work just the same as Express pattern matchers (except yarm has no support for regexps *yet*) and handlers can access the part of the URL they matched in `req.params`.
```javascript
yarm.resource("/posts/:pid/comments/:cid").get(function(req, cb) {
cb(null, "Comment #" + req.params.cid + " from post " + req.params.pid);
});
yarm.resource("/posts/:pid").sub("comments/:cid/*").get(function(req, cb) {
cb(null, "There's no such thing as " + req.params["*"] + " in that comment!");
});
```
```
$ curl http://localhost/rest/posts/first-post/comments/3
Comment #3 from post first-post
$ curl http://localhost/rest/posts/first-post/comments/3/foo/bar
There's no such thing as foo/bar in that comment!
```
URL parts matched with wildcards are made available in `req.params` for all handlers, including sub-resource handlers, unless the same parameter name is used more than once on the path.
```javascript
yarm.resource("post/:pid")
.get(function(req, cb) {
cb(null, "Post " + req.params.pid);
})
.sub("comments/:cid")
.get(function(req, cb) {
cb(null, "Comment #" + req.params.cid + " from post " + req.params.pid);
});
```
Parameter values are URL-decoded, except for the part matched by the "*" catchall wildcard (it's up to handlers to split its value into path components and URL-decode them).
```javascript
yarm.resource("wildcard/:param")
.get(function(req, cb) {
cb(null, "Parameter: " + req.params.param):
});
yarm.resource("catchall/*")
.get(function(req, cb) {
cb(null, "URL ends with: " + req.params["*"]);
});
```
```
$ curl http://localhost/rest/wildcard/url%20encoded
Parameter: url encoded
$ curl http://localhost/rest/catchall/url%2Fencoded/value
URL ends with: url%2Fencoded/value
```
As stated before, yarm will always choose the last defined handler amongst all resource definitions matching the requested URL. As a consequence, specific handlers (that is, handlers on paths without wildcards) should always be defined last or they will always be overriden by generic handlers (those with wildcards).
```javascript
yarm.resource("a/:param")
.get(function(req, cb) {
cb(null, "A: Generic handler");
});
yarm.resource("a/value")
.get(function(req, cb) {
cb(null, "A: Specific handler");
});
yarm.resource("b/value")
.get(function(req, cb) {
cb(null, "B: Specific handler");
});
yarm.resource("b/:param")
.get(function(req, cb) {
cb(null, "B: Generic handler");
});
```
```
$ curl http://localhost/rest/a/foo
A: Generic handler
$ curl http://localhost/rest/a/value
A: Specific handler
$ curl http://localhost/rest/b/foo
B: Generic handler
$ curl http://localhost/rest/b/value
B: Generic handler
```
As the "*" catchall wildcard matches everything until the end of the URL, calling `.sub()` afterwards will have no effect.
```javascript
yarm.resource("path/to/*")
.get(function(req, cb) {
cb(null, "Catchall handler");
})
.sub("bar")
.get(function(req, cb) {
cb(null, "Forever alone...");
});
```
```
$ curl http://localhost/rest/path/to/foo/bar
Catchall handler
```
### Setting options
You can set options on resources and sub-resources using their `.set()` method. Options set this way are made available to method handlers in `req.options`. yarm allows two kinds of options:
* "Deep" options are set on the resource and all its sub-resources
```javascript
resource("deep")
.get(function(req, cb) {
cb(null, "Option is: " + req.options["an option"])
})
.sub("subresource")
.get(function(req, cb) {
cb(null, "Option is: " + req.options["an option"])
});
resource("deep").set("an option", "a value");
```
```
$ curl http://localhost/rest/deep
Option is: a value
$ curl http://localhost/rest/deep/subresource
Option is: a value
```
* "Strict" options are set only on the resource, and not passed to its sub-resources
```javascript
resource("strict")
.get(function(req, cb) {
cb(null, "Option is: " + req.options["an option"])
})
.sub("subresource")
.get(function(req, cb) {
cb(null, "Option is: " + req.options["an option"])
});
resource("strict").set("an option", "a value", true);
```
```
$ curl http://localhost/rest/strict
Option is: a value
$ curl http://localhost/rest/strict/subresource
Option is: undefined
```
Setting options on sub-resource override those with the same name on parent resources.
```javascript
yarm.resource("option")
.get(function(req, cb) {
cb(null, "Option is: " + req.options["an option"])
})
.sub("subresource")
.get(function(req, cb) {
cb(null, "Option is: " + req.options["an option"])
});
yarm.resource("option")
.set("an option", "a value");
yarm.resource("option/subresource")
.set("an option", "an other value")
```
```
$ curl http://localhost/rest/option
Option is: a value
$ curl http://localhost/rest/option/subresource
Option is: an other value
```
### Hooks
Hooks on yarm resources provides a way to add handlers that will be called before any method handler. This enables altering the request object for use by the actual method handlers.
```javascript
yarm.resource("hooked")
.hook(function(req, next) {
req.hookCalled = true;
next();
})
.get(function(req, cb) {
cb(null, req.hookCalled ? "Hook has been called !" : "This does not work");
})
.post(function(req, cb) {
cb(req.hookCalled ? null : new Error("Hook has not been called !"));
});
```
```
$ curl http://localhost/rest/hooked
Hook has been called !
$ curl -i -X POST http://localhost/rest/hooked
HTTP/1.1 204 No content
```
Every hook receives the Express request object and a `next` callback that must be called in order to allow yarm to continue processing the request. Hooks can also halt the handling of the request:
* Passing an Error object to `next()` will make yarm send a "500 Internal server error" response with the error message as the request body
* Calling `next.noContent()` will send a "204 No content" response
* Calling `next.badRequest()` will send a "400 Bad request" response
* Calling `next.notFound()` will send a "404 Not found" response
* Calling `next.methodNotAllowed()` will send a "405 Method not allowed" response
* Calling `next.notImplemented()` will send a "501 Not implemented" response
* Calling `next.status(code[, body])` will send a custom HTTP status code and response body.
Hooks also have access to URL wildcard values (in `req.params`) and resource options (in `req.options`). yarm actually implements setting those objects using hooks.
Hooks are different from method handlers, in that all hooks defined on a resource path will be called when the resource is requested, including those defined on parent paths. On a given path, hooks are called in the order they were defined.
For example, given the following resource definition:
```javascript
yarm.resource("hooks")
.hook(rootHook1)
.hook(rootHook2)
.get(rootGet)
.sub("subresource")
.hook(subHook1)
.hook(subHook2)
.get(subGet);
```
a GET request on "hooks" will call `rootHook1`, `rootHook2`, and then `rootGet`. A GET request on "hooks/subresource" will call `rootHook1`, `rootHook2`, `subHook1`, `subHook2` and finally `subGet`.
This scheme is very useful when working with nested resources, as hooks on a given level can prepare objects for the next level to work with, storing them in the request object. For example, you could define a tree of resources to access a database with something like this:
```javascript
yarm.resource("db/:database")
.hook(function(req, next) {
// Connect to DB and store connection in the request object
dbDriver.connect(req.params.database, function(err, connection) {
if (err) {
next(err);
} else {
req.connection = connection;
next();
}
});
})
.get(function(req, cb) {
cb(null, req.connection.getDatabaseInfo());
})
.post(function(req, cb) {
req.connection.createTable(req.body.tableName, function(err) {
cb(err);
});
});
yarm.resource("db/:database/tables/:table")
.hook(function(req, next) {
// Get a handle on the table an store it in the request object
req.connection.getTable(req.params.table, function(err, table) {
if (err) {
next(err);
} else {
req.table = table;
next();
}
});
})
.get(function(req, cb) {
cb(null, req.table.getTableInfo());
})
.del(function(req, cb) {
req.connection.removeTable(req.table.getTableName(), function(err) {
cb(err);
});
})
.post(function(req, cb) {
req.table.addRow(req.body, function(err) {
cb(err);
});
});
yarm.resource("db/:database/tables/:table/rows/:rowid")
.hook(function(req, next) {
// Get the row and store it in the request object
req.table.fetchRow(req.params.rowid, function(err, next) {
if (err) {
next.notFound();
} else {
req.row = row;
next();
}
});
})
.get(function(req, cb) {
cb(null, req.row.getJSONData());
})
.put(function(req, isPatch, cb) {
(isPatch ? req.row.update : req.row.replace)(req.body, function(err) {
cb(err);
});
})
.del(function(req, cb) {
req.table.deleteRow(row.getRowID(), function(err) {
cb(err);
});
});
```
### Helpers
yarm adds the following helpers to the Express request object, that are available both in hooks and in method handlers:
* `req.getHref([path])` returns the URL of the requested resource, optionnaly adding `path` to the end.
```javascript
yarm.resource("path/to/resource")
.get(function(req, cb) {
cb(null, {
withoutPath: req.getHref(),
withPath: req.getHref("sub/resource")
});
});
```
```
$ curl http://localhost/rest/path/to/resource
{
"withoutPath": "http://localhost/rest/path/to/resource",
"withPath": "http://localhost/rest/path/to/resource/sub/resource"
}
```
* `req.match(pattern, path)` matches `path` to `pattern` and returns the match. `pattern` should be a path pattern with optional parameter or catchall wildcards. When `path` matches, it returns an object with all matched wildcard values, or `false` otherwise.
```javascript
yarm.resource("path/to/resource")
.get(function(req, cb) {
cb(null, {
wildcards: {
param: req.match("foo/:p1/baz/:p2", "foo/bar/baz/42"),
wildcard: req.match("foo/:p1/baz/*", "foo/bar/baz/42/bing"),
noMatch: req.match("foo/:p1/baz/*", "path/to/resource")
},
noWildcards: {
match: req.match("path/to/resource", "path/to/resource"),
noMatch: req.match("path/to/resource", "foo/bar")
}
});
});
```
```
$ curl http://localhost/rest/path/to/resource
{
"wildcards": {
"param": { "p1": "bar", "p2": "42" },
"catchall": { "p1": "bar", "*": "42/bing" },
"noMatch": false
},
"noWildcards": {
"match": {},
"noMatch": false
}
}
```
## Extending yarm
You can add new resource definition helpers to yarm with `yarm.extend()`, and the built-in native and mongoose helpers are actually defined this way. This is very useful when you use the same kind of resource customization on several resources.
```javascript
yarm.extend("onlyOneProperty", function(path, object, property) {
// Use this.sub(...) to define "root" resources
var resource = this.sub(path)
.get(function(req, cb) {
cb(null, object[property]);
});
// Remember to return the resource to enable method chaining
return resource;
});
yarm.onlyOneProperty("path/to/object", { "rest": "Hey !" }, "rest");
app.use("/rest", yarm());
```
```
$ curl http://localhost/rest/path/to/object
Hey !
```
## Using multiple instances
If you use the yarm middleware on multiple routes (or multiple servers), by default all resources will be shared.
```javascript
app.use("/rest1", yarm());
app.use("/rest2", yarm());
yarm.resource("hello")
.get(function(req, cb) {
cb(null, "Hello, world");
});
```
```
$ curl http://localhost/rest1/hello
Hello, world
$ curl http://localhost/rest2/hello
Hello, world
```
If you want separate resources, you can create a separate yarm instance with `yarm.newInstance()`.
```javascript
var yarm = require("yarm");
var yarmFR = yarm.newInstance();
app.use("/rest", yarm);
app.use("/rest-fr", yarmFR);
yarm.resource("hello")
.get(function(req, cb) {
cb(null, "Hello, world");
});
yarmFR.resource("bonjour")
.get(function(req, cb) {
cb(null, "Bonjour, tout le monde");
});
```
```
$ curl http://localhost/rest/hello
Hello, world
$ curl -i http://localhost/rest/bonjour
HTTP/1.1 404 Not found
$ curl -i http://localhost/rest-fr/hello
HTTP/1.1 404 Not found
$ curl http://localhost/rest-fr/bonjour
Bonjour, tout le monde
```
[mongoose-toobject]: http://mongoosejs.com/docs/api.html#document_Document-toObject
\ No newline at end of file
/*jshint node:true */
module.exports = require("./lib");
\ No newline at end of file
/*jshint node:true */
"use strict";
var data = {
created: { code: 201, body: "Created" },
noContent: { code: 204, body: "" },
badRequest: { code: 400, body: "Bad request" },
unauthorized: {code: 401, body: "Unauthorized"},
notFound: { code: 404, body: "Not found" },
methodNotAllowed: { code: 405, body: "Method not allowed" },
notImplemented: { code: 501, body: "Not implemented"}
};
function HTTPStatus(code, message) {
var err = new Error(message);
err.code = code;
return err;
}
HTTPStatus.names = Object.keys(data);
HTTPStatus.names.forEach(function(key) {
HTTPStatus[key] = HTTPStatus.bind(null, data[key].code, data[key].body);
});
module.exports = HTTPStatus;
\ No newline at end of file
/*jshint node:true */
"use strict";
var yarm = require("./yarm");
var hasMongoose = false;
try {
require("mongoose");
hasMongoose = true;
} catch(e) {}
function instanciate() {
var instance = yarm();
/* Add native extension */
instance.extend("native", require("./native"));
/* Add mongoose extensions if mongoose is present */
if (hasMongoose) {
instance.extend("mongoose", require("./mongoose/model"));
instance.extend("aggregate", require("./mongoose/aggregate"));
}
return instance;
}
module.exports = instanciate();
module.exports.newInstance = instanciate;
/*jshint node:true*/
"use strict";
var queryHelpers = require("./query"),
mongodb = require("mongodb"),
ObjectId = mongodb.ObjectID;
/*!
* Misc helpers
*/
/* Create an aggregate pipeline from the base pipeline, the request query if
any, and contextual additions */
function createPipeline(req, pipeline, additions) {
pipeline = pipeline.slice(0);
if (req && req.query["query"]) {
pipeline.push({ $match: queryHelpers.create(req.query["query"]) });
}
for (var i = 0, len = additions.length; i < len; i++) {
pipeline.push(additions[i]);
}
return pipeline;
}
/*!
* Aggregate collection helpers
*/
function aggregateCollCount(req, cb) {
var model = req.mongoose.model;
var pipeline = req.mongoose.pipeline;
var args = createPipeline(req, pipeline, [
{ $group: { _id: 0, count: { $sum: 1 } } }
]);
args.push(function(err, result) {
if (err) {
cb(err);
} else {
cb(null, result.length ? result[0].count : 0);
}
});
model.aggregate.apply(model, args);
}
function aggregateCollList(req, offset, limit, cb) {
var model = req.mongoose.model;
var pipeline = req.mongoose.pipeline;
var additions = [{ $skip: offset }];
if (limit > 0) {
additions.push({ $limit: limit });
}
var args = createPipeline(req, pipeline, additions);
args.push(function(err, items) {
if (err) {
cb(err);
} else {
cb(null, items);
}
});
model.aggregate.apply(model, args);
}
/*!
* Aggregated document helpers
*/
function aggregateDocHook(req, next) {
var oid, match;
var id = req.params.projectedId;
var pipeline = req.mongoose.pipeline;
var model = req.mongoose.model;
try {
oid = new ObjectId(id);
match = { $or: [{ "_id": id }, { "_id": oid }] };
} catch(e) {
// Invalid ObjectID
match = { "_id": id };
}
var args = createPipeline(null, pipeline, [
{ $match: match },
{ $limit: 1 }
]);
args.push(function(err, result) {
if (err) {
next(err);
} else if (result.length) {
req.mongoose.item = result[0];
next();
} else {
next();
}
});
model.aggregate.apply(model, args);
}
function aggregateDocGet(req, cb) {
if (!("item" in req.mongoose)) {
return cb.notFound();
}
cb(null, req.mongoose.item);
}
/*!
* Aggregate resource definition helper
*/
function aggregateResource(name, Model, pipeline) {
var collResource = this.sub(name)
.hook(function aggregateHook(req, next) {
req.mongoose = { model: Model, pipeline: pipeline };
next();
})
.count(aggregateCollCount)
.list(aggregateCollList);
collResource.sub(":projectedId")
.hook(aggregateDocHook)
.get(aggregateDocGet);
return collResource;
}
module.exports = aggregateResource;
/*jshint node:true*/
"use strict";
var queryHelpers = require("./query"),
mongoose = require("mongoose"),
CastError = mongoose.SchemaType.CastError;
/*!
* Misc helpers
*/
function getObject(req, item) {
if (typeof item.toObject === "function") {
item._request = req;
return item.toObject(req.options.toObject);
} else {
return item;
}
}
/*!
* Document resource handlers
*/
function mongooseCollCount(req, cb) {
var query = req.options.query();
if (req.query["query"]) {
// Cache query operator
if (!req._queryOperator) {
req._queryOperator = queryHelpers.create(req.query["query"]);
}
query = query.find(req._queryOperator);
}
query.count(function(err, count) { cb(err, count); });
}
function mongooseCollList(req, offset, limit, cb) {
var options = req.options;
var query = options.query();
if (req.query["query"]) {
// Cache query operator
if (!req._queryOperator) {
req._queryOperator = queryHelpers.create(req.query["query"]);
}
query = query.find(req._queryOperator);
}
query = query.skip(offset).limit(limit);
if (req.query["sort"]) {
query = query.sort(req.query["sort"]);
} else if (options.sort) {
query = query.sort(options.sort);
}
return query.exec(function(err, items) {
if (err) {
cb(err);
} else {
cb(null, items.map(function(item) {
var obj = getObject(req, item);
return obj;
}));
}
});
}
function mongooseCollPost(req, cb) {
var model = req.mongoose.model;
model.create(req.body, function(err, doc) {
if (err) {
cb(err);
} else {
if (req.options.postResponse) {
cb(null, getObject(req, doc));
} else {
cb.created();
}
}
});
}
/*!
* Document resource handlers
*/
function mongooseDocHook(req, next) {
var options = req.options;
var crit = {};
crit[options.key] = req.params.id;
req.mongoose.path += "/" + req.params.id;
options.query().find(crit).findOne(function(err, item) {
if (err instanceof CastError) {
// id is not valid, just continue without saving item
return next();
}
if (err) {
return next(err);
}
req.mongoose.doc = item;
next();
});
}
function mongooseDocGet(req, cb) {
if (req.mongoose.doc) {
cb(null, getObject(req, req.mongoose.doc));
} else {
cb.notFound();
}
}
function mongooseDocPut(req, isPatch, cb) {
var doc = req.mongoose.doc;
if (!doc) {
return cb.notFound();
}
doc.set(req.body);
doc.save(function(err) {
cb(err);
});
}
function mongooseDocDel(req, cb) {
if (!req.mongoose.doc) {
return cb.notFound();
}
req.mongoose.doc.remove(function(err) {
cb(err);
});
}
/*!
* Document path resource handlers
*/
function mongoosePathHook(req, next) {
var doc = req.mongoose.doc;
var docpath = req.mongoose.path;
var subkeys = req.options.subkeys;
if (!doc) {
// We have no doc in the first place, don't try to find member
return next();
}
var path = req.params["*"];
var parts = path.split("/");
var fullpath = docpath;
var current = doc;
var parent = doc;
var link = {};
while(parts.length > 0) {
var part = parts.shift();
fullpath += "/" + part;
var decoded = decodeURIComponent(part);
if (current.isMongooseDocumentArray) {
parent = current;
var key = "_id";
if (subkeys) {
if (typeof subkeys === "string") {
key = subkeys;
} else {
Object.keys(subkeys).forEach(function(pattern) {
if (req.match(pattern, fullpath)) {
key = subkeys[pattern];
}
});
}
}
if (key !== "_id") {
current = current.filter(function(item) {
return item[key] === decoded;
})[0];
link = { id: current._id };
} else {
current = current.id(decoded);
link = { id: decoded };
}
} else {
if ("field" in link) {
link.field += "." + decoded;
} else {
parent = current;
link = { field: decoded };
}
current = parent.get(link.field);
}
if (!current) {
return next();
}
}
req.mongoose.parent = parent;
req.mongoose.item = current;
req.mongoose.link = link;
next();
}
function mongoosePathGet(req, cb) {
if (!("item" in req.mongoose)) {
return cb.notFound();
}
var item = req.mongoose.item;
if (item.isMongooseDocumentArray) {
cb.list(mongooseDocArrayCount, mongooseDocArrayList);
} else {
cb(null, getObject(req, item));
}
}
function mongoosePathPut(req, isPatch, cb) {
if (!("item" in req.mongoose)) {
return cb.notFound();
}
var parent = req.mongoose.parent;
var link = req.mongoose.link;
var doc = req.mongoose.doc;
var value = req.body;
if ("_value" in value) {
value = value._value;
}
if ("id" in link) {
parent.id(link.id).set(value);
} else if ("field" in link) {
parent.set(link.field, value);
} else {
return cb(new Error("Unknown link type"));
}
doc.save(function(err) {
cb(err);
});
}
function mongoosePathDel(req, cb) {
if (!("item" in req.mongoose)) {
return cb.notFound();
}
var parent = req.mongoose.parent;
var link = req.mongoose.link;
var doc = req.mongoose.doc;
if ("id" in link) {
parent.splice(parent.indexOf(parent.id(link.id)), 1);
} else if ("field" in link) {
parent.set(link.field, undefined);
} else {
return cb(new Error("Unknown link type"));
}
doc.save(function(err) {
cb(err);
});
}
function mongoosePathPost(req, cb) {
if (!("item" in req.mongoose)) {
return cb.notFound();
}
var item = req.mongoose.item;
if (item.isMongooseDocumentArray) {
mongooseDocArrayPost(req, cb);
} else if (Array.isArray(item)) {
if ("_value" in req.body) {
req.body = req.body._value;
}
mongooseDocArrayPost(req, cb);
} else {
return cb.methodNotAllowed();
}
}
/*!
* Mongoose DocumentArray helpers
*/
function queryDocArray(req) {
var docArray = req.mongoose.item;
if (req.query["query"]) {
// Cache query result
if (!req.mongoose._queryResult) {
req.mongoose._queryResult = docArray.filter(
queryHelpers.match.bind(
null,
queryHelpers.create(req.query["query"])
)
);
}
return req.mongoose._queryResult;
} else {
return docArray;
}
}
function mongooseDocArrayCount(req, cb) {
var len = queryDocArray(req).length;
cb(null, len);
}
function mongooseDocArrayList(req, offset, limit, cb) {
var items = queryDocArray(req);
if (limit > 0) {
items = items.slice(offset, offset + limit);
} else {
items = items.slice(offset);
}
cb(null, items);
}
function mongooseDocArrayPost(req, cb) {
var docArray = req.mongoose.item;
var doc = req.mongoose.doc;
var index = NaN;
if (req.query["index"]) {
index = Number(req.query["index"]);
}
if (isNaN(index)) {
index = docArray.length;
}
docArray.splice(Math.max(0, Math.min(docArray.length, index)), 0, req.body);
doc.save(function(err) {
if (err) {
cb(err);
} else {
if (req.options.postResponse) {
cb(null, getObject(req, docArray[index]));
} else {
cb.created();
}
}
});
}
/*!
* Mongoose resource definition helper
*/
function mongooseResource(name, Model) {
/*jshint validthis:true*/
var collResource = this.sub(name)
.hook(function modelHook(req, next) {
req.mongoose = { model: Model, path: name };
next();
})
.count(mongooseCollCount)
.list(mongooseCollList)
.post(mongooseCollPost)
.set("query", function mongooseDefaultQuery() { return Model.find(); })
.set("key", "_id");
var docResource = collResource.sub(":id")
.hook(mongooseDocHook)
.get(mongooseDocGet)
.put(mongooseDocPut)
.del(mongooseDocDel);
docResource.sub("*", mongoosePathHook)
.get(mongoosePathGet)
.put(mongoosePathPut)
.del(mongoosePathDel)
.post(mongoosePathPost);
return collResource;
}
module.exports = mongooseResource;
/*jshint node:true*/
"use strict";
/*!
* Search query helpers
*/
var queryRegex = /^\/(.*)\/([imx]*)$/;
/* Generate a mongoose query operator from a ?query= request parameter */
function createQueryOperator(query) {
var or = {
$or: query.split(" OR ").map(function(orOperand) {
var and = {
$and: orOperand.split(" AND ").map(function(andOperand) {
var colonIndex = andOperand.indexOf(":"),
bangIndex = andOperand.indexOf("!");
if (colonIndex === -1 && bangIndex === -1) {
// Invalid operator, skip
return {};
}
var match = andOperand.match(/^([^!:]+)([!:])(.*)$/);
var field = match[1];
var negate = match[2] === "!";
var value = match[3];
var operator = {};
var op, matches;
matches = value.match(queryRegex);
if (matches) {
op = new RegExp(matches[1], matches[2]);
operator[field] = negate ? { $not: op } : op;
} else {
if (negate) {
// Mongoose does not handle { $not: "value" }
operator[field] = { $nin: [value] };
} else {
operator[field] = value;
}
}
return operator;
}).filter(function(operator) {
return Object.keys(operator).length > 0;
})
};
return and.$and.length === 1 ? and.$and[0] : and;
})
};
return or.$or.length === 1 ? or.$or[0] : or;
}
/* Get property path value in a document or in a plain object */
function getPath(obj, path) {
if (typeof obj.get === "function") {
return obj.get(path);
}
var parts = path.split(".");
while (parts.length) {
if (!obj) {
return;
}
obj = obj[parts.shift()];
}
return obj;
}
/* Match a mongoose query criterion to a document */
function matchQueryCriterion(crit, doc) {
return Object.keys(crit).every(function(path) {
var value = getPath(doc, path) || "",
match = crit[path],
negate = false,
result;
if (typeof match === "string") {
result = value.toString() === match;
} else {
if ("$not" in match) {
negate = true;
match = match.$not;
}
if (match instanceof RegExp) {
result = !!value.toString().match(match);
} else if ("$nin" in match) {
result = match.$nin.indexOf(value) === -1;
} else {
return false;
}
}
return negate ? !result : result;
});
}
/* Match a mongoose query operator to a document */
function matchQueryOperator(operator, doc) {
if ("$or" in operator) {
return operator.$or.some(function(op) {
return matchQueryOperator(op, doc);
});
} else if ("$and" in operator) {
return operator.$and.every(function(op) {
return matchQueryOperator(op, doc);
});
} else if ("$not" in operator) {
return !matchQueryOperator(operator.$not, doc);
} else {
return matchQueryCriterion(operator, doc);
}
}
module.exports = {
create: createQueryOperator,
match: matchQueryOperator
};
\ No newline at end of file
/*jshint node:true*/
"use strict";
/*!
* Root handlers
*/
var nativeGet = nativePropertyGet;
var nativePost = nativePropertyPost;
/*!
* Property handlers
*/
function nativePropertyHook(req, next) {
var path = req.params["*"].split("/");
var obj = req.nativeRoot;
var parent;
var property;
while (path.length) {
property = path.shift();
if (!(property in obj)) {
delete req.nativeTarget;
return next();
}
parent = obj;
obj = obj[property];
}
req.nativeParent = parent;
req.nativeProperty = property;
req.nativeTarget = obj;
next();
}
function nativeArrayCount(req, cb) {
if (!("nativeTarget" in req)) {
return cb.notFound();
}
cb(null, req.nativeTarget.length);
}
function nativeArrayList(req, offset, limit, cb) {
if (!("nativeTarget" in req)) {
return cb.notFound();
}
var target = req.nativeTarget;
var arr;
if (limit > 0) {
arr = target.slice(offset, offset + limit);
} else {
arr = target.slice(offset);
}
cb(null, arr);
}
function nativeObjectCount(req, cb) {
cb(null, Object.keys(req.nativeTarget).length);
}
function nativeObjectList(req, offset, limit, cb) {
var keys = Object.keys(req.nativeTarget);
if (limit > 0) {
keys = keys.slice(offset, offset + limit);
} else {
keys = keys.slice(offset);
}
cb(null, keys);
}
function nativePropertyGet(req, cb) {
if (!("nativeTarget" in req)) {
return cb.notFound;
}
var target = req.nativeTarget;
if (Array.isArray(target)) {
if (req.options.rawArrays) {
cb(null, target);
} else {
cb.list(nativeArrayCount, nativeArrayList);
}
} else if (typeof target === "object") {
if (req.options.objectCollections) {
cb.list(nativeObjectCount, nativeObjectList);
} else {
cb(null, target);
}
} else {
cb(null, target);
}
}
function nativePropertyPut(req, isPatch, cb) {
var parent = req.nativeParent;
var property = req.nativeProperty;
var target = req.nativeTarget;
var value = req.body;
if ("_value" in value) {
value = value._value;
}
if (isPatch) {
if (typeof target !== "object") {
return cb.methodNotAllowed();
}
Object.keys(req.body).forEach(function(key) {
target[key] = value[key];
});
} else {
parent[property] = value;
}
cb();
}
function nativePropertyPost(req, cb) {
if (!("nativeTarget" in req)) {
return cb.notFound();
}
var target = req.nativeTarget;
var created;
if (Array.isArray(target)) {
var value = req.body;
if ("_value" in value) {
value = value._value;
}
created = value;
target.push(value);
} else if (typeof target === "object") {
if (!("_key" in req.body) || !("_value" in req.body) || typeof req.body._key !== "string") {
return cb.badRequest();
}
created = target[req.body._key] = req.body._value;
} else {
cb.methodNotAllowed();
}
if (req.options.postResponse) {
cb(null, created);
} else {
cb.created();
}
}
function nativePropertyDelete(req, cb) {
if (!("nativeTarget" in req)) {
return cb.notFound();
}
var parent = req.nativeParent;
var property = req.nativeProperty;
if (Array.isArray(parent) && !req.options.sparseArrays) {
parent.splice(property, 1);
} else {
delete parent[property];
}
cb();
}
/*!
* Native resource definition helper
*/
module.exports = function(name, obj) {
var resource = this.sub(name);
resource
.hook(function nativeHook(req, next) {
req.nativeRoot = req.nativeTarget = obj;
next();
})
.get(nativeGet)
.post(nativePost);
resource.sub("*")
.hook(nativePropertyHook)
.get(nativePropertyGet)
.put(nativePropertyPut)
.post(nativePropertyPost)
.del(nativePropertyDelete);
return resource;
};
/*jshint node:true*/
"use strict";
var utils = require("./utils");
var regexpSlashes = /\//g,
regexpTrimSlashes = /^\/|\/$/g,
regexpTrailingStar = /\*$/,
regexpAllNamedParameters = /:[^\/]+/g;
/*!
* Generic helpers
*/
var compiledCache = {};
function compilePattern(pattern, matchSubPaths) {
var cacheKey = matchSubPaths ? pattern + "[/*]" : pattern;
if (!(cacheKey in compiledCache)) {
var compiled = {
raw: pattern,
key: cacheKey
};
var regexp = "^\\/" + pattern
.replace(regexpSlashes, "\\/")
.replace(regexpAllNamedParameters, "([^\\/]+)")
.replace(regexpTrailingStar, "(.*)$");
compiled.trailingStar = !!(pattern.match(regexpTrailingStar));
if (!compiled.trailingStar && matchSubPaths) {
compiled.regexp = new RegExp(regexp + "(\\/.*)?$");
} else {
compiled.regexp = new RegExp(compiled.trailingStar ? regexp : (regexp + "$"));
}
compiled.names = (pattern.match(regexpAllNamedParameters) || []).map(function(name) {
return name.substr(1);
});
if (compiled.trailingStar) {
compiled.names.push("*");
}
compiledCache[cacheKey] = compiled;
}
return compiledCache[cacheKey];
}
function addHandler(handlers, compiled, method, handler) {
var item = Object.create(compiled);
item.method = method;
item.handler = handler;
handlers.push(item);
return item;
}
function addHook(handlers, compiled, hook, strict) {
if (!strict && !compiled.trailingStar) {
compiled = compilePattern(compiled.raw, true);
}
var item = Object.create(compiled);
item.hook = hook;
handlers.push(item);
return item;
}
function addOptions(handlers, compiled, options) {
var item = Object.create(compiled);
item.options = options;
handlers.push(item);
return item;
}
/*!
* Path matcher
*/
function Path(root, pattern) {
this.root = root;
this.compiled = compilePattern(pattern);
if (this.compiled.trailingStar) {
this.sub = undefined;
this.remove = undefined;
}
}
"get list count post put del".split(" ").forEach(function(method) {
Path.prototype[method] = function(handler) {
addHandler(this.root.handlers, this.compiled, method, handler);
return this;
};
});
Path.prototype.hook = function(hook, strict) {
addHook(this.root.handlers, this.compiled, hook, strict);
return this;
};
Path.prototype.readonly = function(subsToo) {
var path = this;
"post put del".split(" ").forEach(function(method) {
path[method](undefined);
if (subsToo) {
path.sub("*")[method](undefined);
}
});
return this;
};
Path.prototype.sub = function(pattern, hook) {
pattern = pattern.replace(regexpTrimSlashes, "");
return this.root.sub(this.compiled.raw + "/" + pattern, hook);
};
Path.prototype.remove = function(pattern) {
pattern = pattern.replace(regexpTrimSlashes, "");
return this.root.remove(this.compiled.raw + "/" + pattern);
};
Path.prototype.set = function(key, value, strict) {
if (typeof key === "object") {
strict = value;
}
var compiled = this.compiled;
if (!strict && !compiled.trailingStar) {
compiled = compilePattern(compiled.raw, true);
}
var hook = this.root.handlers.filter(function(h) {
return h.options && h.raw === compiled.raw;
})[0];
if (!hook) {
hook = addOptions(this.root.handlers, compiled, {});
}
if (typeof key === "object") {
Object.keys(key).forEach(function(k) {
hook.options[k] = key[k];
});
} else {
hook.options[key] = value;
}
return this;
};
/*!
* Base hooks
*/
function getParamHook(names, match) {
// Strip full match
var values = match.slice(1);
return function paramHook(req, next) {
req.params = req.params || {};
names.forEach(function(name) {
var value = values.shift();
if (name === "*") {
req.params[name] = value;
} else {
req.params[name] = decodeURIComponent(value);
}
});
next();
};
}
function getOptionsHook() {
var hook = function optionsHook(req, next) {
req.options = req.options || {};
Object.keys(hook.options).forEach(function(key) {
req.options[key] = hook.options[key];
});
next();
};
hook.options = {};
return hook;
}
function getHref(subpath) {
/*jshint validthis:true */
var req = this;
var path = req.path.replace(regexpTrimSlashes, "");
if (subpath) {
path = path + "/" + subpath.replace(regexpTrimSlashes, "");
}
return utils.getHref(req, path);
}
function matchPattern(pattern, path) {
/*jshint validthis:true */
var compiled = compilePattern(pattern);
var req = this;
var match = (path || req.path).match(compiled.regexp);
if (match) {
var values = match.slice(1);
var params = {};
compiled.names.forEach(function(name) {
params[name] = values.shift();
});
return params;
}
return false;
}
var defaultHooks = [
/* Add request helpers */
function requestHelpersHook(req, next) {
req.getHref = getHref;
req.match = matchPattern;
next();
}
];
/*!
* Root resource
*/
function rootResource() {
var root = {
handlers: [],
sub: function(pattern, hook) {
var path = new Path(root, pattern.replace(regexpTrimSlashes, ""));
if (hook) {
path.hook(hook);
}
return path;
},
remove: function(pattern) {
pattern = pattern.replace(regexpTrimSlashes, "");
function filterFunc(h) {
var raw = h.raw;
if (raw.substr(0, pattern.length) === pattern) {
if (raw.length === pattern.length || raw[pattern.length] === "/") {
return false;
}
}
return true;
}
root.handlers = root.handlers.filter(filterFunc);
},
match: function(req) {
var matchingHooks = defaultHooks.slice(0);
var spec = {};
var matchedPatterns = [];
/* Add options hook */
var optionsHook = getOptionsHook();
matchingHooks.push(optionsHook);
/* Find handlers matching requested path */
root.handlers.forEach(function(h) {
var match = req.path.match(h.regexp);
if (match) {
/* Add parameter hook only once for each pattern */
if (h.names.length > 0 && matchedPatterns.indexOf(h.raw) === -1) {
matchingHooks.push(getParamHook(h.names, match));
matchedPatterns.push(h.raw);
}
if (h.options) {
var options = optionsHook.options;
Object.keys(h.options).forEach(function(key) {
options[key] = h.options[key];
});
}
if (h.hook) {
matchingHooks.push(h.hook);
}
if (h.method) {
// get and count/list override each other
if (h.method === "get") {
delete spec.count;
delete spec.list;
}
if (h.method === "count" || h.method === "list") {
delete spec.get;
}
spec[h.method] = h.handler;
}
}
});
if (Object.keys(spec).length) {
return { spec: spec, hooks: matchingHooks };
}
}
};
return root;
}
module.exports = rootResource;
/*jshint node:true */
"use strict";
var util = require("util");
function extractRoute(req) {
var url = req.url,
orig = req.originalUrl;
return orig.substring(0, orig.lastIndexOf(url));
}
var utils = {
getHref: function(req, urlpath) {
return util.format("%s://%s%s/%s",
req.protocol,
req.headers.host,
extractRoute(req),
urlpath
);
},
addHref: function(req, doc, urlpath) {
doc._href = this.getHref(req, urlpath);
}
};
module.exports = utils;
\ No newline at end of file
/*jshint node:true */
"use strict";
var rootResource = require("./root"),
httpStatus = require("./httpStatus");
function instanciate() {
var root = rootResource();
function yarm(options) {
options = options || {};
options.defaultLimit = options.defaultLimit || 10;
options.errorStack = options.errorStack || false;
function handleError(req, res, err) {
if (err) {
res.status(err.code || 500)
.send(options.errorStack ? err.stack : err.message);
return true;
}
}
function makeCallback(req, res) {
function cb(err, body, mime) {
if (!handleError(req, res, err)) {
sendResponse(req, res, body, mime);
}
}
cb.file = function(err, path, mime) {
if (!handleError(req, res, err)) {
sendFile(req, res, path, mime);
}
};
cb.status = function(code, body) {
handleError(req, res, httpStatus(code, body));
};
cb.list = function(counter, lister) {
sendListResponse(req, res, counter, lister);
};
cb.custom = function(handler) {
handler(req, res);
};
httpStatus.names.forEach(function(name) {
cb[name] = function() {
handleError(req, res, httpStatus[name]());
};
});
return cb;
}
function sendFile(req, res, path, mime) {
if (mime) {
res.type(mime);
}
res.sendFile(path);
}
function sendResponse(req, res, body, mime) {
if (body === null || body === undefined) {
handleError(req, res, httpStatus.noContent());
return;
}
if (mime) {
res.type(mime);
}
if (typeof body === "number") {
// Cast to string to avoid mistaking body for HTTP status
body = "" + body;
}
// TODO look for a cleaner way to identify Readables
if (body && typeof body._read === "function") {
body.pipe(res);
} else if (body) {
res.send(body);
}
}
function sendListResponse(req, res, counter, lister) {
var skip = parseInt(req.query["skip"], 10),
limit = parseInt(req.query["limit"], 10);
if (isNaN(skip)) {
skip = 0;
}
if (isNaN(limit)) {
limit = options.defaultLimit;
}
counter(req, function(err, count) {
if (handleError(req, res, err)) {
return;
}
lister(req, skip, limit, function(err, items) {
if (handleError(req, res, err)) {
return;
}
res.send({
_count: count,
_items: items
});
});
});
}
function restResult(req, res, handlers) {
var method = req.method.toUpperCase();
switch(method) {
case "GET":
case "HEAD":
if (handlers.get) {
return handlers.get(req, makeCallback(req, res));
} else if (handlers.count && handlers.list) {
return sendListResponse(req, res, handlers.count, handlers.list);
}
break;
case "PUT":
case "PATCH":
if (handlers.put) {
return handlers.put(req, method === "PATCH", makeCallback(req, res));
}
break;
case "DELETE":
if (handlers.del) {
return handlers.del(req, makeCallback(req, res));
}
break;
case "POST":
if (handlers.post) {
return handlers.post(req, makeCallback(req, res));
}
break;
}
(makeCallback(req, res)).methodNotAllowed();
}
return function(req, res) {
var data = root.match(req);
function nextHook(err) {
setImmediate(function() {
if (err) {
handleError(req, res, err);
return;
}
var hook = data.hooks.shift();
if (hook) {
try {
hook.call(null, req, nextHook);
} catch(e) {
nextHook(e);
}
} else {
restResult(req, res, data.spec);
}
});
}
nextHook.status = function(code, body) {
handleError(req, res, httpStatus(code, body));
};
httpStatus.names.forEach(function(name) {
nextHook[name] = function() {
nextHook(httpStatus[name]());
};
});
if (data && data.spec) {
data.hooks = data.hooks || [];
nextHook();
} else {
handleError(req, res, httpStatus.notFound());
}
};
}
/* Resource definers */
yarm.resource = function(name) {
return root.sub(name);
};
yarm.remove = function(name) {
root.remove(name);
};
/* Extension helper */
yarm.extend = function(name, handler) {
if (name in yarm) {
throw new Error("Yarm extension '" + name + "' is already defined");
}
yarm[name] = handler.bind(root);
};
return yarm;
}
module.exports = instanciate;
{
"_from": "yarm",
"_id": "yarm@0.4.0",
"_inBundle": false,
"_integrity": "sha1-PZpzq+5JDs53yI670JKf6m9ykHs=",
"_location": "/yarm",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "yarm",
"name": "yarm",
"escapedName": "yarm",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/yarm/-/yarm-0.4.0.tgz",
"_shasum": "3d9a73abee490ece77c88ebbd0929fea6f72907b",
"_spec": "yarm",
"_where": "/home/lab1-24/Desktop/nodejs/student",
"author": {
"name": "Nicolas Joyard",
"email": "joyard.nicolas@gmail.com"
},
"bugs": {
"url": "https://github.com/njoyard/yarm/issues"
},
"bundleDependencies": false,
"deprecated": false,
"description": "Yet Another REST Middleware for express and mongoose",
"devDependencies": {
"body-parser": "*",
"express": "4.*",
"mocha": "2.*",
"mongodb": "2.*",
"mongoose": "4.*"
},
"homepage": "http://yarm.njoyard.fr",
"keywords": [
"rest",
"restful",
"express",
"mongoose"
],
"license": "MIT",
"main": "index.js",
"name": "yarm",
"repository": {
"type": "git",
"url": "git://github.com/njoyard/yarm.git"
},
"scripts": {
"test": "mocha"
},
"version": "0.4.0"
}
/*jshint node:true */
/*global describe, it */
"use strict";
var assert = require("assert"),
common = require("./common"),
resource = common.resource,
request = common.request,
callbackTests = common.callbackTests;
describe("All resources", function() {
describe("PUT", function() {
callbackTests("PUT", it);
it("should pass false as a second argument to .put", function(done) {
var value;
resource("test").put(function(req, isPatch, cb) {
value = isPatch;
cb();
});
request.put("/test", function(res, body) {
assert.strictEqual(false, value);
done();
});
});
});
describe("PATCH", function() {
callbackTests("PATCH", it);
it("should pass true as a second argument to .put", function(done) {
var value;
resource("test").put(function(req, isPatch, cb) {
value = isPatch;
cb();
});
request.patch("/test", function(res, body) {
assert.strictEqual(true, value);
done();
});
});
});
describe("POST", function() {
callbackTests("POST", it);
});
describe("DELETE", function() {
callbackTests("DELETE", it);
});
});
describe("Document resources", function() {
describe("GET", function() {
callbackTests("GET", it);
});
});
describe("Collection resources", function() {
describe("GET", function() {
callbackTests("COUNT", it);
callbackTests("LIST", it);
it("should send a JSON response with the results from .count and .list", function(done) {
resource("test")
.count(function(req, cb) {
cb(null, 42);
})
.list(function(req, offset, limit, cb) {
cb(null, ["foo", "bar"]);
});
request.get("/test", function(res, body) {
var jbody;
assert.doesNotThrow(function() { jbody = JSON.parse(body); });
assert.deepEqual({
_count: 42,
_items: ["foo", "bar"]
}, jbody);
done();
});
});
});
});
\ No newline at end of file
/*jshint node:true */
"use strict";
var http = require("http"),
assert = require("assert"),
util = require("util"),
Readable = require("stream").Readable,
express = require("express"),
yarm = require("../index.js"),
app = express();
/* Test app setup */
app.use(require("body-parser").json());
app.use("/rest", yarm());
app.listen(8081);
/* HTTP request helpers */
function request(method, path, data, callback) {
if (typeof data === "function" && !callback) {
callback = data;
data = undefined;
}
var options = {
host: "localhost",
port: 8081,
path: "/rest" + path,
method: method
};
var req = http.request(options, function(res) {
var body = "";
res.on("data", function(data) {
body += data;
});
res.on("end", function() {
callback(res, body);
});
});
if (data) {
req.setHeader("Content-Type", "application/json");
data = JSON.stringify(data);
}
req.end(data);
}
request.get = request.bind(null, "GET");
request.put = request.bind(null, "PUT");
request.post = request.bind(null, "POST");
request.del = request.bind(null, "DELETE");
request.patch = request.bind(null, "PATCH");
/* Test resource definition helper */
function resource(name, definition) {
yarm.remove(name);
return yarm.resource(name, definition);
}
/* Define tests for all methods
init is called at the beginning of each test with an empty context object
callback is called when receiving the response for each test, it receives
the response, the body, the context object and a done function.
*/
function allMethods(it, init, path, callback) {
var call = ["get", "put", "post", "del", "patch"];
call.forEach(function(method) {
it("(" + method.toUpperCase() + " method)", function(done) {
var ctx = {};
init(ctx);
request[method](path, function(res, body) {
callback(res, body, ctx, done);
});
});
});
}
/* Data for standard callback tests
"name" is the name of the method called on the resource definition
"cbIndex" is the argument index of the callback passed to "name"
*/
var methods = {
"GET": { name: "get", cbIndex: 1, noContent: { code: 204, msg: "" } },
"PUT": { name: "put", cbIndex: 2, noContent: { code: 204, msg: "" } },
"PATCH": { name: "put", cbIndex: 2, noContent: { code: 204, msg: "" } },
"DELETE": { name: "del", cbIndex: 1, noContent: { code: 204, msg: "" } },
"POST": { name: "post", cbIndex: 1, noContent: { code: 204, msg: "" } },
};
/* Describe standard tests valid for all methods */
function callbackTests(method, it) {
var methodName, doRequest, callbackIndex, noContent,
doResultTests = true,
additionalMethods = {};
switch(method) {
case "COUNT":
methodName = "count";
doRequest = request.bind(null, "GET");
callbackIndex = 1;
doResultTests = false;
additionalMethods = {
"list": function(req, offset, limit, cb) { cb(); }
};
break;
case "LIST":
methodName = "list";
doRequest = request.bind(null, "GET");
callbackIndex = 3;
doResultTests = false;
additionalMethods = {
"count": function(req, cb) { cb(); }
};
break;
default:
methodName = methods[method].name;
doRequest = request.bind(null, method);
callbackIndex = methods[method].cbIndex;
noContent = methods[method].noContent;
break;
}
it("should respond with 405 Not Allowed when no " + methodName + " handler was set", function(done) {
var r = resource("test");
// Define a handler for an other method
if (["get", "list", "count"].indexOf(methodName) === -1) {
r.get(function(req, cb) {
cb();
});
} else {
r.put(function(req, cb) {
cb();
});
}
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
doRequest("/test", function(res, body) {
assert.strictEqual(body, "Method not allowed");
assert.strictEqual(res.statusCode, 405);
done();
});
});
it("should call " + methodName + " handler", function(done) {
var called = false,
r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
called = true;
cb();
});
doRequest("/test", function(res, body) {
assert(called);
done();
});
});
it("should respond 500 with the error message passed from " + methodName + " handler", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb(new Error("Test error"));
});
doRequest("/test", function(res, body) {
assert.strictEqual(body, "Test error");
assert.strictEqual(res.statusCode, 500);
done();
});
});
it("should respond with the error message and code passed from " + methodName + " handler", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
var err = new Error("Test error");
err.code = 542;
cb(err);
});
doRequest("/test", function(res, body) {
assert.strictEqual(body, "Test error");
assert.strictEqual(res.statusCode, 542);
done();
});
});
if (doResultTests) {
it("should respond with " + noContent.code + " " + noContent.msg + " when " + methodName + " handler sends nothing", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb();
});
doRequest("/test", function(res, body) {
assert.strictEqual(res.statusCode, noContent.code);
assert.strictEqual(body, noContent.msg);
done();
});
});
it("should respond with the result from " + methodName + " handler", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb(null, "Test content");
});
doRequest("/test", function(res, body) {
assert.strictEqual(body, "Test content");
assert.strictEqual(res.statusCode, 200);
done();
});
});
it("should respond with the Buffer result from " + methodName + " handler", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb(null, new Buffer("Test content"));
});
doRequest("/test", function(res, body) {
assert.strictEqual(body, "Test content");
assert.strictEqual(res.statusCode, 200);
done();
});
});
it("should respond with the readable stream result from " + methodName + " handler", function(done) {
function TestStream(opt) {
Readable.call(this, opt);
this._done = false;
}
util.inherits(TestStream, Readable);
TestStream.prototype._read = function() {
if (!this._done) {
this.push("Test content");
this._done = true;
} else {
this.push(null);
}
};
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb(null, new TestStream());
});
doRequest("/test", function(res, body) {
assert.strictEqual(body, "Test content");
assert.strictEqual(res.statusCode, 200);
done();
});
});
it("should send response with mimetype from " + methodName + " handler", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb(null, "Test content", "text/x-test-content");
});
doRequest("/test", function(res, body) {
assert.strictEqual(res.headers["content-type"], "text/x-test-content; charset=utf-8");
assert.strictEqual(body, "Test content");
done();
});
});
it("should send file when " + methodName + " handler calls cb.file()", function(done) {
var r = resource("test");
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
cb.file(null, __dirname + "/testfile", "text/x-test-content");
});
doRequest("/test", function(res, body) {
assert.strictEqual(res.headers["content-type"], "text/x-test-content; charset=utf-8");
assert.strictEqual(body, "Test file content");
done();
});
});
}
it("should pass the request object to " + methodName + " handler", function(done) {
var r = resource("test"),
request;
Object.keys(additionalMethods).forEach(function(key) {
r[key](additionalMethods[key]);
});
r[methodName](function() {
var req = arguments[0],
cb = arguments[callbackIndex];
request = req;
cb();
});
doRequest("/test?foo=bar", function(res, body) {
assert.strictEqual(request.query["foo"], "bar");
done();
});
});
}
function composeTests(array) {
if (array.length === 1) {
return array[0];
} else {
var first = array.shift(),
second = array.shift();
array.unshift(function(done) {
function firstDone() {
second(done);
}
first(firstDone);
});
return composeTests(array);
}
}
module.exports = {
app: app,
request: request,
resource: resource,
callbackTests: callbackTests,
allMethods: allMethods,
composeTests: composeTests
};
/*jshint node:true */
/*global describe, it, before, after, beforeEach, afterEach */
"use strict";
var mongoose = require("mongoose"),
assert = require("assert"),
yarm = require("../index.js"),
common = require("./common"),
request = common.request,
callbackTests = common.callbackTests,
allMethods = common.allMethods,
composeTests = common.composeTests;
function assertJSON(json) {
var data;
try {
data = JSON.parse(json);
} catch(e) {
assert.strictEqual(json, "[valid json]");
}
return data;
}
function assertReturnedDoc(body, doc) {
var returned = assertJSON(body);
// Remove properties added by mongoose
delete returned.__v;
delete returned._id;
if (returned.docArray) {
returned.docArray.forEach(function(subdoc) {
delete subdoc._id;
});
}
assert.deepEqual(returned, doc);
}
function assertEmpty(body) {
if (body && body.length > 0) {
assert.strictEqual(body, "[empty body]");
}
}
function assertCreated(res, body) {
assert.strictEqual(res.statusCode, 201);
assert.strictEqual(body, "Created");
}
/*!
* Test data
*/
var testSchema = new mongoose.Schema({
field1: String,
field2: String,
subDoc: {
field: String
},
docArray: [{
field: String,
sub: {
field: String
}
}],
array: [String],
"url encoded/property": String
});
testSchema.virtual("description").get(function() {
return "Document " + this.field1 + " with " + this.docArray.length + " sub-documents";
});
var TestModel = mongoose.model("test", testSchema);
/* Empty docArrays are not mandatory, but mongoose adds them anyway so
having them here makes comparison easier */
var testData = [
{ field1: "foo", docArray: [], array: [] },
{ field1: "bar", field2: "baz", docArray: [], array: [] },
{ field1: "sub", subDoc: { field: "foo" }, docArray: [], array: ["foo", "bar"] },
{ field1: "arr", docArray: [{ field: "foo" }, { field: "bar" }, { field: "baz", sub: { field: "sub" } }], array: [] },
{ field1: "urldecode", docArray: [], array: [], "url encoded/property": "foo" }
];
/*!
* Test helpers
*/
/* Resource definition helpers */
function mongooseResource(name, model) {
yarm.remove(name);
return yarm.mongoose(name, model);
}
function aggregateResource(name, model, pipeline, options) {
yarm.remove(name);
return yarm.aggregate(name, model, pipeline, options);
}
/* Collection result checking helper */
function assertCollection(res, body, field1values) {
var data = assertJSON(body);
// Basic response check
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof data, "object");
assert.strictEqual(data._count, field1values.length);
assert(Array.isArray(data._items));
assert.strictEqual(data._items.length, field1values.length);
// Find expected objects in testData
var expected = {};
testData.forEach(function(doc) {
if (field1values.indexOf(doc.field1) !== -1) {
expected[doc.field1] = doc;
}
});
// Check that all items are expected
data._items.forEach(function(doc) {
assert(doc.field1 in expected);
// Cleanup fields before comparing
delete doc.__v;
delete doc._href;
if (doc.subDoc) {
delete doc.subDoc._href;
}
doc.docArray.forEach(function(subdoc) {
delete subdoc._href;
if (subdoc.sub) {
delete subdoc.sub._href;
}
});
assert.deepEqual(doc, expected[doc.field1]);
});
}
/* DocumentArray collection result checking helper */
function assertDocArrayCollection(res, body, fieldvalues) {
var docArray = testData[3].docArray;
var data = assertJSON(body);
// Basic response check
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof data, "object");
assert.strictEqual(data._count, fieldvalues.length);
assert(Array.isArray(data._items));
assert.strictEqual(data._items.length, fieldvalues.length);
// Find expected objects in testData
var expected = {};
docArray.forEach(function(doc) {
if (fieldvalues.indexOf(doc.field) !== -1) {
expected[doc.field] = doc;
}
});
// Check that all items are expected
data._items.forEach(function(doc) {
assert(doc.field in expected);
// Cleanup fields before comparing
delete doc.__v;
delete doc._href;
assert.deepEqual(doc, expected[doc.field]);
});
}
/*!
* Test definitions
*/
describe("Mongoose resources", function() {
// Connect once before all tests
before(function(done) {
mongoose.connect("mongodb://localhost/yarmTest", function(err) {
done(err);
});
});
// Drop database and disconnect once after all tests
after(function(done) {
mongoose.connection.db.dropDatabase(function(err) {
if (err) {
done(err);
} else {
mongoose.disconnect(function(err) {
done(err);
});
}
});
});
// Create data before each test
beforeEach(function(done) {
var copy = [];
function saveNext() {
var data = testData.shift();
if (!data) {
testData = copy;
done();
return;
}
copy.push(data);
var doc = new TestModel(data);
doc.save(function(err) {
if (err) {
done(err);
} else {
TestModel.findOne({ field1: doc.field1 }, function(err, found) {
if (err) {
done(err);
} else if (!found) {
done(new Error("Previousy saved document was not found again"));
} else {
/* Grab saved _ids */
data._id = found._id.toString();
if (data.docArray) {
data.docArray.forEach(function(sub, index) {
sub._id = found.docArray[index]._id.toString();
});
}
saveNext();
}
});
}
});
}
saveNext();
});
// Drop data after each test
afterEach(function(done) {
TestModel.remove(function(err) {
done(err);
});
});
describe("Model resources", function() {
describe("Model collections", function() {
it("should GET collections", function(done) {
mongooseResource("test", TestModel);
request.get("/test", function(res, body) {
assertCollection(res, body, ["foo", "bar", "sub", "arr", "urldecode"]);
done();
});
});
function queryTest(query, expected, done) {
mongooseResource("test", TestModel);
request.get("/test?query=" + encodeURIComponent(query), function(res, body) {
assertCollection(res, body, expected);
done();
});
}
it(
"should compare fields when query has field:value",
queryTest.bind(null, "field1:foo", ["foo"])
);
it(
"should regex-compare fields when query has field:/regexp/",
queryTest.bind(null, "field1:/a/", ["bar", "arr"])
);
it(
"should regex-compare with flags",
queryTest.bind(null, "field1:/A/i", ["bar", "arr"])
);
it(
"should negate comparisons with field!value",
queryTest.bind(null, "field1!foo", ["bar", "arr", "sub", "urldecode"])
);
it(
"should negate regex-comparisons with field!/regexp/",
queryTest.bind(null, "field1!/a/", ["foo", "sub", "urldecode"])
);
it(
"should allow queries on sub-fields",
queryTest.bind(null, "subDoc.field:foo", ["sub"])
);
it(
"should allow queries with AND operators",
queryTest.bind(null, "field1:/a/ AND field2:baz", ["bar"])
);
it(
"should allow queries with OR operators",
queryTest.bind(null, "field1:/o/ OR field2:/a/", ["foo", "bar", "urldecode"])
);
it(
"should allow queries with both AND and OR operators",
queryTest.bind(null, "field1:/o/ OR field1:/a/ AND field2:/a/", ["foo", "bar", "urldecode"])
);
it("should POST new documents to collections", function(done) {
mongooseResource("test", TestModel);
var doc = {
field1: "add",
field2: "hello",
subDoc: {
field: "world"
},
docArray: [
{ field: "a" },
{ field: "b" }
],
array: []
};
request.post("/test", doc, function(res, body) {
assertCreated(res, body);
// Check addition to mongoose collection first
TestModel.findOne({ field1: "add" }, function(err, item) {
assert.ifError(err);
assert(item);
done();
});
});
});
it("should return POSTed documents when postResponse is true", function(done) {
mongooseResource("test", TestModel).set("postResponse", true);
var doc = {
field1: "add",
field2: "hello",
subDoc: {
field: "world"
},
docArray: [
{ field: "a" },
{ field: "b" }
],
array: [ "x", "y" ]
};
request.post("/test", doc, function(res, body) {
assert.strictEqual(res.statusCode, 200);
assertReturnedDoc(body, doc);
done();
});
});
it("should allow sorting collections", function(done) {
mongooseResource("test", TestModel);
request.get("/test?sort=field1", function(res, body) {
var data = assertJSON(body);
var docs = data._items;
assert.strictEqual(docs[0].field1, "arr");
assert.strictEqual(docs[1].field1, "bar");
assert.strictEqual(docs[2].field1, "foo");
assert.strictEqual(docs[3].field1, "sub");
done();
});
});
it("should allow setting the default sort with an option", function(done) {
mongooseResource("test", TestModel)
.set("sort", { field1: "asc" });
request.get("/test", function(res, body) {
var data = assertJSON(body);
var docs = data._items;
assert.strictEqual(docs[0].field1, "arr");
assert.strictEqual(docs[1].field1, "bar");
assert.strictEqual(docs[2].field1, "foo");
assert.strictEqual(docs[3].field1, "sub");
done();
});
});
});
describe("Documents", function() {
it(
"should GET documents in collection",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel);
request.get("/test/" + item._id, function(res, body) {
var doc = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof doc, "object");
/* Remove additional properties before comparing */
delete doc.__v;
delete doc._href;
if (doc.docArray) {
doc.docArray.forEach(function(sub) {
delete sub._href;
});
}
assert.deepEqual(doc, item);
done();
});
};
}))
);
it(
"should allow setting mongoose toObject options",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel)
.set("toObject", { virtuals: true });
request.get("/test/" + item._id, function(res, body) {
var doc = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof doc, "object");
/* Remove additional properties before comparing */
delete doc.__v;
delete doc._href;
if (doc.docArray) {
doc.docArray.forEach(function(sub) {
delete sub._href;
});
}
assert.strictEqual(
doc.description,
"Document " + item.field1 + " with " + item.docArray.length + " sub-documents"
);
done();
});
};
}))
);
it("should 404 on nonexistent documents", function(done) {
mongooseResource("test", TestModel);
request.get("/test/nonexistent", function(res, body) {
assert.strictEqual(res.statusCode, 404);
assert.strictEqual(body, "Not found");
done();
});
});
it(
"should allow specifying an alternate primary key",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel)
.set("key", "field1");
request.get("/test/" + item.field1, function(res, body) {
var doc = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof doc, "object");
/* Remove additional properties before comparing */
delete doc.__v;
delete doc._href;
if (doc.docArray) {
doc.docArray.forEach(function(sub) {
delete sub._href;
});
}
assert.deepEqual(doc, item);
done();
});
};
}))
);
it("should DELETE documents", function(done) {
var item = testData[0];
mongooseResource("test", TestModel);
request.del("/test/" + item._id, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.find({ _id: item._id }, function(err, items) {
assert.ifError(err);
assert.strictEqual(items.length, 0);
done();
});
});
});
it("should PUT documents", function(done) {
var item = testData[0];
mongooseResource("test", TestModel);
request.put("/test/" + item._id, { field2: "bar" }, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.field1, "foo");
assert.strictEqual(doc.field2, "bar");
done();
});
});
});
});
describe("Document fields", function() {
it(
"should GET fields from documents",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel);
request.get("/test/" + item._id + "/field1", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, item.field1);
done();
});
};
}))
);
it("should URLdecode field names", function(done) {
mongooseResource("test", TestModel);
request.get("/test/" + testData[4]._id + "/url%20encoded%2Fproperty", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "foo");
done();
});
});
it("should 404 on nonexistent document fields", function(done) {
mongooseResource("test", TestModel);
request.get("/test/" + testData[0]._id + "/nonexistent", function(res, body) {
assert.strictEqual(res.statusCode, 404);
assert.strictEqual(body, "Not found");
done();
});
});
it(
"should DELETE field values in documents",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel);
request.del("/test/" + item._id + "/field1", function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.strictEqual(doc.field1, undefined);
done();
});
});
};
}))
);
it(
"should PUT field values in documents",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel);
request.put("/test/" + item._id + "/field1", { _value: "newValue" }, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.field1, "newValue");
done();
});
});
};
}))
);
it(
"should POST to array fields in documents",
composeTests(testData.map(function(item) {
return function(done) {
mongooseResource("test", TestModel);
request.post("/test/" + item._id + "/array", { _value: "baz" }, function(res, body) {
assert.strictEqual(body, "Created");
assert.strictEqual(res.statusCode, 201);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.notStrictEqual(doc.array.indexOf("baz"), -1);
done();
});
});
};
}))
);
it("should DELETE array field items in documents", function(done) {
mongooseResource("test", TestModel);
var item = testData.filter(function(item) {
return item.field1 === "sub";
})[0];
request.del("/test/" + item._id + "/array/0", function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.array.indexOf("foo"), -1);
done();
});
});
});
});
describe("Subdocuments", function() {
it("should GET subdocuments", function(done) {
var item = testData[2];
mongooseResource("test", TestModel);
request.get("/test/" + item._id + "/subDoc", function(res, body) {
var doc = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof doc, "object");
delete doc._href;
assert.deepEqual(doc, item.subDoc);
done();
});
});
it("should DELETE subdocuments", function(done) {
var item = testData[2];
mongooseResource("test", TestModel);
request.del("/test/" + item._id + "/subDoc", function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
// Mongoose limitation: subDoc is still present but empty
assert.strictEqual(doc.subDoc.field, undefined);
done();
});
});
});
it("should PUT subdocuments", function(done) {
var item = testData[2];
mongooseResource("test", TestModel);
request.put("/test/" + item._id + "/subDoc", { _value: { field: "bar" } }, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.subDoc.field, "bar");
done();
});
});
});
});
describe("Subdocument fields", function() {
it("should GET fields in subdocuments", function(done) {
var item = testData[2];
mongooseResource("test", TestModel);
request.get("/test/" + item._id + "/subDoc/field", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.deepEqual(body, item.subDoc.field);
done();
});
});
it("should DELETE fields in subdocuments", function(done) {
var item = testData[2];
mongooseResource("test", TestModel);
request.del("/test/" + item._id + "/subDoc/field", function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.subDoc.field, undefined);
done();
});
});
});
it("should PUT field values in subdocuments", function(done) {
var item = testData[2];
mongooseResource("test", TestModel);
request.put("/test/" + item._id + "/subDoc/field", { _value: "newValue" }, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.subDoc.field, "newValue");
done();
});
});
});
});
describe("DocumentArray collections", function() {
it("should GET DocumentArrays as collections", function(done) {
var item = testData[3];
mongooseResource("test", TestModel);
request.get("/test/" + item._id + "/docArray", function(res, body) {
var data = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof data, "object");
assert.strictEqual(data._count, item.docArray.length);
assert(Array.isArray(data._items));
assert.strictEqual(data._items.length, item.docArray.length);
item.docArray.forEach(function(doc) {
var found = false;
data._items.forEach(function(rdoc) {
if (rdoc.field1 === doc.field1) {
found = true;
}
});
assert(found);
});
done();
});
});
function queryTest(query, expected, done) {
var item = testData[3];
mongooseResource("test", TestModel);
request.get("/test/" + item._id + "/docArray?query=" + encodeURIComponent(query), function(res, body) {
assertDocArrayCollection(res, body, expected);
done();
});
}
it(
"should compare fields when query has field:value",
queryTest.bind(null, "field:foo", ["foo"])
);
it(
"should regex-compare fields when query has field:/regexp/",
queryTest.bind(null, "field:/a/", ["bar", "baz"])
);
it(
"should regex-compare with flags",
queryTest.bind(null, "field:/A/i", ["bar", "baz"])
);
it(
"should negate comparisons when query has field!value",
queryTest.bind(null, "field!foo", ["bar", "baz"])
);
it(
"should negate regex-comparisons when query has field!/regexp/",
queryTest.bind(null, "field!/a/", ["foo"])
);
it(
"should allow queries on sub-fields",
queryTest.bind(null, "sub.field:sub", ["baz"])
);
it(
"should allow queries with AND operators",
queryTest.bind(null, "field:/a/ AND field:/^b/", ["bar", "baz"])
);
it(
"should allow queries with OR operators",
queryTest.bind(null, "field:/o/ OR field:/a/", ["foo", "bar", "baz"])
);
it(
"should allow queries with both AND and OR operators",
queryTest.bind(null, "field:/o/ OR field:/z/ AND field:/^b/", ["foo", "baz"])
);
it("should POST new documents to DocumentArray collections", function(done) {
var item = testData[3];
mongooseResource("test", TestModel);
request.post("/test/" + item._id + "/docArray", { field: "bang" }, function(res, body) {
assertCreated(res, body);
assert.strictEqual(res.statusCode, 201);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
var subs = doc.docArray.filter(function(sub) {
return sub.field === "bang";
});
assert.strictEqual(subs.length, 1);
done();
});
});
});
it("should return POSTed documents when postResponse is true", function(done) {
var item = testData[3];
mongooseResource("test", TestModel).set("postResponse", true);
request.post("/test/" + item._id + "/docArray", { field: "bang" }, function(res, body) {
assert.strictEqual(res.statusCode, 200);
assertReturnedDoc(body, { field: "bang" });
done();
});
});
it("should POST new documents to DocumentArrays at specified index", function(done) {
var item = testData[3];
mongooseResource("test", TestModel);
request.post("/test/" + item._id + "/docArray?index=1", { field: "bang" }, function(res, body) {
assertCreated(res, body);
assert.strictEqual(res.statusCode, 201);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
var subs = doc.docArray.filter(function(sub) {
return sub.field === "bang";
});
assert.strictEqual(subs.length, 1);
assert.strictEqual(doc.docArray.indexOf(subs[0]), 1);
done();
});
});
});
});
describe("DocumentArray documents", function() {
it("should GET documents in DocumentArrays",
composeTests(testData[3].docArray.map(function(item) {
return function(done) {
mongooseResource("test", TestModel);
request.get("/test/" + testData[3]._id + "/docArray/" + item._id, function(res, body) {
var doc = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof doc, "object");
/* Remove additional properties before comparing */
delete doc.__v;
delete doc._href;
assert.deepEqual(doc, item);
done();
});
};
}))
);
it(
"should allow specifying an alternate primary key on collection paths",
composeTests(testData[3].docArray.map(function(item) {
return function(done) {
mongooseResource("test", TestModel)
.sub(":id/docArray")
.set("subkeys", "field");
request.get("/test/" + testData[3]._id + "/docArray/" + item.field, function(res, body) {
var doc = assertJSON(body);
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(typeof doc, "object");
/* Remove additional properties before comparing */
delete doc.__v;
delete doc._href;
assert.deepEqual(doc, item);
done();
});
};
}))
);
it("should DELETE documents in DocumentArray collections", function(done) {
var item = testData[3],
sub = item.docArray[0];
mongooseResource("test", TestModel);
request.del("/test/" + item._id + "/docArray/" + sub._id, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.docArray.id(sub._id), null);
done();
});
});
});
it("should PUT documents in DocumentArray collections", function(done) {
var item = testData[3],
sub = item.docArray[0];
mongooseResource("test", TestModel);
request.put("/test/" + item._id + "/docArray/" + sub._id, { field: "bang" }, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.docArray.id(sub._id).field, "bang");
done();
});
});
});
});
describe("DocumentArray document fields", function() {
it("should GET fields in documents in DocumentArrays",
composeTests(testData[3].docArray.map(function(item, index) {
return function(done) {
mongooseResource("test", TestModel);
request.get("/test/" + testData[3]._id + "/docArray/" + item._id + "/field", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, testData[3].docArray[index].field);
done();
});
};
}))
);
it("should DELETE fields in documents in DocumentArrays", function(done) {
var item = testData[3],
sub = item.docArray[0];
request.del("/test/" + item._id + "/docArray/" + sub._id + "/field", function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.docArray.id(sub._id).field, undefined);
done();
});
});
});
it("should PUT fields in documents in DocumentArrays", function(done) {
var item = testData[3],
sub = item.docArray[0];
request.put("/test/" + item._id + "/docArray/" + sub._id + "/field", { _value: "bang" }, function(res, body) {
assertEmpty(body);
assert.strictEqual(res.statusCode, 204);
TestModel.findById(item._id, function(err, doc) {
assert.ifError(err);
assert.strictEqual(doc.docArray.id(sub._id).field, "bang");
done();
});
});
});
});
describe("Overrides", function() {
it("should allow overriding documents", function(done) {
mongooseResource("test", TestModel)
.sub('override/property')
.get(function(req, cb) {
cb(null, "hello, world!");
});
request.get("/test/override/property", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "hello, world!");
done();
});
});
it("should allow overriding document properties", function(done) {
var item = testData[0];
mongooseResource("test", TestModel)
.sub(':id/helloworld')
.get(function(req, cb) {
cb(null, "hello, world!");
});
request.get("/test/" + item._id + "/helloworld", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "hello, world!");
done();
});
});
});
});
describe("Aggregate resources", function() {
var aggregatePipeline = [
{ $project: {
field1: 1,
docArray: 1
} },
{ $unwind: "$docArray" },
{ $project: {
_id: "$docArray.field",
parent: "$field1"
} },
{ $sort: {
_id: -1
} }
];
function aggregateTest(query, expected, done) {
var uri = "/test";
aggregateResource("test", TestModel, aggregatePipeline);
if (query) {
uri += "?query=" + encodeURIComponent(query);
}
request.get(uri, function(res, body) {
assert.strictEqual(res.statusCode, 200);
var data = assertJSON(body);
assert.strictEqual(typeof data, "object");
assert.strictEqual(data._count, expected.length);
assert(Array.isArray(data._items));
assert.strictEqual(data._items.length, expected.length);
assert.deepEqual(
data._items.map(function(item) {
return item._id;
}),
expected
);
done();
});
}
it(
"should GET aggregates as collections",
aggregateTest.bind(null, "", [ "foo", "baz", "bar" ])
);
it(
"should compare fields when query has field:value",
aggregateTest.bind(null, "_id:foo", ["foo"])
);
it(
"should regex-compare fields when query has field:/regexp/",
aggregateTest.bind(null, "_id:/a/", ["baz", "bar"])
);
it(
"should regex-compare with flags",
aggregateTest.bind(null, "_id:/A/i", ["baz", "bar"])
);
it(
"should negate comparisons when query has field!value",
aggregateTest.bind(null, "_id!foo", ["baz", "bar"])
);
it(
"should negate regex-comparisons when query has field!/regexp/",
aggregateTest.bind(null, "_id!/a/", ["foo"])
);
it(
"should allow queries with AND operators",
aggregateTest.bind(null, "_id:/a/ AND parent:arr", ["baz", "bar"])
);
it(
"should allow queries with OR operators",
aggregateTest.bind(null, "_id:/a/ OR _id:/o/", ["foo", "baz", "bar"])
);
it(
"should allow queries with both AND and OR operators",
aggregateTest.bind(null, "parent:/r/ OR _id:/a/ AND _id:/b/", ["foo", "baz", "bar"])
);
it("should GET agregated documents with their projected _id", function(done) {
aggregateResource("test", TestModel, aggregatePipeline);
request.get("/test/bar", function(res, body) {
assert.strictEqual(res.statusCode, 200);
var doc = assertJSON(body);
assert.strictEqual(typeof doc, "object");
assert.strictEqual(doc._id, "bar");
assert.strictEqual(doc.parent, "arr");
done();
});
});
it("should 404 on nonexistent aggregated documents", function(done) {
aggregateResource("test", TestModel, aggregatePipeline);
request.get("/test/nope", function(res, body) {
assert.strictEqual(res.statusCode, 404);
assert.strictEqual(body, "Not found");
done();
});
});
it("should allow defining custom subresources", function(done) {
aggregateResource("test", TestModel, aggregatePipeline)
.sub(":id/foo")
.get(function(req, cb) {
cb(null, "I'm foo inside " + req.mongoose.item._id);
});
request.get("/test/bar/foo", function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "I'm foo inside bar");
done();
});
});
});
});
/*jshint node:true */
/*global describe, it */
"use strict";
var assert = require("assert"),
common = require("./common"),
yarm = require("../index.js"),
request = common.request;
function resource(name, value) {
yarm.remove(name);
return yarm.native(name, value);
}
function assertJSON(json) {
var data;
try {
data = JSON.parse(json);
} catch(e) {
assert.strictEqual(json, "[valid json]");
}
return data;
}
function allTypes(description, doRequest, checkResponse, options) {
var types = {
"string": { value: "foo" },
"number": { value: 42 },
"bool": { value: true },
"array": { value: ["foo", "bar", "baz"] },
"object": { value: { foo: "bar" } }
};
options = options || {};
function objectValues(obj) {
return Object.keys(obj).map(function(k) { return obj[k]; });
}
Object.keys(types).forEach(function(type) {
it(description.replace("%type", type), function(done) {
var expectedValue = types[type].value,
jsonBody = expectedValue;
if (type !== "array" && type !== "object") {
jsonBody = { _value: expectedValue };
}
doRequest(expectedValue, jsonBody, function(res, body) {
var actualValue = checkResponse(expectedValue, res, body);
if (type === "array") {
if (options.rawArrays) {
assert(Array.isArray(actualValue));
assert.strictEqual(actualValue.join(","), expectedValue.join(","));
} else {
assert.strictEqual(typeof actualValue, "object");
assert.strictEqual(actualValue._count, expectedValue.length);
assert(Array.isArray(actualValue._items));
assert.strictEqual(actualValue._items.join(","), expectedValue.join(","));
}
} else if (type === "object") {
if (!options.objectCollections) {
assert.strictEqual(typeof actualValue, "object");
assert.strictEqual(Object.keys(actualValue).join(","), Object.keys(expectedValue).join(","));
assert.strictEqual(objectValues(actualValue).join(","), objectValues(expectedValue).join(","));
} else {
assert.strictEqual(typeof actualValue, "object");
assert.strictEqual(actualValue._count, Object.keys(expectedValue).length);
assert(Array.isArray(actualValue._items));
assert.strictEqual(actualValue._items.join(","), Object.keys(expectedValue).join(","));
}
} else {
assert.strictEqual(actualValue, expectedValue);
}
done();
});
});
});
}
describe("Native resources", function() {
describe("Root resources", function() {
it("Should GET object resources", function(done) {
resource("test", {
number: 42,
string: "foo",
bool: true,
arr: [1, 2, 3]
})
request.get("/test", function(res, body) {
var data = assertJSON(body);
assert.strictEqual(typeof data, "object");
assert.strictEqual(data.number, 42);
assert.strictEqual(data.string, "foo");
assert.strictEqual(data.bool, true);
assert.strictEqual(Array.isArray(data.arr), true);
assert.strictEqual(data.arr.join(","), "1,2,3");
done();
});
});
it("Should GET objects as collections when objectCollections is true", function(done) {
resource("test", {
number: 42,
string: "foo",
bool: true,
arr: [1, 2, 3]
}).set("objectCollections", true);
request.get("/test?skip=1&limit=2", function(res, body) {
var data = assertJSON(body);
assert.strictEqual(data._count, 4);
assert.strictEqual(data._items.join(","), "string,bool");
done();
});
});
it("Should GET arrays as collections", function(done) {
resource("test", ["foo", "bar", "baz"])
.set("arrayCollections", true);
request.get("/test?skip=1&limit=1", function(res, body) {
var data = assertJSON(body);
assert.strictEqual(data._count, 3);
assert.strictEqual(data._items.join(","), "bar");
done();
});
});
it("Should GET array resources as is when rawArrays is true", function(done) {
resource("test", ["foo", "bar", "baz"])
.set("rawArrays", true);
request.get("/test", function(res, body) {
var data = assertJSON(body);
assert.strictEqual(Array.isArray(data), true);
assert.strictEqual(data.join(","), "foo,bar,baz");
done();
});
});
it("Should POST new key/value pairs to object resources", function(done) {
var obj = { foo: "bar" };
resource("test", obj);
request.post("/test", { _key: "added", _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 201);
assert.strictEqual(obj.added, "baz");
done();
});
});
it("Should return POSTed values when postResponse is true", function(done) {
var obj = { foo: "bar" };
resource("test", obj).set("postResponse", true);
request.post("/test", { _key: "added", _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "baz");
done();
});
});
it("Should refuse to POST to object resources without _value in input", function(done) {
var obj = { foo: "bar" };
resource("test", obj);
request.post("/test", { _key: "added" }, function(res, body) {
assert.strictEqual(res.statusCode, 400);
done();
});
});
it("Should refuse to POST to object resources without _key in input", function(done) {
var obj = { foo: "bar" };
resource("test", obj);
request.post("/test", { _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 400);
done();
});
});
it("Should refuse to POST to object resources with non-string _key in input", function(done) {
var obj = { foo: "bar" };
resource("test", obj);
request.post("/test", { _key: 1, _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 400);
done();
});
});
it("Should POST new array items to array resources", function(done) {
var arr = ["foo", "bar"];
resource("test", arr);
request.post("/test", { _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 201);
assert.strictEqual(arr.join(","), "foo,bar,baz");
done();
});
});
it("Should return POSTed array items when postResponse is true", function(done) {
var arr = ["foo", "bar"];
resource("test", arr).set("postResponse", true);
request.post("/test", { _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "baz");
done();
});
});
});
describe("Properties", function() {
describe("GET", function() {
allTypes("Should GET %type property values", function(expected, json, callback) {
resource("test", { property: expected });
request.get("/test/property", callback);
}, function(expected, res, body) {
assert.strictEqual(res.statusCode, 200);
return (typeof expected === "string") ? body : assertJSON(body);
});
allTypes("Should GET %type array items", function(expected, json, callback) {
resource("test", ["foo", expected, "bar"]);
request.get("/test/1", callback);
}, function(expected, res, body) {
assert.strictEqual(res.statusCode, 200);
return (typeof expected === "string") ? body : assertJSON(body);
});
});
describe("PUT", function() {
var obj = { property: "foo" };
allTypes("Should PUT %type property values", function(expected, json, callback) {
resource("test", obj);
request.put("/test/property", json, callback);
}, function(expected, res, body) {
assert.strictEqual(res.statusCode, 204);
return obj.property;
}, { rawArrays: true });
var arr = [ "foo", "bar", "baz" ];
allTypes("Should PUT %type array items", function(expected, json, callback) {
resource("test", arr);
request.put("/test/1", json, callback);
}, function(expected, res, body) {
assert.strictEqual(res.statusCode, 204);
return arr[1];
}, { rawArrays: true });
});
describe("PATCH", function() {
it("Should PATCH object property values", function(done) {
var obj = { obj: { foo: "bar" } };
resource("test", obj);
request.patch("/test/obj", { number: 42, string: "foo" }, function(res, body) {
assert.strictEqual(res.statusCode, 204);
assert.strictEqual(obj.obj.foo, "bar");
assert.strictEqual(obj.obj.number, 42);
assert.strictEqual(obj.obj.string, "foo");
done();
});
});
it("Should PATCH object array items", function(done) {
var obj = [{ foo: "bar" }];
resource("test", obj);
request.patch("/test/0", { number: 42, string: "foo" }, function(res, body) {
assert.strictEqual(res.statusCode, 204);
assert.strictEqual(obj[0].foo, "bar");
assert.strictEqual(obj[0].number, 42);
assert.strictEqual(obj[0].string, "foo");
done();
});
});
it("Should refuse to PATCH non-object property values", function(done) {
resource("test", { nonObject: "foo" });
request.patch("/test/nonObject", { number: 42 }, function(res, body) {
assert.strictEqual(res.statusCode, 405);
done();
});
});
it("Should refuse to PATCH non-object array items", function(done) {
resource("test", ["foo"]);
request.patch("/test/0", { number: 42 }, function(res, body) {
assert.strictEqual(res.statusCode, 405);
done();
});
});
});
describe("DELETE", function() {
it("Should DELETE properties", function(done) {
var obj = { number: 42, string: "foo" };
resource("test", obj);
request.del("/test/number", function(res, body) {
assert.strictEqual(res.statusCode, 204);
assert.strictEqual(typeof obj.number, "undefined");
done();
});
});
it("Should DELETE array items", function(done) {
var obj = [42, "foo"];
resource("test", obj);
request.del("/test/0", function(res, body) {
assert.strictEqual(res.statusCode, 204);
assert.strictEqual(obj.length, 1);
assert.strictEqual(obj[0], "foo");
done();
});
});
it("Should DELETE array items leaving a hole when sparseArrays is true", function(done) {
var obj = [42, "foo"];
resource("test", obj).set("sparseArrays", true);
request.del("/test/0", function(res, body) {
assert.strictEqual(res.statusCode, 204);
assert.strictEqual(obj.length, 2);
assert.strictEqual(typeof obj[0], "undefined");
done();
});
});
});
describe("POST", function() {
it("Should POST new key/value pairs to object sub-resources", function(done) {
var obj = { obj: { foo: "bar" } };
resource("test", obj);
request.post("/test/obj", { _key: "added", _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 201);
assert.strictEqual(obj.obj.added, "baz");
done();
});
});
it("Should return POSTed value when postResponse is true", function(done) {
var obj = { obj: { foo: "bar" } };
resource("test", obj).set("postResponse", true);
request.post("/test/obj", { _key: "added", _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "baz");
done();
});
});
it("Should refuse to POST to object sub-resources without _value in input", function(done) {
var obj = { obj: { foo: "bar" } };
resource("test", obj);
request.post("/test/obj", { _key: "added" }, function(res, body) {
assert.strictEqual(res.statusCode, 400);
done();
});
});
it("Should refuse to POST to object sub-resources without _key in input", function(done) {
var obj = { obj: { foo: "bar" } };
resource("test", obj);
request.post("/test/obj", { _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 400);
done();
});
});
it("Should refuse to POST to object sub-resources with non-string _key in input", function(done) {
var obj = { obj: { foo: "bar" } };
resource("test", obj);
request.post("/test/obj", { _key: 1, _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 400);
done();
});
});
it("Should POST new array items to array sub-resources", function(done) {
var obj = { arr: ["foo", "bar"] };
resource("test", obj);
request.post("/test/arr", { _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 201);
assert.strictEqual(obj.arr.join(","), "foo,bar,baz");
done();
});
});
it("Should return POSTed array items when postResponse is true", function(done) {
var obj = { arr: ["foo", "bar"] };
resource("test", obj).set("postResponse", true);
request.post("/test/arr", { _value: "baz" }, function(res, body) {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(body, "baz");
done();
});
});
});
});
});
\ No newline at end of file
/*jshint node:true */
/*global describe, it */
"use strict";
var assert = require("assert"),
common = require("./common"),
yarm = require("../index.js"),
request = common.request;
describe("New instance", function() {
it("Should enable creating new yarm instances", function() {
assert.strictEqual(typeof yarm.newInstance, "function");
var instance = yarm.newInstance();
assert.strictEqual(typeof instance, "function");
assert.strictEqual(typeof instance.resource, "function");
});
it("Should not share resources between separate instances", function(done) {
var instance = yarm.newInstance();
common.app.use("/rest2", instance());
yarm.resource("test1")
.get(function(req, cb) {
cb(null, "Test 1 resource");
});
instance.resource("test2")
.get(function(req, cb) {
cb(null, "Test 2 resource");
});
request.get("/test1", function(res, body) {
assert.strictEqual(body, "Test 1 resource");
request.get("/test2", function(res, body) {
assert.strictEqual(res.statusCode, 404);
request.get("2/test1", function(res, body) {
assert.strictEqual(res.statusCode, 404);
request.get("2/test2", function(res, body) {
assert.strictEqual(body, "Test 2 resource");
done();
});
});
});
});
});
});
/*jshint node:true */
/*global describe, it */
"use strict";
var assert = require("assert"),
common = require("./common"),
resource = common.resource,
request = common.request;
function assertJSON(json) {
var data;
try {
data = JSON.parse(json);
} catch(e) {
assert.strictEqual(json, "[valid json]");
}
return data;
}
describe("Sub-resources", function() {
it("Should allow defining sub-resources with .sub()", function(done) {
var r = resource("test");
"get put post del sub list count".split(" ").forEach(function(method) {
assert.strictEqual("function", typeof r.sub("foo")[method]);
});
r.sub("foo").get(function(req, cb) { cb(null, "bar"); });
r.sub("bar").get(function(req, cb) { cb(null, "baz"); });
request.get("/test/foo", function(res, body) {
assert.strictEqual("bar", body);
request.get("/test/bar", function(res, body) {
assert.strictEqual("baz", body);
done();
});
});
});
it("Should allow defining deeper sub-resources with chained .sub() calls", function(done) {
resource("test").sub("foo").sub("bar").get(function(req, cb) {
cb(null, "baz");
});
request.get("/test/foo/bar", function(res, body) {
assert.strictEqual("baz", body);
done();
});
});
it("Should allow sub-resource wildcards with .sub(':var')", function(done) {
resource("test").sub(":var").get(function(req, cb) {
cb(null, "bar");
});
request.get("/test/foo", function(res, body) {
assert.strictEqual("bar", body);
done();
});
});
it("Should allow direct deeper definitions with a single .sub() call", function(done) {
var r = resource("test");
r.sub("foo/:x/bar").get(function(req, cb) {
cb(null, "bar");
});
request.get("/test/foo/whatever/bar", function(res, body) {
assert.strictEqual("bar", body);
done();
});
});
it("Should allow greedy sub-resource wildcards with .sub('*')", function(done) {
resource("test").sub("*").get(function(req, cb) {
cb(null, "bar");
});
request.get("/test/foo", function(res, body) {
assert.strictEqual("bar", body);
request.get("/test/bar/baz", function(res, body) {
assert.strictEqual("bar", body);
done();
});
});
});
it("Should store values matched by sub-resource wildcards in req.params", function(done) {
var r = resource("test");
r.sub("foo/:bar/baz/:bing").get(function(req, cb) {
cb(null, req.params.bar + "/" + req.params.bing);
});
r.sub("foo2/*").get(function(req, cb) {
cb(null, req.params["*"]);
});
request.get("/test/foo/barValue/baz/bingValue", function(res, body) {
assert.strictEqual("barValue/bingValue", body);
request.get("/test/foo2/bar/baz", function(res, body) {
assert.strictEqual("bar/baz", body);
done();
});
});
});
it("Should URL-decode values matched by wildcards", function(done) {
var r = resource("test");
r.sub("foo/:bar/baz").get(function(req, cb) {
cb(null, req.params.bar);
});
request.get("/test/foo/url%20encoded%2Fvalue/baz", function(res, body) {
assert.strictEqual("url encoded/value", body);
done();
});
});
it("Should not URL-decode values matched by catchall wildcard", function(done) {
var r = resource("test");
r.sub("foo/*").get(function(req, cb) {
cb(null, req.params["*"]);
});
request.get("/test/foo/url%20encoded/baz/with%2Fslash", function(res, body) {
assert.strictEqual("url%20encoded/baz/with%2Fslash", body);
done();
});
});
it("Should override previously defined handlers for sub-resources", function(done) {
var r = resource("test");
r.sub("*").get(function(req, cb) { cb(null, "*"); });
r.sub("foo/*").get(function(req, cb) { cb(null, "foo/*"); });
r.sub("foo/bar").get(function(req, cb) { cb(null, "first"); });
r.sub("foo/:x").get(function(req, cb) { cb(null, "second"); });
r.sub("foo").sub("bar").get(function(req, cb) { cb(null, "third"); });
r.sub("foo").sub(":x").get(function(req, cb) { cb(null, "fourth"); });
request.get("/test/foo/bar", function(res, body) {
assert.strictEqual("fourth", body);
done();
});
});
it("Should disable write methods when .readonly() has been called", function(done) {
var writeCalled = false;
function handler() {
writeCalled = true;
arguments[arguments.length - 1]();
}
resource("test")
.put(handler)
.post(handler)
.del(handler)
.readonly();
request.put("/test", {}, function(req, body) {
assert.strictEqual(req.statusCode, 405);
assert(!writeCalled);
request.post("/test", {}, function(req, body) {
assert.strictEqual(req.statusCode, 405);
assert(!writeCalled);
request.del("/test", function(req, body) {
assert.strictEqual(req.statusCode, 405);
assert(!writeCalled);
done();
});
});
});
});
describe("Hooks", function() {
it("Sould allow defining hooks for each .sub() call", function(done) {
var r = resource("test");
r.sub("foo", function(req, next) {
req.hooks = req.hooks || [];
req.hooks.push("first");
next();
});
r.sub("foo").sub("bar", function(req, next) {
req.hooks = req.hooks || [];
req.hooks.push("second");
next();
});
r.sub("foo/bar", function(req, next) {
req.hooks = req.hooks || [];
req.hooks.push("third");
next();
}).get(function(req, cb) {
cb(null, req.hooks.join("-"));
});
request.get("/test/foo/bar", function(res, body) {
assert.strictEqual("first-second-third", body);
done();
});
});
it("Should stop handling when a hook returns an error", function(done) {
var r = resource("test"),
called = [];
r.sub("foo", function(req, next) { next(); });
r.sub("foo").sub("bar", function(req, next) { next(new Error("Oops")); });
r.sub("foo/bar", function(req, next) {
called.push("third");
next();
}).get(function(req, cb) {
called.push("get");
cb();
});
request.get("/test/foo/bar", function(res, body) {
assert.strictEqual(-1, called.indexOf("third"));
assert.strictEqual(-1, called.indexOf("get"));
assert.strictEqual("Oops", body);
assert.strictEqual(500, res.statusCode);
done();
});
});
it("Should stop handling when a hook throws", function(done) {
var r = resource("test"),
called = [];
r.sub("foo", function(req, next) { next(); });
r.sub("foo").sub("bar", function() { throw new Error("Oops"); });
r.sub("foo/bar", function(req, next) {
called.push("third");
next();
}).get(function(req, cb) {
called.push("get");
cb();
});
request.get("/test/foo/bar", function(res, body) {
assert.strictEqual(-1, called.indexOf("third"));
assert.strictEqual(-1, called.indexOf("get"));
assert.strictEqual("Oops", body);
assert.strictEqual(500, res.statusCode);
done();
});
});
});
it("Should allow getting URLs for subresources with req.getHref([subpath])", function(done) {
var r = resource("test");
r.sub("foo/bar").get(function(req, cb) {
cb(null, {
raw: req.getHref(),
sub: req.getHref("baz/bing")
});
});
request.get("/test/foo/bar", function(res, body) {
var data = assertJSON(body);
assert.strictEqual(data.raw, "http://localhost:8081/rest/test/foo/bar");
assert.strictEqual(data.sub, "http://localhost:8081/rest/test/foo/bar/baz/bing");
done();
});
});
it("Should allow pattern matching with req.match(pattern)", function(done) {
resource("test").sub("foo/bar").get(function(req, cb) {
cb(null, [
req.match("test/foo"),
req.match("test/foo/bar"),
req.match(":first/:second"),
req.match(":first/foo/:third"),
req.match(":first/*")
]);
});
request.get("/test/foo/bar", function(res, body) {
var data = assertJSON(body);
/* Return false when not matching */
assert.strictEqual(data[0], false);
assert.strictEqual(data[2], false);
/* Return an empty object when matching pattern without parameters */
assert.strictEqual(typeof data[1], "object");
assert.strictEqual(Object.keys(data[1]).length, 0);
/* Return an object with matching parameters */
assert.strictEqual(typeof data[3], "object");
assert.strictEqual(data[3].first, "test");
assert.strictEqual(data[3].third, "bar");
assert.strictEqual(typeof data[4], "object");
assert.strictEqual(data[4].first, "test");
assert.strictEqual(data[4]["*"], "foo/bar");
done();
});
});
it("Should allow custom path pattern matching with req.path(pattern, path)", function(done) {
resource("test").get(function(req, cb) {
cb(null, [
req.match("test/foo", "/test/foo/bar"),
req.match("test/foo/bar", "/test/foo/bar"),
req.match(":first/:second", "/test/foo/bar"),
req.match(":first/foo/:third", "/test/foo/bar"),
req.match(":first/*", "/test/foo/bar")
]);
});
request.get("/test", function(res, body) {
var data = assertJSON(body);
/* Return false when not matching */
assert.strictEqual(data[0], false);
assert.strictEqual(data[2], false);
/* Return an empty object when matching pattern without parameters */
assert.strictEqual(typeof data[1], "object");
assert.strictEqual(Object.keys(data[1]).length, 0);
/* Return an object with matching parameters */
assert.strictEqual(typeof data[3], "object");
assert.strictEqual(data[3].first, "test");
assert.strictEqual(data[3].third, "bar");
assert.strictEqual(typeof data[4], "object");
assert.strictEqual(data[4].first, "test");
assert.strictEqual(data[4]["*"], "foo/bar");
done();
});
});
describe("Options", function() {
it("Should allow setting options on resources", function(done) {
var options1, options2;
resource("test")
.set("foo", "bar")
.hook(function(req, next) {
options1 = req.options;
next();
})
.get(function(req, cb) {
options2 = req.options;
cb();
});
request.get("/test", function(res, body) {
assert.strictEqual("bar", options1.foo);
assert.strictEqual("bar", options2.foo);
done();
});
});
it("Should allow setting multiple options at once on resources", function(done) {
var options1, options2;
resource("test")
.set({ "foo": "bar", "fuu": "baz" })
.hook(function(req, next) {
options1 = req.options;
next();
})
.get(function(req, cb) {
options2 = req.options;
cb();
});
request.get("/test", function(res, body) {
assert.strictEqual("bar", options1.foo);
assert.strictEqual("bar", options2.foo);
assert.strictEqual("baz", options1.fuu);
assert.strictEqual("baz", options2.fuu);
done();
});
});
it("Should allow setting options on sub-resources", function(done) {
var options1, options2;
resource("test")
.sub("foo")
.set("foo", "bar")
.hook(function(req, next) {
options1 = req.options;
next();
})
.get(function(req, cb) {
options2 = req.options;
cb();
});
request.get("/test/foo", function(res, body) {
assert.strictEqual("bar", options1.foo);
assert.strictEqual("bar", options2.foo);
done();
});
});
it("Should pass parent options to sub-resource hooks and handlers", function(done) {
var options1, options2;
resource("test")
.set("foo", "bar")
.sub("foo")
.hook(function(req, next) {
options1 = req.options;
next();
})
.get(function(req, cb) {
options2 = req.options;
cb();
});
request.get("/test/foo", function(res, body) {
assert.strictEqual("bar", options1.foo);
assert.strictEqual("bar", options2.foo);
done();
});
});
it("Should not pass strict parent options to sub-resource hooks and handlers", function(done) {
var options1, options2;
resource("test")
.set("foo", "bar", true)
.sub("foo")
.hook(function(req, next) {
options1 = req.options;
next();
})
.get(function(req, cb) {
options2 = req.options;
cb();
});
request.get("/test/foo", function(res, body) {
assert.strictEqual("undefined", typeof options1.foo);
assert.strictEqual("undefined", typeof options2.foo);
done();
});
});
it("Should not leak sub-resource options to parent hooks and handlers", function(done) {
var options1, options2;
resource("test")
.hook(function(req, next) {
options1 = req.options;
next();
})
.get(function(req, cb) {
options2 = req.options;
cb();
})
.sub("foo")
.set("foo", "bar");
request.get("/test", function(res, body) {
assert.strictEqual("undefined", typeof options1.foo);
assert.strictEqual("undefined", typeof options2.foo);
done();
});
});
});
});
\ No newline at end of file
Test file content
\ No newline at end of file
{
"name": "student",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"yarm": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/yarm/-/yarm-0.4.0.tgz",
"integrity": "sha1-PZpzq+5JDs53yI670JKf6m9ykHs="
}
}
}
{
"name": "student",
"version": "1.0.0",
"description": "\"display information given for partially entered Patiphan Marak\"",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "http://projectcs.sci.ubu.ac.th/Patiphan/node-js-60-2.git"
},
"author": "csubu",
"license": "ISC",
"dependencies": {
"yarm": "^0.4.0"
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment