Overview
CompoSR is a nodeJS middleware, built on top of express, for Corbel.
It offers developers the ability to make their own specific application API based in corbel-js
It is responsible for composing phrases of code than can be reused by multiple applications. This way you can keep your code reusable and simplify your API.
wiki: A composer (Latin com+ponere, literally "one who puts together") is a person who creates music.
wiki: In music and music theory, phrase and phrasing are concepts and practices related to grouping consecutive melodic notes, both in their composition and performance. A musical work is typically made up of a melody that consists of numerous consecutive phrases.
Diagram
QuickStart
Postman Playground
- Get postman
- Import corbel-composer collection:
https://raw.githubusercontent.com/bq/corbel-composer/master/doc/postman/postman.json
- Import evironment example:
https://raw.githubusercontent.com/bq/corbel-composer/master/doc/postman/environment.example.json
- Import globals:
https://raw.githubusercontent.com/bq/corbel-composer/master/doc/postman/globals.example.json
- Enjoy!
Phrase Model
{
"url": "phraseName",
"get": {
"code": "res.render('index', {title: 'hello world'});",
"doc": {
"description": "Phrase description",
"queryParameters": {
"param1": {
"type": "number",
"description": "Param description",
"default": 0
}
},
"responses": {
"200": {
"body": {
"application/json": {
"schema": "{\n\t"$schema": "http://json-schema.org/schema",\n\t"type": "object",\n\t"description": "A canonical song",\n\t"properties": {\n\t\t"title": {\n\t\t\t"type": "String"\n\t\t},\n\t\t"artist": {\n\t\t\t"type": "String"\n\t\t}\n\t},\n\t"required": ["title", "artist"]\n}"
}
}
}
}
}
}
}
Writting CompoSR phrases
All the phrases are wrapped inside this Function
closure, meaning you can access any of it's params:
function phrase(req, res, next, corbelDriver, corbel, ComposerError, domain, _, q, request, compoSR){
//Your phrase code
}
- q : A library for promises
- _: Lodash, a library for modular utilities
- request: Simplified HTTP request client
- ComposerError: Our custom error manager.
new ComposerError('error:undefined:name', 'Please insert a name', 400)
- domain : Your domain name
- req, res, next : express request parameters
- corbel: Corbel JavaScript SDK with an extended
corbel.generateDriver
function that generates corbelDriver
instances with the correct urlBase
. - corbelDriver : An instance of corbelDriver provided by corbel, instantiated with your
Authorization
header if provided - compoSR : A snippet runner
count
value in collections query
{
"url": "countExample",
"get": {
"code": "CORBEL-JS_SNIPPET"
}
}
where code
should be a string with this corbel-js snippet:
var count;
corbelDriver.resources.collection('test:ComposrTest').get(undefined, {
aggregation: {
$count: '*'
}
}).then(function(response) {
count = response.data.count;
return corbelDriver.resources.collection('test:ComposrTest').get();
}).then(function(response) {
res.send({
data: response.data,
'count': count
});
}).catch(function(error) {
res.send(error);
});
Path & query parameters
{
"url": "paramsExample/:pathparam",
"get": {
"code": "res.status(200).send('path param: ' + req.params.pathparam + ', query param: ' + req.query.queryparam);"
}
}
Reference
API design best practices
Naming
- Use nouns not verbs
- Use plural nouns
Resource | GET (read) | POST (create) | PUT (update) | DELETE |
---|
/cars | Returns a list of cars | Create a new ticket | Bulk update of cars | Delete all cars |
/cars/711 | Returns a specific car | Method not allowed (405) | Updates a specific ticket | Deletes a specific ticket |
/purchase | Get al purchases | Create a new purchase | Bulk update of purschases | Delete all purchases |
/purchase/85 | Returns a purchase | Method not allowed (405) | Updates a specific purchase | Delete all purchases |
Resource GET
read POST
create PUT
update DELETE
/cars Returns a list of cars Create a new ticket Bulk update of cars Delete all cars
/cars/711 Returns a specific car Method not allowed (405) Deletes a specific ticket
Versioning your phrases
A simple way to achieve this is definning the phrase version in the url, like this
{
"url": "v1/paramsExample/:pathparam",
"get": { ... }
}
A phrase version should change only if the phrase contract is broken
Reference
Run in a docker container
-
clone repo
-
build image
docker build -t <username>/corbel-composer .
-
run container
docker run -d -p 3000:3000 --name="corbel-composer" <username>/corbel-composer
-
start/stop container
docker start/stop corbel-composer
Tests
npm test
Coverage
grunt test:coverage
Debug
Requires node-inspector
npm install -g node-inspector
-
Server
npm run debug
-
Tests
npm run test:debug
Example code for phrases
Login a client/application
Prerequisites for login a client/application: Generate a jwt
NodeJS example
var corbel = require('corbel-js');
function generateAssertion(claims, clientSecret) {
claims.aud = corbel.Iam.AUD;
return corbel.jwt.generate(claims, clientSecret);
}
var claims = {
iss: credentials.clientId,
scope: credentials.scopes
};
var jwt = generateAssertion(claims, credentials.clientSecret);
Login client/application phrase code
if (!req.body || !req.body.jwt) {
res.status(401).send(new ComposerError('error:jwt:undefined', '', 401));
}
var corbelDriver = corbel.generateDriver({iamToken: ''});
corbelDriver.iam.token().create({
jwt: req.body.jwt
}).then(function(response) {
corbelDriver.assets().access();
res.send(response.data);
}).catch(function(err){
compoSR.run('global:parseError', { err : err, res : res});
});
Login a user
Prerequisites for login a user: Generate a jwt
NodeJS example
var corbel = require('corbel-js');
function generateAssertion(claims, clientSecret) {
claims.aud = corbel.Iam.AUD;
return corbel.jwt.generate(claims, clientSecret);
}
var claims = {
iss: appCredentials.clientId,
'basic_auth.username': userCredentials.username,
'basic_auth.password': userCredentials.password,
scope: userCredentials.scopes
};
var jwt = generateAssertion(claims, appCredentials.clientSecret);
Login user phrase code
if (!req.body || !req.body.jwt) {
return res.status(401).send(new ComposerError('error:jwt:undefined', '', 401));
}
var corbelDriver = corbel.generateDriver({iamToken: ''});
var tokenObject;
return corbelDriver.iam.token().create({
jwt : req.body.jwt
}).then(function(response){
corbelDriver.assets().access();
tokenObject = response.data;
corbelDriver = corbel.generateDriver({
iamToken : tokenObject
});
return corbelDriver.iam.user('me').get();
}).then(function(response){
return res.send({
tokenObject: tokenObject,
user: response.data
});
}).catch(function(err){
return compoSR.run('global:parseError', { err : err, res : res});
});
Refresh a token
Prerequisites for refreshing a user token: Generate a jwt with the refresh token
NodeJS example
var corbel = require('corbel-js');
function generateAssertion(claims, clientSecret) {
claims.aud = corbel.Iam.AUD;
return corbel.jwt.generate(claims, clientSecret);
}
var claims = {
iss: appCredentials.clientId,
'refresh_token': refresh_token,
scope: userCredentials.scopes
};
var jwt = generateAssertion(claims, appCredentials.clientSecret);
Refresh token phrase code
if (!req.body || !req.body.jwt) {
res.status(400).send(new ComposerError('error:jwt:undefined', 'JWT is missing', 400));
return false;
}
var decoded = corbel.jwt.decode(req.body.jwt);
if(Object.keys(decoded).length === 0){
res.status(400).send(new ComposerError('error:jwt:malformed', 'Your JWT is malformed', 400));
return false;
}
if(!decoded.refresh_token){
res.status(400).send(new ComposerError('error:jwt:refresh_token:missing', 'Add the refresh_token property', 400));
return false;
}else{
var refreshTokenDecoded = corbel.jwt.decode(decoded.refresh_token);
if(Object.keys(refreshTokenDecoded).length === 0){
res.status(400).send(new ComposerError('error:jwt:refresh_token:malformed', 'Your refresh_token is malformed', 400));
return false;
}
}
var corbelDriver = corbel.generateDriver({iamToken: ''});
corbelDriver.iam.token().create({
jwt : req.body.jwt
})
.then(function(response){
res.send(response.data);
})
.catch(function(err){
compoSR.run('global:parseError', { err : err, res : res});
});
Logout a user
Prerequisites for login out a user: have an accessToken
NodeJS example
var http = require('http');
var accessToken = "xxxxx":
var post_options = {
host: 'composrendpoint.composr',
path: '/logoutuser',
method: 'POST',
headers: {
'Authorization': accessToken
}
};
http.request(post_options, function(res) {
});
Logout user phrase code
if (!req.get('Authorization')) {
res.status(401).send(new ComposerError('error:unauthorized', 'Authorization missing', 401));
}
var method = req.params.type && req.params.type === 'all' ? 'disconnect' : 'signOut';
corbelDriver.iam.user('me')[method]()
.then(function(response){
res.send(response.data);
}).catch(function(err){
compoSR.run('global:parseError', { err : err, res : res});
});
Logout a user from all devices:
if (!req.get('Authorization')) {
throw new ComposerError('error:unauthorized', 'Authorization missing', 401);
}
corbelDriver.iam.user('me')
.disconnect()
.then(function(response){
res.send(response.data);
}).catch(function(err){
compoSR.run('global:parseError', { err : err, res : res});
});
Return current user info
corbelDriver.iam.user('me').get();
Library
Get library phrase code
if (!req.get('Authorization')) {
throw new ComposerError('error:authorization:undefined', '', 401);
}
var books = [];
for( var i = 0; i < 20; i++){
books.push({
_id: Date.now(),
_createdAt: new Date("2015-05-08T14:37:37.628Z"),
_src_id: "Libranda",
_dst_id: "books:Book/7004c092",
isbn: "9788415564430",
distributorId: "LIBR",
title: "Remedio: la geografía",
synopsis: "",
authors: [
{
name: "Luigi Pirandello",
biographicalNote: ""
}
],
rawCategories: [
"FA"
],
storeCategories: [
"F",
"FA"
],
cover: "http://www.nordicalibros.com/upload/fgr02102012145613.jpg",
format: "epub",
language: "spa",
publisherGroupName: "Nordica Libros",
publishingTime: 1347753600000,
_updatedAt: new Date("2015-05-08T14:37:37.628Z"),
_order: 1
});
}
res.send({
data : books,
count : books.length
});
Code snippets
Code snippets are a minor form of phrases
, they are accesible through the compoSR
object on your phrases.
You can run your code snippets by executing compoSR.run('snippetName', params);
where params
is anything you want it to be. From your snippets you will be allowed to access to the params
variable and the compoSR
object itself.
compoSR
will be allowed to access any snippets defined in your domain and your parent domains.
For example, _silkroad:composer
will be able to access all the _silkroad:composer
snippets and all the _silkroad
snippets. If a snippet has the same name on both of the domains, the one with a deepest hierarchy will overwrite the first one.
Let's take a look at it:
var snippets = {
'domainName' : [
{
name : 'myFunction',
code : 'compoSR.run("hello", "world")'
},
{
name : 'hello',
code: 'console.log(params);'
}
],
'domainName:childDomain' : [
{
name : 'hello',
code: 'console.log("I am the child: ", params);'
}
],
}
- If we run the
myFunction
snippet, accesing from a client that belongs to the domain named domainName:childDomain
it will show this:
compoSR.run('myFunction');
- If the client or user belongs to the domain named
domainName
and we execute the same function we'll get:
compoSR.run('myFunction');
Logs
Logs are written to the linux syslog and in the logs folder.
You can set logFile
and logLevel
in your config file.
Available log levels can be found at winston's npm page:
You can disable syslog by setting syslog
property to false
in the config file.