Skip to content

Commit

Permalink
Merge pull request #163 from opentable/batch-endpoint
Browse files Browse the repository at this point in the history
POST route for allowing batch request
  • Loading branch information
jankowiakmaria committed Jan 11, 2016
2 parents 193a001 + 43a1dd8 commit cdd4ce3
Show file tree
Hide file tree
Showing 24 changed files with 853 additions and 487 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ In case you would like to do the rendering yourself, try:

In this case you get the compiled view + the data, and you can do the rendering, eventually, interpolating the view-model data and rendering the compiled view with it.

When retrieving multiple components, a [batch POST endpoint](docs/registry-post-route.md) allows to make a single request to the API.

## Server-side rendering with node.js

First install the node.js client in your project:
Expand Down
30 changes: 30 additions & 0 deletions docs/registry-post-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Registry BATCH endpoint
=======================

It allows to retrieve a set of components with a single request to the API. While this should be convenient during the server-side rendering, it is not a good practice for client-side rendering.

# Server-side rendering via rest API using the post route

```sh
curl http://my-components-registry.mydomain.com/
-X POST
-d '{components:[{"name": hello-world", "version": "1.X.X"}, {"name": "my-component", "parameters": { "something": 2345 }}]}'

[{
"href": "https://my-components-registry.mydomain.com/hello-world/1.X.X",
"name": "hello-world",
"version": "1.0.0",
"requestVersion": "1.X.X",
"html": "Hello John doe!",
"type": "oc-component",
"renderMode": "rendered"
},{
"href": "https://my-components-registry.mydomain.com/my-component/?something=2345",
"name": "my-component",
"version": "1.0.0",
"requestVersion": "",
"html": "Bla bla",
"type": "oc-component",
"renderMode": "rendered"
}]
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"load-grunt-tasks": "0.6.0",
"mocha": "2.2.4",
"phantomjs": "^1.9.19",
"request": "^2.67.0",
"sinon": "1.14.1"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions registry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ module.exports = function(options){
}

app.get(options.prefix, router.listComponents);
app.post(options.prefix, router.components);

app.get(format('{0}:componentName/:componentVersion{1}', options.prefix, settings.registry.componentInfoPath), router.componentInfo);
app.get(format('{0}:componentName{1}', options.prefix, settings.registry.componentInfoPath), router.componentInfo);
Expand Down
2 changes: 1 addition & 1 deletion registry/middleware/cors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ module.exports = function(req, res, next){
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS, PUT');
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS, PUT, POST');
next();
};
2 changes: 2 additions & 0 deletions registry/router.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var ComponentRoute = require('./routes/component');
var ComponentsRoute = require('./routes/components');
var ComponentInfoRoute = require('./routes/component-info');
var ComponentPreviewRoute = require('./routes/component-preview');
var ListComponentsRoute = require('./routes/list-components');
Expand All @@ -10,6 +11,7 @@ var StaticRedirectorRoute = require('./routes/static-redirector');
module.exports = function(conf, repository){
return {
component: new ComponentRoute(conf, repository),
components: new ComponentsRoute(conf, repository),
componentInfo: new ComponentInfoRoute(repository),
componentPreview: new ComponentPreviewRoute(repository),
listComponents: new ListComponentsRoute(repository),
Expand Down
5 changes: 2 additions & 3 deletions registry/routes/component-preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@ var urlBuilder = require('../domain/url-builder');

module.exports = function(repository){
return function(req, res){

repository.getComponent(req.params.componentName, req.params.componentVersion, function(err, component){

if(err){
res.errorDetails = err;
res.errorCode = 'NOT_FOUND';
return res.json(404, { err: err });
}

var isHtmlRequest = !!req.headers.accept && req.headers.accept.indexOf('text/html') >= 0;

if(isHtmlRequest && !!res.conf.discovery){



return res.render('component-preview', {
component: component,
Expand Down
248 changes: 12 additions & 236 deletions registry/routes/component.js
Original file line number Diff line number Diff line change
@@ -1,249 +1,25 @@
'use strict';

var acceptLanguageParser = require('accept-language-parser');
var Cache = require('nice-cache');
var Domain = require('domain');
var format = require('stringformat');
var vm = require('vm');
var _ = require('underscore');

var Client = require('../../client');
var detective = require('../domain/plugins-detective');
var RequireWrapper = require('../domain/require-wrapper');
var sanitiser = require('../domain/sanitiser');
var strings = require('../../resources');
var urlBuilder = require('../domain/url-builder');
var validator = require('../domain/validators');
var GetComponentHelper = require('./helpers/get-component');

module.exports = function(conf, repository){

var client = new Client(conf),
cache = new Cache({
verbose: !!conf.verbosity,
refreshInterval: conf.refreshInterval
});
var getComponent = new GetComponentHelper(conf, repository);

return function(req, res){

var requestedComponent = {
getComponent({
conf: res.conf,
headers: req.headers,
name: req.params.componentName,
version: req.params.componentVersion || '',
parameters: req.query
};

var conf = res.conf,
componentCallbackDone = false;

repository.getComponent(requestedComponent.name, requestedComponent.version, function(err, component){

// check route exist for component and version
if(err){
res.errorDetails = err;
return res.json(404, { err: err });
parameters: req.query,
version: req.params.componentVersion
}, function(result){
if(!!result.response.error){
res.errorCode = result.response.code;
res.errorDetails = result.response.error;
}

// check component requirements are satisfied by registry
var pluginsCompatibility = validator.validatePluginsRequirements(component.oc.plugins, conf.plugins);

if(!pluginsCompatibility.isValid){
res.errorDetails = format(strings.errors.registry.PLUGIN_NOT_IMPLEMENTED, pluginsCompatibility.missing.join(', '));
res.errorCode = 'PLUGIN_MISSING_FROM_REGISTRY';

return res.json(501, {
code: res.errorCode,
error: res.errorDetails,
missingPlugins: pluginsCompatibility.missing
});
}

// sanitise and check params
var params = sanitiser.sanitiseComponentParameters(requestedComponent.parameters, component.oc.parameters),
validationResult = validator.validateComponentParameters(params, component.oc.parameters);

if(!validationResult.isValid){
res.errorDetails = validationResult.errors.message;
return res.json(400, { error: res.errorDetails });
}

var returnComponent = function(err, data){

if(componentCallbackDone){ return; }
componentCallbackDone = true;

if(!!err){
res.errorDetails = format(strings.errors.registry.COMPONENT_EXECUTION_ERROR, err.message || '');
return res.json(500, { error: res.errorDetails, details: { message: err.message, stack: err.stack, originalError: err} });
}

var componentHref = urlBuilder.component({
name: component.name,
version: requestedComponent.version,
parameters: params
}, res.conf.baseUrl);

var response = {
href: componentHref,
type: res.conf.local ? 'oc-component-local' : 'oc-component',
version: component.version,
requestVersion: requestedComponent.version,
name: requestedComponent.name
};

if(req.headers.accept === 'application/vnd.oc.prerendered+json' ||
req.headers.accept === 'application/vnd.oc.unrendered+json'){
res.json(200, _.extend(response, {
data: data,
template: {
src: repository.getStaticFilePath(component.name, component.version, 'template.js'),
type: component.oc.files.template.type,
key: component.oc.files.template.hashKey
},
renderMode: 'unrendered'
}));
} else {

var cacheKey = format('{0}/{1}/template.js', component.name, component.version),
cached = cache.get('file-contents', cacheKey),
key = component.oc.files.template.hashKey,
options = {
href: componentHref,
key: key,
version: component.version,
name: component.name,
templateType: component.oc.files.template.type,
container: (component.oc.container === false) ? false : true,
renderInfo: (component.oc.renderInfo === false) ? false : true
};

var returnResult = function(template){
client.renderTemplate(template, data, options, function(err, html){
res.json(200, _.extend(response, {
html: html,
renderMode: 'rendered'
}));
});
};

if(!!cached && !res.conf.local){
returnResult(cached);
} else {
repository.getCompiledView(component.name, component.version, function(err, templateText){
var context = { jade: require('jade/runtime.js')};
vm.runInNewContext(templateText, context);
var template = context.oc.components[key];
cache.set('file-contents', cacheKey, template);
returnResult(template);
});
}
}
};

if(!component.oc.files.dataProvider){
returnComponent(null, {});
} else {

var cacheKey = format('{0}/{1}/server.js', component.name, component.version),
cached = cache.get('file-contents', cacheKey),
domain = Domain.create(),
contextObj = {
acceptLanguage: acceptLanguageParser.parse(req.headers['accept-language']),
baseUrl: conf.baseUrl,
env: conf.env,
params: params,
plugins: conf.plugins,
staticPath: repository.getStaticFilePath(component.name, component.version, '').replace('https:', '')
};

var setCallbackTimeout = function(){
if(!!conf.executionTimeout){
setTimeout(function(){
returnComponent({
message: format('timeout ({0}ms)', conf.executionTimeout * 1000)
});
}, conf.executionTimeout * 1000);
}
};

if(!!cached && !res.conf.local){
domain.on('error', returnComponent);

try {
domain.run(function(){
cached(contextObj, returnComponent);
setCallbackTimeout();
});
} catch(e){
return returnComponent(e);
}
} else {
repository.getDataProvider(component.name, component.version, function(err, dataProcessorJs){

if(err){
componentCallbackDone = true;
res.errorDetails = strings.errors.registry.RESOLVING_ERROR;
return res.json(502, { error: res.errorDetails });
}

var context = {
require: new RequireWrapper(res.conf.dependencies),
module: {
exports: {}
},
console: res.conf.local ? console : { log: _.noop },
setTimeout: setTimeout,
Buffer: Buffer
};


var handleError = function(err){

if(err.code === 'DEPENDENCY_MISSING_FROM_REGISTRY'){
res.errorDetails = format(strings.errors.registry.DEPENDENCY_NOT_FOUND, err.missing.join(', '));
res.errorCode = err.code;
componentCallbackDone = true;

return res.json(501, {
code: res.errorCode,
error: res.errorDetails,
missingDependencies: err.missing
});
}

var usedPlugins = detective.parse(dataProcessorJs),
unRegisteredPlugins = _.difference(usedPlugins, _.keys(res.conf.plugins));

if(!_.isEmpty(unRegisteredPlugins)){

res.errorDetails = format(strings.errors.registry.PLUGIN_NOT_FOUND, unRegisteredPlugins.join(' ,'));
res.errorCode = 'PLUGIN_MISSING_FROM_COMPONENT';
componentCallbackDone = true;

return res.json(501, {
code: res.errorCode,
error: res.errorDetails,
missingPlugins: unRegisteredPlugins
});
}

returnComponent(err);
};

try {
vm.runInNewContext(dataProcessorJs, context);
var processData = context.module.exports.data;
cache.set('file-contents', cacheKey, processData);

domain.on('error', handleError);
domain.run(function(){
processData(contextObj, returnComponent);
setCallbackTimeout();
});
} catch(err){
handleError(err);
}
});
}
}
return res.json(result.status, result.response);
});
};
};
Loading

0 comments on commit cdd4ce3

Please sign in to comment.