REST API Generator for Google Datastore Entities
gstore-api is a NodeJS tool to generate RESTful APIs to interact with Google Datastore entities.
It is built on top of gcloud-node and the gstore-node library with its Entities Modeling definition.
Motivation
While I was coding the gstore-node library I was working on a 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 generate all the REST routes for CRUD operations on the Google Datastore entities.
Installation
npm install gstore-api --save
What do I get from it?
With just 6 lines of code (3 being a one time config), generate a full API (with data type & value validation) for a Datastore entity kind.
See the doc for all the information about gstore-node Model creation.
const router = require('express').Router();
const gstoreApi = require('gstore-api')();
const apiBuilder = gstoreApi.express(router);
const BlogPost = require('./blog-post.model');
const blogPostApì = apiBuilder.create(BlogPost);
app.use('/api/v1', blogPostApi);
Demo Application
Inside the "example" folder you will find a running application to demostrate how to use gstore-api. Follow the instruction in the README file.
Getting Started
settings
const gstoreApi = require('gstore-api')({ ...settings });
You can require gstore-api with or without settings. Those settings are optional, only define what you need. You only need to define them once for all your apis.
You will be able to override those settings or define them later when you create a Model api.
The settings object has the following properties:
{
host: { string }
contexts: { object }
readAll: { boolean }
showKey: { boolean }
}
host
The host of your API. It is needed to set the <Link> Header in the Response object when listing the entities.
This <Link> Header contains the next pageCursor for pagination.
If you don't specify the host, it will be auto-generated with the information coming in the Request object: req.protocol + '://' + req.get('host') + req.originalUrl
context
Contexts is an objects with 2 properties: "public" and "private" that specify a prefix 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 prefix to any route.
{
public : '',
private : '',
}
But for example if you require gstoreApi whit those settings:
const gstoreApi = require('gstore-api')({
contexts : {
'public' : '',
'private' : '/private'
}
});
...and you've defined an Auth middleware in your router for all routes containing '/private'
router.use('/private/', yourAuthMiddleware);
Then all the POST, PUT, PATCH and DELETE routes will automatically be routed through your Auth middleware.
Of course you could also leave the default contexts and then manually add a prefix to your API path (see below) like this: { path: '/private/user' }
for example.
readAll
Override the Model Schema property parameter read (here) to return all the properties of the entities. This setting can be overriden on each operation later.
showKey
Adds a "__key" property to the entity data with the complete Key from the Datastore. This setting can be overriden on each operation later.
Create an API for an Entity
You build an api with the "create" methods from the Express api builder.
apiBuilder.create(
MyModel,
settings,
);
@Returns -- the express Router.
Example
const router = require('express').Router();
const gstoreApi = require('gstore-api')();
const apiBuilder = gstoreApi.express(router);
const BlogPost = require('./blog-post.model');
module.exports = apiBuilder.create(BlogPost);
You can then pass the api to your Express app routes.
const blogPostRouter = require('./modules/blog/blog-post.router.js');
module.exports = (app) {
app.use('/api/v1', blogPostRouter);
}
const express = require('express');
const app = express();
require('./routes')(app);
settings (optional)
You can configure the api build by passing an optional object with the following parameters
{
path: '/end-point',
ancestors : 'Dad',
readAll, true,
showKey, true,
operations : {
list: {...},
get: {...}
create: {...},
udpatePatch: {...},
updateReplace: {...},
delete: {...},
deleteAll: {...},
}
}
path
Path to access the resource (our Model).
If not set the path to the resource is auto-generated with the following rules:
- lowercase
- dash for camelCase
- pluralize entity Kind
Example:
entity Kind path
----------------------------
'BlogPost' --> '/blog-posts'
'Query' --> '/queries'
ancestors
You can pass here one or several ancestors (entity Kinds) for the Model. The conversion from the entity kind to the path generated follows the same rules as mentioned above.
const Comment = require('./comment.model');
const commentApi = apiBuilder.create(Comment, { ancestors : 'BlogPost' });
GET /blog-posts/:anc0ID/comments
GET /blog-posts/:anc0ID/comments/:id
...
const commentApi = apiBuilder.create(Comment, { ancestors : ['Blog', 'BlogPost'] });
/blogs/:anc0ID/blog-posts/:anc1ID/comments
/blogs/:anc0ID/blog-posts/:anc1ID/comments/:id
operations
Operations can be any of
- list (GET all entities) --> call the list() query shortcut on Model. Documentation here.
- get (GET one entity)
- create (POST new entity)
- updatePatch (PATCH update entity) --> only update the properties sent
- updateReplace (PUT to update entity) --> replace all data for entity
- delete (DELETE one entity)
- deleteAll (DELETE all entities)
The list operation adds a Link Header (rel="next") with the link to the next page to fetch if there are more result.
You can then pass a pageCursor query param to fetch the next page passing the pageCursor.
Example: GET /blog-posts?pageCursor=abcdef123456
To set the limit of the number of entities returned, have a look at the documentation.
operation settings
Each operation can be configured with the following settings
{
handler: someController.someMethod,
middleware: someMiddleware,
exec: true,
options: {
readAll: false,
showKey: false,
},
path : {
prefix : 'additional-prefix',
suffix : 'additional-suffix',
}
}
handler
Your own hanlder method for the route generated.
Like any Express Router method, it receives the request and response arguments.
function controllerMethod(req, res) {
}`
middleware
You can specify a custom middleware for any operation. You might want for example to specify a middleware to upload a file.
const router = require('express').Router();
const gstoreApi = require('gstore-api')();
const apiBuilder = gstoreApi.express(router);
const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage });
const Image = require('./image.model');
const imageController = require('./image.controller');
const imageApi = apiBuilder.create(Image, {
operations: {
create: {
handler : imageController.create,
middleware : upload.single('file'),
},
updatePatch: {
handler : imageController.update,
middleware : upload.single('file'),
},
updateReplace: { exec: false },
}
});
POST /images
PATCH /images/:id
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
- readAll: (default: false) in case you have defined some properties in your Schema with
read: false
(see the doc), they won't show up in the response. If you want them in the response set this property to true. - showKey: (default: false). If set to "true" adds a "__key" property to the entity(ies) returned with the Datastore Entity Key.
Additional options 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
- limit
- order
- select
- ancestors -- except if already defined in api creation
- filters
path
You can add here some custom prefix or suffix to the path.
If you pass an <Array> of prefix like this ['', '/private', '/some-other-route']
, this will create 3 endPoints to access your entity (with all the correspondig verbs).
/my-entity
/private/my-entity
/some-other-route/my-entity
You could have then 2 middlewares (on routes containing 'private' or 'some-other-route') that could add some data to the request. You will then have to define a custom handler (Controller method) to deal with these differents scenarios and customize the data saved or returned. For example: outputing more data if the user is authenticated.
Important: this "path" setting will override the global "contexts" settings.
Example:
(...)
router.use('/private/', authMiddleware);
function authMiddleware(req, res, next) {
const token = req.headers['x-access-token'];
if (token) {
req.body.__auth = { role: 'admin' };
next();
} else {
return res.status(403).send({
success: false,
message: 'No token provided.'
});
}
}
const router = require('express').Router();
const gstoreApi = require('gstore-api')();
const apiBuilder = gstoreApi.express(router);
const BlogPost = require('./blog-post.model');
const blogPostController = require('./blog-post.controller');
module.exports = apiBuilder.create(BlogPost, {
operations: {
list: { handler: blogPostController.list }
}
});
const BlogPost = require('./blog-post.model');
const list = (req, res) => {
const settings = {};
const isAdmin = req.body.__auth && req.body.__auth.role === 'admin';
settings.start = req.query.pageCursor;
if (isAdmin) {
settings.filters = [];
settings.select = undefined;
settings.readAll = true;
settings.showKey = true;
}
BlogPost.list(settings)
.then((entities) => {
res.json(entities);
});
}
module.exports = {
list,
};