diff --git a/README.md b/README.md
index 6e29adb..778f6ce 100644
--- a/README.md
+++ b/README.md
@@ -15,19 +15,19 @@ It is built on top of [gcloud-node](https://github.com/GoogleCloudPlatform/gclou
- [Installation](#installation)
- [What do I get from it](#what-do-i-get-from-it)
- [Getting started](#getting-started)
- - [Initiate](#initiate)
-- [Create a REST API for an Entity](#create-a-rest-api-for-an-entity)
- - [Basic](#basic)
- - [Additional settings](#additional-settings)
+ - [Initiate library](#initiate-library)
+- [Create an API for an Entity](#create-an-api-for-an-entity)
+ - [settings](#settings)
- [path](#path)
- [ancestors](#ancestors)
- [op](#op)
+ - [op settings](#op-settings)
## Motivation
-While I was working on the [gstore-node](https://github.com/sebelga/gstore-node) library I was building a REST API
-for a mobile project. I found myself copying a lot of the same code over and over to create all the routes and controllers needed to manage my Datastore entities. So I decided to create this small utility to help me generate all the REST routes for CRUD operations on the Google Datastore entities.
+While I was coding the [gstore](https://github.com/sebelga/gstore-node) library I was working on an REST API
+for a mobile project. I found myself copying a lot of the same code to create all the routes and controllers needed to manage my Datastore entities. So I decided to create this small utility to help me create all the REST routes for CRUD operations over the Google Datastore entities.
## Installation
@@ -40,7 +40,7 @@ for a mobile project. I found myself copying a lot of the same code over and ove
**Without** gstoreApi
-```
+```js
// blogPost.routes.js
var BlogPostController = require('../controllers/blogPost.controller');
@@ -69,7 +69,7 @@ module.exports = BlogPostRoutes;
```
-```
+```js
// blogPost.controller.js
// BlogPost is a Datastools Model
@@ -146,7 +146,7 @@ module.exports = {
**With** gstoreApi
-```
+```js
// server.js
var gstoreApi = require('datastore-api');
@@ -157,16 +157,16 @@ gstoreApi.init({
```
-The next file is all you need to have a REST API for a "BlogPost" Datastore Entity
+The next file is all you need to have a full CRUD REST API of a [gstore-node BlogPost Model](https://github.com/sebelga/gstore-node#model)
-```
+```js
// lib
var gstoreApi = require('gstore-api');
// Model (gstore-node)
var BlogPost = require('../models/blogPost');
-module.exports = function() {
+module.exports = function() {
// --> REST API for Model
new datastoreApi(BlogPost);
}
@@ -176,28 +176,33 @@ module.exports = function() {
## Getting started
-### Initiate
+### Initiate library
Before using gstoreApi you need to initiate the library with `gstoreApi.init({...settings})`
The settings is an object with the following properties:
- router
-- simplifyResult // (optional) default: true
-- contexts // (optional)
+- host (optional)
+- simplifyResult(optional) default: true
+- contexts (optional)
-**router**
+**router**
The Express Router instance
-**simplifyResult**
+**host**
+Specify the host of your API. It is needed to create the \ Header in the Model.list() response that contains the next pageCursor. If you don't specify it will be auto-generated with the info in the request
+`req.protocol + '://' + req.get('host') + req.originalUrl`
+
+**simplifyResult**
Define globally if the response format is simplified or not. See explanation in [gstore-node docs](https://github.com/sebelga/gstore-node#queries)
-**context**
-Contexts is an objects with 2 properties: "**public**" and "**private**" that specify a sufix for the routes to be generated.
+**context**
+Contexts is an objects with 2 properties: "**public**" and "**private**" that specify a suffix for the routes to be generated.
gstoreApi considers that "GET" calls (that don't mutate the resource) are *public* and all others (POST, PUT, PATCH, DELETE) are *private*.
-Its default value is an object that does not add any sufix to any route.
+Its default value is an object that does not add any suffix to any route.
-```
+```js
{
public : '',
private : ''
@@ -206,7 +211,7 @@ Its default value is an object that does not add any sufix to any route.
But for example if you initiate gstoreApi like this
-```
+```js
gstoreApi.init({
router : router,
contexts : {
@@ -218,55 +223,42 @@ gstoreApi.init({
And you defined an Auth middelware
-```
+```js
router.use('/private/', yourAuthMiddelware);
```
Then all the POST, PUT, PATCH and DELETE routes will automatically be routed through your Auth middelware.
-## Create a REST API for an Entity
+## Create an API for an Entity
-### Basic
-To its simplest form, to create an API for a Model you just need to create a new instance of the gstoreApi and pass the Model.
+To its simplest form, to create an API for a Model you just need to create a new instance of the gstoreApi and pass the gstore-node Model.
-```
+```js
var gstoreApi = require('gstore-api');
var Model = require('../models/my-model');
new gstoreApi(Model);
```
-### Additional settings
+### settings
If you need some fine-tuning, the gstoreApi constructor has a second parameter where you can configure the following settings
-```
-// NOTE: All the settings below are OPTIONAL. You only need to set the one you need to tweak.
+```js
+// NOTE: All the settings below are OPTIONAL. Just define what you need to tweak.
{
- path: '/end-point', // if not specified will be automatically generated (see below)
- ancestors : 'Dad', // can also ben an array ['GranDad', 'Dad']
+ path: '/end-point', // if not specified will be auto-generated (see below)
+ ancestors : 'Dad', // can also ben an ['GranDad', 'Dad']
op : {
- list : {
- fn : someController.someMethod,
- middelware : someMiddelware,
- exec : true, (default true)
- options : {
- simplifyResult : true, (default to settings in init())
- readAll : false, (default false)
- },
- path : {
- prefix : 'additional-prefix' // can also be an .
- sufix : 'additional-sufix' // not sure why I added this feature, but it's there :)
- }
- },
- get : {...} // same as above
+ list : {... see op setting below},
+ get : {...}
create : {...},
- udpatePatch : {...}, // PATCH verb
- updateReplace : {...}, // PUT verb
+ udpatePatch : {...}, // PATCH :id
+ updateReplace : {...}, // PUT :id
delete : {...},
- deleteAll : {...} // exec defaults to false (for security)
+ deleteAll : {...}
}
}
@@ -274,26 +266,26 @@ If you need some fine-tuning, the gstoreApi constructor has a second parameter w
```
#### path
-If you don't pass the path for the resource it will be **auto-generated** with the following rules:
+If not set the path to the resource is **auto-generated** with the following rules:
- lowercase
- dash for camelCase
-- pluralize the entity Kind
+- pluralize entity Kind
-```
+```js
Example:
entity Kind path
----------------------------
-'BlogPost' --> '/blog-posts'
+'BlogPost' --> '/blog-posts'
'Query' --> '/queries'
```
#### ancestors
-You can set here one or several ancestors (Entity Kinds) for the current Model. The path that is generated follows the same rules as the path setting above.
+You can pass here one or several ancestors (entity Kinds) for the Model. The path create follows the same rules as mentioned above.
-```
+```js
// gstore-node Model
var Comment = require('./models/comment.model');
@@ -328,24 +320,47 @@ new gstoreApi(Comment, {
Operations can be any of
-- list (GET all entities)
+- list (GET all entities) --> call the list() query shortcut on Model. [Documentation here](https://github.com/sebelga/gstore-node#list).
- get (GET one entity)
- create (POST new entity)
-- updatePatch (PATCH update entity) --> only update the properties sent in body
+- updatePatch (PATCH update entity) --> only update properties sent
- updateReplace (PUT to update entity) --> replace all data for entity
- delete (DELETE one entity)
- deleteAll (DELETE all entities)
-They all have the **same configuration settings** with the following properties
+**Link Header**
+The **list** operation adds a [Link Header](https://tools.ietf.org/html/rfc5988#page-6) (rel="next") with the link to the next page to fetch if there are more result.
-**fn**
-Controller function to call. If you don't pass it it will default to get and return the entity from Google Datastore.
+##### op settings
-**middelware**
-You can specify a custom middelware for any operation. You might want, for example, to add a middleware to upload files.
+Each operation has the **same configuration settings** with the following properties
+```js
+{
+ fn : someController.someMethod,
+ middelware : someMiddelware,
+ exec : true, (default true)
+ options : {
+ simplifyResult : true, (default to settings passed in init())
+ readAll : false, (default false)
+ },
+ path : {
+ prefix : 'additional-prefix' // can also be an .
+ suffix : 'additional-suffix' // not sure why I added this feature, but it's there :)
+ }
+}
```
+
+
+**fn**
+Custom Controller method. Like any Express Router method, it has the **request** and **response** parameters. `function controllerMethod(req, res) {...}`
+
+
+**middelware**
+You can specify a custom middelware for any operation. You might want for example to specify a middleware to upload a file.
+
+```js
// Upload file with multer package
var multer = require('multer');
var storage = multer.memoryStorage();
@@ -362,25 +377,38 @@ new gstoreApi(Image, {
op : {
create : {
middelware : upload.single('file'),
- fn : imageController.create // Add a custom logic just for this POST
+ fn : imageController.create // Add a custom Controller for the POST
}
}
});
-// The following will have the middelware added and call the custom controller method
+// The following will have the middelware added and call the custom Controller method
POST /images
```
-**exec**
-You define with this property to execute or not this operation on the entity Model. Defaults to **true** except for "*deleteAll*" operation that you must manually set to true for security reason.
+**exec**
+This property defines if the route for the operation is created (and therefore executed) or not. Defaults to **true** except for "*deleteAll*" that you must manually set to true for security reason.
+
+**options**
+
+- **simplifyResult**: for list operation it will pass this setting to the Model.list() action. And for get, create, and update(s) operation it will call entity.plain()
+- **readAll**: (default: false) in case you have defined some properties in your Schema with read:false ([see the doc](https://github.com/sebelga/gstore-node#read)), they won't show up in the response. If you want them in the response set this property to true.
+
+Extra options:
+**Only** for the "list" operation, there are some extra options that you can set to override any of the shortcut query "list" settings. [See the docs](https://github.com/sebelga/gstore-node#list)
+
+- **limit**
+- **order**
+- **select**
+- **ancestors** (except if already defined in global init())
+- **filters**
+
-**options**
+**path**
+You can add here some custom prefix or suffix to the path.
-- **simplifyResult**: for the list() operation it will forward this setting to the Model.list() action. For get, create, and update(s) operations it will call entity.plain() on the response.
-- **readAll**: (default: false) in case you have set the **read setting** on some of your Schema's properties to false ([see the doc](https://github.com/sebelga/gstore-node#read)), these won't show up in the response data unless you set "readAll" to true.
+If you pass an **\** of prefix like this `['', '/private', '/some-other-route']`, this will create 3 endPoints to access your entity and so let you define 3 different middelwares if needed. You will then have to define a custom Controller method (fn) to deal with these differents scenarios and customize the data saved or returned. For example: outputing more data if the user is authenticated.
-**path**
-You can add here some custom prefix or suffix to the path.
-Important: This will override the settings from the global "contexts".
+Important: This "path" setting will override the global "contexts" settings.
diff --git a/lib/index.js b/lib/index.js
index d4d3433..e9da940 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -5,7 +5,7 @@ const extend = require('extend');
const errorsHandler = require('./errorsHandler');
const arrify = require('arrify');
const S = require('string');
-const pluralize = require('pluralize')
+const pluralize = require('pluralize');
const OPERATIONS = ['list', 'get', 'create', 'updatePatch', 'updateReplace', 'delete', 'deleteAll'];
@@ -97,27 +97,39 @@ class DatastoreApi {
}
list(req, res) {
+ let _this = this;
let listConfig = this.settings.op && this.settings.op.hasOwnProperty('list') ? this.settings.op.list : {};
if (listConfig.fn) {
listConfig.fn(req, res);
} else {
let options = listConfig.options || {};
- let simplify = options.hasOwnProperty('simplifyResult') ? options.simplifyResult : this.settings.simplifyResult;
let ancestors = ancestorsFromParams(req.params, this.settings.ancestors);
- let settings = {
- simplifyResult:simplify
- };
+
+ let settings = extend(true, {}, options);
+ if (!settings.hasOwnProperty('simplifyResult')) {
+ settings.simplifyResult = this.settings.simplifyResult;
+ }
if (ancestors) {
settings.ancestors = ancestors;
}
- this.Model.list(settings, (err, entities) => {
+ if (req.query.pageCursor) {
+ settings.start = req.query.pageCursor;
+ }
+
+ this.Model.list(settings, (err, result) => {
if (err) {
return errorsHandler.rpcError(err, res);
}
- res.json(entities);
+ if (result.nextPageCursor) {
+ var linkNextPage = _this.settings.host || req.protocol + '://' + req.get('host');
+ req.originalUrl = req.originalUrl.replace(/\?pageCursor=[^&]+/, '');
+ linkNextPage += req.originalUrl + '?pageCursor=' + result.nextPageCursor;
+ res.set('Link', '<' + linkNextPage + '>; rel="next"');
+ }
+ res.json(result.entities);
});
}
}
@@ -299,7 +311,7 @@ class DatastoreApi {
fn: myController.method,
path : {
prefix: 'some-prefix',
- sufix: 'some-sufix'
+ suffix: 'some-suffix'
}
}
*/
@@ -325,7 +337,7 @@ function createRoutes(self) {
prefix = config && config.path && config.path.prefix ? config.path.prefix : prefix;
let prefixs = arrify(prefix);
- let sufix = config && config.path && config.path.sufix ? config.path.sufix : '';
+ let suffix = config && config.path && config.path.suffix ? config.path.suffix : '';
let middelware = config.middelware ? [config.middelware] : null;
let ancestors = settings.hasOwnProperty('ancestors') ? arrify(settings.ancestors) : undefined;
@@ -336,7 +348,7 @@ function createRoutes(self) {
prefix += '/' + pathFromEntityKind(ancestor) + '/:anc' + index + 'ID';
});
}
- let path = pathWithId.indexOf(op) < 0 ? prefix + settings.path + sufix : prefix + settings.path + '/:id' + sufix;
+ let path = pathWithId.indexOf(op) < 0 ? prefix + settings.path + suffix : prefix + settings.path + '/:id' + suffix;
paths.push(path);
});
diff --git a/package.json b/package.json
index 2a6a19a..933db39 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gstore-api",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "gstore-api is a NodeJS Express route builder that help designing RESTful APIs to manage Google Datastore entities.",
"main": "index.js",
"scripts": {
diff --git a/test/index-test.js b/test/index-test.js
index 214c7fe..fbb77b6 100644
--- a/test/index-test.js
+++ b/test/index-test.js
@@ -73,11 +73,18 @@ describe('Datastore API', function() {
Model = gstore.model('BlogPost', schema);
- req = {params:{id:123, anc0ID:'ancestor1', anc1ID:'ancestor2'}, body:{title:'Blog Title'}};
+ req = {
+ params:{id:123, anc0ID:'ancestor1', anc1ID:'ancestor2'},
+ query : {},
+ body:{title:'Blog Title'},
+ get : () => 'http://localhost',
+ originalUrl : ''
+ };
res = {
status:() => {
return {json:() => {}}
},
+ set : () => {},
json:() => {}
};
@@ -112,6 +119,7 @@ describe('Datastore API', function() {
it('should merge settings passed', () => {
var settings = {
+ host: '',
contexts : {
public:'/public',
private : '/private'
@@ -187,9 +195,9 @@ describe('Datastore API', function() {
args.push(arguments[i]);
}
cb = args.pop();
- settings = args.length > 0 ? args[0] : undefined;
-
- return cb(null, entities);
+ return cb(null, {
+ entities : entities
+ });
});
});
@@ -198,7 +206,7 @@ describe('Datastore API', function() {
});
it('should read from general settings', () => {
- var dsApi = new gstoreApi(Model, {path:'/users', simplifyResult:true}, router);
+ var dsApi = new gstoreApi(Model, {path:'/users', simplifyResult:true});
dsApi.list(req, res);
@@ -206,12 +214,46 @@ describe('Datastore API', function() {
expect(res.json.getCall(0).args[0]).equal(entities);
});
+ it('should set Link Header if nextPageCursor', () => {
+ Model.list.restore();
+ sinon.stub(Model, 'list', function(settings, cb) {
+ return cb(null, {
+ nextPageCursor : 'abc123'
+ });
+ });
+ sinon.spy(res, 'set');
+ var dsApi = new gstoreApi(Model);
+
+ dsApi.list(req, res);
+
+ expect(res.set.called).be.true;
+ expect(res.set.getCall(0).args[0]).equal('Link');
+
+ res.set.restore();
+ });
+
+ it('should add start setting if pageCursor in request query', () => {
+ req.query.pageCursor = 'abcd1234';
+ var dsApi = new gstoreApi(Model);
+
+ dsApi.list(req, res);
+
+ expect(Model.list.getCall(0).args[0]).deep.equal({simplifyResult:false, start:'abcd1234'});
+ });
+
it('should read options', () => {
var settings = {
path:'/users',
op : {
list : {
- options : {simplifyResult:true}
+ options : {
+ simplifyResult:true,
+ limit : 13,
+ order : {property: 'title', descending:true},
+ select : 'name',
+ ancestors : ['MyDad'],
+ filters : ['paid', true]
+ }
}
}
};
@@ -219,13 +261,20 @@ describe('Datastore API', function() {
dsApi.list(req, res);
- expect(Model.list.getCall(0).args[0]).deep.equal({simplifyResult:true});
+ expect(Model.list.getCall(0).args[0]).deep.equal(settings.op.list.options);
});
it('should add ancestors to query', () => {
var settings1 = {
path:'/users',
- ancestors : 'Dad'
+ ancestors : 'Dad',
+ op : {
+ list : {
+ options : {
+ ancestors : ['MyDad']
+ }
+ }
+ }
};
var dsApi1 = new gstoreApi(Model, settings1, router);
@@ -901,12 +950,12 @@ describe('Datastore API', function() {
expect(router.route.getCall(3).args[0]).equal('/myprefix2/users/:id');
});
- it('should add sufix to path', () => {
+ it('should add suffix to path', () => {
var dsApi = new gstoreApi(Model, {
path:'/users',
op : {
- list: {path:{sufix:'/mysufix'}},
- get: {path:{sufix:'/mysufix'}}
+ list: {path:{suffix:'/mysufix'}},
+ get: {path:{suffix:'/mysufix'}}
}
});
expect(router.route.getCall(0).args[0]).equal('/public/users/mysufix');