yukon
Advanced tools
Comparing version 0.0.4 to 0.0.5
54
api.js
var fs = require('fs'); | ||
var path = require('path'); | ||
var sa = require('superagent'); | ||
var _ = require('lodash'); | ||
@@ -8,35 +10,38 @@ module.exports = function(app, config) { | ||
return { | ||
callApi: callApi, | ||
getData: getData, | ||
}; | ||
// route call to get stub or use live API | ||
function callApi(req, res, next, callArgs) { | ||
debug("callApi called, callArgs = " + callArgs); | ||
function getData(callArgs, req, res, next) { | ||
debug("getData called"); | ||
if (callArgs.stubPath) | ||
readStub(req, res, next, callArgs); | ||
if (callArgs.useStub) | ||
readStub(callArgs, req, res, next); | ||
else | ||
getData(req, res, next, callArgs); | ||
callApi(callArgs, req, res, next); | ||
} | ||
// call live API - return data as res.locals[namespace] (namespace = data1, data2, data3 etc. for component level API calls) | ||
function getData(req, res, next, callArgs) { | ||
debug('getData called, namespace = ' + callArgs.namespace); | ||
function callApi(args, req, res, next) { | ||
// if path ends with '/', assume it gets an id from the express request :id matcher | ||
callArgs.apiPath = callArgs.apiPath.match(/\/$/) ? callArgs.apiPath + req.params.id : callArgs.apiPath; | ||
callArgs.apiVerb = callArgs.apiVerb || 'get'; | ||
callArgs.apiParams = callArgs.apiParams || {}; | ||
callArgs.apiBodyType = callArgs.apiBodyType || 'json'; | ||
callArgs.paramMethod = (callArgs.apiVerb === 'get') ? 'query' : 'send'; | ||
debug('callApi called, namespace = ' + args.namespace); | ||
var callArgs = _.assign(_.cloneDeep(config.apiDefaults), args); | ||
callArgs.paramMethod = (callArgs.verb === 'get') ? 'query' : 'send'; | ||
// MAGIC ALERT: if path ends with '/', assume it gets an id from the express request :id matcher | ||
callArgs.path = callArgs.path.match(/\/$/) ? callArgs.path + req.params.id : callArgs.path; | ||
console.log(callArgs); | ||
config.beforeApiCall(callArgs, req, res); | ||
var call = sa | ||
[callArgs.apiVerb](callArgs.apiPath) | ||
[callArgs.paramMethod](callArgs.apiParams) | ||
.type(callArgs.apiBodyType) | ||
[callArgs.verb](callArgs.path) | ||
[callArgs.paramMethod](callArgs.params) | ||
.type(callArgs.bodyType) | ||
.timeout(callArgs.timeout); | ||
callArgs.customHeaders.forEach(function(header) { | ||
debug('adding custom header: '+header.name+'='+header.value); | ||
call.set(header.name, header.value); | ||
@@ -61,9 +66,16 @@ }); | ||
// return stub data as res.locals[namespace] (namespace = data1, data2, data3 etc. for component level API calls) | ||
function readStub(req, res, next, callArgs) { | ||
debug('loooking for stub - ' + callArgs.stubPath); | ||
// return stub data as res.locals[namespace] same as API | ||
function readStub(callArgs, req, res, next) { | ||
// MAGIC ALERT: framework assumes the stub name = nodule name if no stubPath is supplied | ||
var stubName = callArgs.stubPath || req.nodule.name; | ||
var stub = (stubName.indexOf('/') > -1) | ||
? path.join(process.cwd(), stubName) | ||
: path.join(req.nodule.path, stubName+'.stub.json'); | ||
debug('loooking for stub - ' + stub); | ||
var data = {}; | ||
try { | ||
data = fs.readFileSync(callArgs.stubPath); | ||
data = fs.readFileSync(stub); | ||
debug('stub found!, namespace='+callArgs.namespace); | ||
@@ -70,0 +82,0 @@ } |
135
doApi.js
@@ -1,125 +0,50 @@ | ||
// calls all APIs in parallel (inlcuding those added by the app-level appDoApi middleware) | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var _ = require('lodash'); | ||
var debug; | ||
// calls all APIs in parallel (inlcuding those added by the app-level appDoApi middleware) | ||
module.exports = function(app, config) { | ||
debug = config.debug('yukon->doApi'); | ||
var api = require('./api.js')(app, config); | ||
// called as route comes in, before it goes to API | ||
return function(req, res, next) { | ||
debug("called"); | ||
console.log(req.nodule.apiCalls); | ||
if (req.nodule.apiCalls.length > 0) { | ||
// skip if nodule doesn't have any API calls | ||
if (req.nodule.skipApi) { | ||
next(); | ||
return; | ||
} | ||
var dataIdx = 1; | ||
req.nodule.apiCalls.forEach(function(apiCall, index) { | ||
if (!apiCall.namespace) apiCall.namespace = "data" + dataIdx++; | ||
}); | ||
var apiCalls = req.nodule.apiCalls || [], apiArgs = req.nodule.apiArgs || [], nodule = req.nodule; | ||
processStubs(app, req); | ||
var apiParamsIsArray = false; | ||
if (typeof nodule.apiPath === "string") | ||
nodule.apiPath = [nodule.apiPath]; | ||
// need to identify when apiPath is an array and params are effectively an array (although still technically an object) of objects | ||
else if (nodule.apiPath && !_.isEmpty(nodule.apiParams) && _.keys(nodule.apiParams)[0].match(/\d+/)) | ||
apiParamsIsArray = true; | ||
_.each(nodule.apiPath, function(apiPath, index) { | ||
apiCalls.unshift(req.nodule.callApi); // add apiCalls to front of apiCalls array in case app-level apiCalls have no apiArgs | ||
apiArgs.unshift({ | ||
apiPath : apiPath, | ||
apiHost : getValue(nodule.apiHost, index), | ||
apiParams : getValue(nodule.apiParams, index, apiParamsIsArray), | ||
apiVerb : getValue(nodule.apiVerb, index), | ||
apiBodyType : getValue(nodule.apiBodyType, index), | ||
handleError : getValue(nodule.handleError, index), | ||
timeout : getValue(nodule.timeout, index), | ||
stubPath : getValue(nodule.stubPath, index), | ||
namespace : "data" + (1*index+1) }); | ||
}); | ||
if (apiCalls.length > 0) { | ||
parallel(apiCalls, req, res, next, apiArgs); | ||
parallel(req.nodule.apiCalls, req, res, next); | ||
} | ||
else | ||
else { | ||
next(); | ||
} | ||
}; | ||
}; | ||
// return correct value for property if it is an array, or return the same value for every API if the property is not an array | ||
function getValue(property, index, forceArray) { | ||
if (property && (property instanceof Array || forceArray)) | ||
return property[index]; | ||
else | ||
return property; | ||
} | ||
// invokes N number of api calls, then invokes the express next() when all have returned or one returns an error | ||
function parallel(apiCalls, req, res, next) { | ||
debug('parallel started!! # calls:' + apiCalls.length); | ||
var results = 0; | ||
var errorReceived = null; | ||
function processStubs(app, req) { | ||
if (req.nodule.forceStub) { | ||
apiCalls.forEach(function(apiCall){ | ||
api.getData(apiCall, req, res, function(err){ | ||
if (err && !errorReceived) { | ||
errorReceived = err; | ||
next(err); // if any error - send full request into error flow, exit out of parallel calls | ||
} | ||
var nodule = req.nodule; | ||
// set one stub to array to share stub finding logic with multiple stub scenario | ||
if (typeof nodule.stubName === "string") { | ||
nodule.stubName = [nodule.stubName]; | ||
} | ||
// if there is no stub we need to create an array of the same length as apiPath - kind of awkward but it's just stub mode | ||
else if (!nodule.stubName) { | ||
// if apiPath is a string then look for one stub, if it's an array look for that many stubs, if there is no apiPath don't look for stubs. | ||
var arrayLength = (typeof nodule.apiPath === 'string') ? 1 : ((nodule.apiPath) ? nodule.apiPath.length : 0); | ||
nodule.stubName = new Array(arrayLength); | ||
// initialize array with strings | ||
for (var i=0; i<arrayLength; i++) | ||
nodule.stubName[i] = ""; | ||
} | ||
// loop over all stubs | ||
nodule.stubPath = []; | ||
var stubPath, sharedPath; | ||
_.each(nodule.stubName, function(stubName, index) { | ||
stubName = (stubName) ? stubName : nodule.name; // "guess" the nodule name if no stubName is supplied | ||
stubPath = path.join(nodule.path, stubName+'.stub.json'); | ||
if (nodule.sharedStubPath) | ||
sharedPath = path.join(process.cwd(), nodule.sharedStubPath, stubName+'.stub.json'); // TODO - make config setting | ||
// look for stob in nodule folder, then shared folder, if no stub is found use API | ||
if (fs.existsSync(stubPath)) | ||
nodule.stubPath.push(stubPath); | ||
else if (sharedPath && fs.existsSync(sharedPath)) | ||
nodule.stubPath.push(sharedPath); | ||
results++; | ||
if (!errorReceived && results === apiCalls.length) { | ||
debug('parallel done!!'); | ||
next(); // if all calls return successfully call the express next() | ||
} | ||
}); | ||
}); | ||
debug('nodule name = ' + nodule.name + ', stubPath = ' + nodule.stubPath); | ||
} | ||
} | ||
// grabs N number of middleware methods, calls next() when all have returned | ||
// optional args array matches each element in the apiMiddlewares array | ||
function parallel(apiMiddlewares, req, res, next, args) { | ||
debug('parallel started!! # calls:' + apiMiddlewares.length); | ||
var results = 0; | ||
var errorReceived = null; | ||
if (!args) args = []; | ||
apiMiddlewares.forEach(function(middleware, index){ | ||
middleware(req, res, function(err){ | ||
// if error send full request into error flow (errors are logged at API level) | ||
if (err && !errorReceived) { | ||
errorReceived = err; | ||
next(err); | ||
} | ||
results++; | ||
if (!errorReceived && results === apiMiddlewares.length) { | ||
debug('parallel done!!'); | ||
next(); | ||
} | ||
}, args[index]); | ||
}); | ||
} | ||
}; |
114
index.js
@@ -10,7 +10,5 @@ var _ = require('lodash'); | ||
var api = require('./api.js')(app, yukonConfig); | ||
yukonConfig.noduleDefaults.middlewares = [ | ||
config.appPreApi || passThrough, | ||
require('./preApi')(app, yukonConfig, api), // preprocessing logic before APIs are called | ||
require('./preApi')(app, yukonConfig), // preprocessing logic before APIs are called | ||
@@ -36,17 +34,11 @@ config.appDoApi || passThrough, | ||
var defaultConfig = { | ||
// called at the start of every api call if defined | ||
// called at the start of every api or stub call | ||
beforeApiCall: null, | ||
// called after every api call if defined | ||
// called after every api call | ||
afterApiCall: null, | ||
// called after every stub call if defined | ||
// called after every stub call | ||
afterStubCall: null, | ||
// directory to start in for templateNames with a directory in them | ||
templateRoot: 'nodules', | ||
// alternate place to look for stubs than the nodule dir | ||
sharedStubPath: null, | ||
// default debug function | ||
@@ -60,20 +52,7 @@ yukonCustomDebug: function(identifier) { | ||
noduleDefaults: { | ||
// array of middleware functions to be executed on each request, set in yukon init method | ||
middlewares: null, | ||
// use to skip API call(s) altogether | ||
skipApi: false, | ||
// array of optional functions to be called with the apis specified in the nodule (IE - global or semi-global calls like getProfile, getGlobalNav) | ||
// these can be conditional per nodule/request if set in the appDoApi middleware | ||
apiCalls: [], | ||
// arguments (if needed) to go with the apiCalls above | ||
apiAgs: [], | ||
// Properties inherited from nodule.js (see nodule conf (TODO:link here) as these may get out of date): | ||
// middlewares (REQUIRED) - array of (or function which returns array of) middleware functions which will be called in order for each nodule on each express request | ||
// route (REQUIRED) - needs to be defined in each nodule, and be unique | ||
// routeVerb - (default:get) | ||
// routeIndex - (default:0) | ||
// middlewares - array of middleware functions to be executed on each request, defined in yukon module init | ||
@@ -83,60 +62,59 @@ // NOTE: the params below call be mutated in the preProcessor using this.myParam notation | ||
// app looks for [nodule name].[ templateExt ] if not specified and request is not JSON | ||
// MAGIC ALERT: if template name is null, the framework looks for [nodule name].templateExt | ||
// first in the nodule folder, then in the shared template folder | ||
templateName: null, | ||
// app looks for templates with the same filename + this extension | ||
// the framework looks for templates with the template name + this extension | ||
templateExt: '.jade', | ||
// can be array or string - path to API server, can be used to over-ride default | ||
apiHost: null, | ||
// 'html', 'json' only current values - use this to force any nodule to behave like a json or html call regardless of naming conventions or directory conventions | ||
contentType: null, | ||
// path to API - note: if path ends with a / framework automatically appends req.params.id | ||
// Notes on multiple APIs per component: | ||
// 1. Set the apiPath property to an array instead of a string. | ||
// 2. The app places each API call in res.locals.data1, res.locals.data2, etc. | ||
// 3. If any of the APIs needs params, make sure you put them in the correct slot. IE - apiParams[1] = {indludecta:'true'}; | ||
// 4. If an array of stubNames is created - those will be used in place of the corresponding APIs when in stub mode. | ||
// 6. As before, to use the same stub for each call just leave stubName blank (looks for standard name) or specify a custom string. | ||
// 7. As always you can set any of the properties above dynamically inside your preProcessor using the this.[propertyName] nomenclature. | ||
apiPath: null, | ||
// use to manipulate query params or other business logic before api call(s) | ||
preProcessor: function(req, res) { }, | ||
// params to send to API server | ||
// If apiVerb is 'post', this can be a deep json object (apiBodyType=json) or a shallow object of name value pairs (apiBodyType=form) | ||
// If using multiple apiPaths, make sure you put the apiParams in the correct slot. IE - apiParams[1] = {indludecta:'true'}; | ||
apiParams: {}, | ||
// use to process data returned from the API before calling template or sending back to client as JSON | ||
postProcessor: function(req, res) { }, | ||
// NOTE: one important property you usually need to set in the postProcessor is res.renderData | ||
// this is the data sent to the jade template or back to the client as JSON | ||
// MAGIC ALERT: if you don't specify res.renderData the framework sets res.renderData = res.locals.data1 | ||
// valid values: get, post, put, del - or array of these if different values are needed for different calls (uses 'del' since delete is a reserved word) | ||
apiVerb: 'get', | ||
// set this.error to an Error() instance to call next(error) inside the preProcessor or postProcessor | ||
error: null, | ||
// valid values: json, form - or array of these if different values are needed for different calls (use 'form' for a standard post submit with name/value pairs - everything else is json body) | ||
apiBodyType: 'json', | ||
// array of apiCalls to call in parallel | ||
// NOTE: global or semi-global calls like getProfile, getGlobalNav, etc. can be added to this array in the appDoApi middleware | ||
apiCalls: [], | ||
}, | ||
// set to > 0 (milliseconds) force stub calls to simualate a longer api call - works for API or Stubs | ||
// WARNING: don't forget to turn this off in production mode! | ||
apiSleep: 0, | ||
/// API CALL PROPERTIES //////////////////////////////////////////////////////////////// | ||
/// NOTE: there can be multiple api calls per nodule, all called in parallel | ||
apiDefaults: { | ||
// path to server, can be used to over-ride default | ||
host: null, | ||
// (numeric) - max API return time in ms, needs to be array if more than one API is called, set other values to null to use default - IE - [null,20000,null] | ||
timeout: null, | ||
// MAGIC ALERT: if api path ends with a slash(/), the framework automatically tries to append req.params.id from the express :id wildcard | ||
// as this is a very common REST paradigm | ||
path: null, | ||
// if not specified, app looks for [nodule name].stub.json - looks for stubName first in nodule folder, then in app/shared/stubs | ||
// Note: can be array to use stubs for multiple api calls | ||
stubName: null, | ||
// params to send to API server | ||
// if verb is 'post', this can be a deep json object (apiBodyType=json) or a shallow object of name value pairs (apiBodyType=form) | ||
params: {}, | ||
// set true to force api to use stub (IE - if API isn't ready yet) | ||
forceStub: false, | ||
// valid values: get, post, put, del (express uses 'del' since delete is a reserved word) | ||
verb: 'get', | ||
// 'html', 'json' only current values - use this to force any nodule to behave like a json or html call regardless of naming conventions or directory conventions | ||
contentType: null, | ||
// valid values: json, form (use 'form' for a standard post submit with name/value pairs - everything wants json body) | ||
bodyType: 'json', | ||
// set this to an Error() instance to "throw" an error from your nodule - see channel.js for example | ||
error: null, | ||
// (numeric) - max API return time in ms | ||
timeout: null, | ||
// use to manipulate query params or other business logic before api call(s) | ||
preProcessor: function(req, res) { }, | ||
// set true to force api to use stub (IE - if API isn't ready yet) | ||
useStub: false, | ||
// use to process data returned from the API before calling template or sending back to client as JSON | ||
postProcessor: function(req, res) { }, | ||
// NOTE: one important property you can set in this function is res.renderData - this is the data sent to the jade template or back to the client as JSON | ||
// if you don't specify res.renderData the app uses res.locals.data1 | ||
} | ||
// can contain path or just name if in same folder | ||
// MAGIC ALERT: if not specified, app looks for [nodule name].stub.json in nodule folder | ||
stubPath: null, | ||
}, | ||
}; |
{ | ||
"name": "yukon", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"description": "Self-discovering component-based API-driven framework based on express", | ||
@@ -30,5 +30,5 @@ "main": "index.js", | ||
"gitHead": "d30252453e13ddf4b9d52282dd4e56879e4fa4bf", | ||
"_id": "yukon@0.0.2", | ||
"_shasum": "cac4972c51bf63dbb7377a51ded1bb32036f997e", | ||
"_from": "yukon@^0.0.2" | ||
"_id": "yukon@0.0.4", | ||
"_shasum": "98e0eb40b7bddfb0e7f80dd6da0853ccc2e2a9ab", | ||
"_from": "yukon@^0.0.4" | ||
} |
@@ -28,3 +28,3 @@ var path = require('path'); | ||
res.templatePath = (templateName.indexOf('/') > -1) | ||
? path.join(process.cwd(), config.templateRoot, templateName) | ||
? path.join(process.cwd(), templateName) | ||
: path.join(nodule.path, templateName); | ||
@@ -31,0 +31,0 @@ } |
// wraps nodule.preProcessor, called after app-level appPreApi middleware | ||
module.exports = function(app, config, api) { | ||
module.exports = function(app, config) { | ||
return function(req, res, next) { | ||
@@ -8,7 +8,4 @@ config.debug('yukon-preApi')('called'); | ||
// TDOO - research if this is somehow a bad idea | ||
req.nodule.callApi = api.callApi; // set ref to api caller for app to use | ||
next(); | ||
}; | ||
}; |
23247
245