From 5fe06afbb117135f0b4889d49175b6c475e215bc Mon Sep 17 00:00:00 2001 From: sebelga Date: Sun, 17 Jul 2016 12:28:14 +0200 Subject: [PATCH 1/5] nextPageCursor in list() now in Link Header --- README.md | 108 ++++++++++++++++++++++++++++----------------- lib/index.js | 18 +++++--- test/index-test.js | 34 +++++++++++--- 3 files changed, 106 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index a660b6d..36e88a7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ for a mobile project. I found myself copying a lot of the same code to create al **Without** gstoreApi -``` +```js // blogPost.routes.js var BlogPostController = require('../controllers/blogPost.controller'); @@ -62,7 +62,7 @@ module.exports = BlogPostRoutes; ``` -``` +```js // blogPost.controller.js // BlogPost is a Datastools Model @@ -139,7 +139,7 @@ module.exports = { **With** gstoreApi -``` +```js // server.js var gstoreApi = require('datastore-api'); @@ -152,7 +152,7 @@ gstoreApi.init({ 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'); @@ -175,22 +175,27 @@ Before using gstoreApi you need to initiate the library with `gstoreApi.init({.. 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** The Express Router instance +**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. +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 : '' @@ -199,7 +204,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 : { @@ -211,7 +216,7 @@ gstoreApi.init({ And you defined an Auth middelware -``` +```js router.use('/private/', yourAuthMiddelware); ``` @@ -222,7 +227,7 @@ Then all the POST, PUT, PATCH and DELETE routes will automatically be routed thr To its simplest form, to create an API for a Model you just need to create a new instance of the gstoreApi with the Model. -``` +```js var gstoreApi = require('gstore-api'); var Model = require('../models/my-model'); @@ -233,32 +238,20 @@ new gstoreApi(Model); If you need some fine-tuning, the gstoreApi constructor has a second parameter where you can configure the following settings -``` +```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'] 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 : {...op setting see below}, + get : {...} create : {...}, udpatePatch : {...}, // PATCH :id updateReplace : {...}, // PUT :id delete : {...}, - deleteAll : {...} // exec defaults to false (for security) + deleteAll : {...} } } @@ -272,7 +265,7 @@ If not set the path to the resource is **auto-generated** with the following rul - dash for camelCase - pluralize entity Kind -``` +```js Example: entity Kind path @@ -285,7 +278,7 @@ entity Kind path #### ancestors 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'); @@ -320,7 +313,7 @@ new gstoreApi(Comment, { Operations can be any of -- list (GET all entities) +- list (GET all entities) --> call the list() query shortcut on Model - get (GET one entity) - create (POST new entity) - updatePatch (PATCH update entity) --> only update properties sent @@ -328,17 +321,38 @@ Operations can be any of - delete (DELETE one entity) - deleteAll (DELETE all entities) -The all have the **same configuration settings** with the following properties +The **list** operation calls the same method from the gstore-node shortcut query "list" as explained in the [documentation here](https://github.com/sebelga/gstore-node#list). + +##### op settings + +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** -Controller function to call. If you don't pass it it will default to get and return the entity from Google Datastore. +Custom Controller method to call. Like any Express Router method, it is passed the **request** and **response** object like this `function controllerMethod(req, res) {...}` + **middelware** -You can specify a custom middelware for any operation. You might one for example to specify a middleware for file upload for example. +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(); @@ -355,25 +369,37 @@ 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** -This property defines if the route for the operation is created (and executed) or not. Defaults to **true** except for "*deleteAll*" that you must manually set to true for security reason. +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 op it will pass this setting to the Model.list() action. And for get, create, and update(s) operation it will call entity.plain() +- **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 you already defined them in init()) +- **filters** + **path** -You can add here some custom prefix or sufix to the path. -Important: This will override the settings from the global "contexts". +You can add here some custom prefix or suffix to the path. + +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. + +Important: The path setting that you set here will override global "contexts" settings. diff --git a/lib/index.js b/lib/index.js index d4d3433..a9c2172 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']; @@ -71,6 +71,7 @@ class DatastoreApi { } this.defaultSettings = { + host : '', contexts: { public : '', private : '' @@ -97,6 +98,7 @@ 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) { @@ -113,11 +115,15 @@ class DatastoreApi { settings.ancestors = ancestors; } - this.Model.list(settings, (err, entities) => { + this.Model.list(settings, (err, result) => { if (err) { return errorsHandler.rpcError(err, res); } - res.json(entities); + if (result.nextPageCursor) { + // var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; + res.set('Link', '<' + _this.settings.host + req.originalUrl + '?pageCursor=' + result.nextPageCursor + '>; rel=next'); + } + res.json(result.entities); }); } } @@ -299,7 +305,7 @@ class DatastoreApi { fn: myController.method, path : { prefix: 'some-prefix', - sufix: 'some-sufix' + suffix: 'some-suffix' } } */ @@ -325,7 +331,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 +342,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/test/index-test.js b/test/index-test.js index 214c7fe..0f0d025 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -78,6 +78,7 @@ describe('Datastore API', function() { status:() => { return {json:() => {}} }, + set : () => {}, json:() => {} }; @@ -112,6 +113,7 @@ describe('Datastore API', function() { it('should merge settings passed', () => { var settings = { + host: '', contexts : { public:'/public', private : '/private' @@ -187,9 +189,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 +200,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,6 +208,24 @@ 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 read options', () => { var settings = { path:'/users', @@ -901,12 +921,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'); From 7194724ca9d2800299b784faa6bd13681caf35e1 Mon Sep 17 00:00:00 2001 From: sebelga Date: Sun, 17 Jul 2016 12:52:57 +0200 Subject: [PATCH 2/5] Added options passed to list() method to override the "list" shortcut query --- README.md | 18 +++++++++--------- lib/index.js | 9 +++++---- test/index-test.js | 20 +++++++++++++++++--- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 36e88a7..b9dda7d 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Then all the POST, PUT, PATCH and DELETE routes will automatically be routed thr ## Create an Entity API -To its simplest form, to create an API for a Model you just need to create a new instance of the gstoreApi with 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'); @@ -242,10 +242,10 @@ If you need some fine-tuning, the gstoreApi constructor has a second parameter w // 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 : {...op setting see below}, + list : {... see op setting below}, get : {...} create : {...}, udpatePatch : {...}, // PATCH :id @@ -261,7 +261,7 @@ If you need some fine-tuning, the gstoreApi constructor has a second parameter w #### path If not set the path to the resource is **auto-generated** with the following rules: -- start with lowercase +- lowercase - dash for camelCase - pluralize entity Kind @@ -346,7 +346,7 @@ Each operation has the **same configuration settings** with the following proper **fn** -Custom Controller method to call. Like any Express Router method, it is passed the **request** and **response** object like this `function controllerMethod(req, res) {...}` +Custom Controller method. Like any Express Router method, it has the **request** and **response** parameters. `function controllerMethod(req, res) {...}` **middelware** @@ -393,13 +393,13 @@ Extra options: - **limit** - **order** - **select** -- **ancestors** (except if you already defined them in init()) +- **ancestors** (except if already defined in global init()) - **filters** **path** You can add here some custom prefix or suffix to the path. -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. +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. -Important: The path setting that you set here will override global "contexts" settings. +Important: This "path" setting will override the global "contexts" settings. diff --git a/lib/index.js b/lib/index.js index a9c2172..a762bda 100644 --- a/lib/index.js +++ b/lib/index.js @@ -105,11 +105,12 @@ class DatastoreApi { 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; diff --git a/test/index-test.js b/test/index-test.js index 0f0d025..cf6d70d 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -231,7 +231,14 @@ describe('Datastore API', function() { path:'/users', op : { list : { - options : {simplifyResult:true} + options : { + simplifyResult:true, + limit : 13, + order : {property: 'title', descending:true}, + select : 'name', + ancestors : ['MyDad'], + filters : ['paid', true] + } } } }; @@ -239,13 +246,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); From 543aab8950577e9b57171844efee5cbf6592bf5f Mon Sep 17 00:00:00 2001 From: sebelga Date: Sun, 17 Jul 2016 15:44:29 +0200 Subject: [PATCH 3/5] Update Link Header in list() --- README.md | 15 +++++++++++---- lib/index.js | 11 ++++++++--- test/index-test.js | 16 +++++++++++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b9dda7d..0552dd3 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,18 @@ It is built on top of the [gstore-node](https://github.com/sebelga/gstore-node) + - [Motivation](#motivation) - [Installation](#installation) - [What do I get from it](#what-do-i-get-from-it) - [Getting started](#getting-started) - [Initiate library](#initiate-library) -- [Create an Entity API](#create-an-entity-api) +- [Create an API for an Entity](#create-an-api-for-an-entity) - [settings](#settings) + - [path](#path) + - [ancestors](#ancestors) + - [op](#op) + - [op settings](#op-settings) @@ -223,7 +228,7 @@ router.use('/private/', yourAuthMiddelware); Then all the POST, PUT, PATCH and DELETE routes will automatically be routed through your Auth middelware. -## Create an Entity API +## Create an API for an Entity 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. @@ -313,7 +318,7 @@ new gstoreApi(Comment, { Operations can be any of -- list (GET all entities) --> call the list() query shortcut on Model +- 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 properties sent @@ -321,7 +326,8 @@ Operations can be any of - delete (DELETE one entity) - deleteAll (DELETE all entities) -The **list** operation calls the same method from the gstore-node shortcut query "list" as explained in the [documentation here](https://github.com/sebelga/gstore-node#list). +**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. ##### op settings @@ -396,6 +402,7 @@ Extra options: - **ancestors** (except if already defined in global init()) - **filters** + **path** You can add here some custom prefix or suffix to the path. diff --git a/lib/index.js b/lib/index.js index a762bda..e9da940 100644 --- a/lib/index.js +++ b/lib/index.js @@ -71,7 +71,6 @@ class DatastoreApi { } this.defaultSettings = { - host : '', contexts: { public : '', private : '' @@ -116,13 +115,19 @@ class DatastoreApi { settings.ancestors = ancestors; } + if (req.query.pageCursor) { + settings.start = req.query.pageCursor; + } + this.Model.list(settings, (err, result) => { if (err) { return errorsHandler.rpcError(err, res); } if (result.nextPageCursor) { - // var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; - res.set('Link', '<' + _this.settings.host + req.originalUrl + '?pageCursor=' + result.nextPageCursor + '>; rel=next'); + 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); }); diff --git a/test/index-test.js b/test/index-test.js index cf6d70d..304f3bb 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -73,7 +73,12 @@ 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' + }; res = { status:() => { return {json:() => {}} @@ -226,6 +231,15 @@ describe('Datastore API', function() { 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', From eb8df42e2267801fd410d3e0bd1a8efc1cc1e15a Mon Sep 17 00:00:00 2001 From: sebelga Date: Sun, 17 Jul 2016 15:48:26 +0200 Subject: [PATCH 4/5] fix mock req --- test/index-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/index-test.js b/test/index-test.js index 304f3bb..fbb77b6 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -77,7 +77,8 @@ describe('Datastore API', function() { params:{id:123, anc0ID:'ancestor1', anc1ID:'ancestor2'}, query : {}, body:{title:'Blog Title'}, - get : () => 'http://localhost' + get : () => 'http://localhost', + originalUrl : '' }; res = { status:() => { From 5816797d54884bc4e6bf992deabcf12771a1053f Mon Sep 17 00:00:00 2001 From: sebelga Date: Sun, 17 Jul 2016 15:49:27 +0200 Subject: [PATCH 5/5] update package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": {