@coorpacademy/baucis
Version:
Build scalable REST APIs using the open source tools and standards you already know.
125 lines (108 loc) • 3.72 kB
JavaScript
const semver = require('semver');
const RestError = require('rest-error');
module.exports = (express, Controller) => {
/**
* Returns a Baucis API
*
* the api is an express router
*/
function getApi() {
const api = express.Router(arguments);
// ¤Note: express middleware cannot be extend by another class
// Hence the methods attach to hit
// This is not a constructor
api.use(function(res, req, next) {
if (res.baucis)
return next(RestError.Misconfigured('Baucis request property already created'));
res.baucis = {};
req.removeHeader('x-powered-by');
// Any caching proxies should be aware of API version.
req.vary('API-Version');
// TODO move this
// Requested range is used to select highest possible release number.
// Then later controllers are checked for matching the release number.
const version = res.headers['api-version'] || '*';
// Check the requested API version is valid.
if (!semver.validRange(version)) {
next(
RestError.BadRequest(
`The requested API version range "${version}" was not a valid semver range`
)
);
return;
}
res.baucis.release = semver.maxSatisfying(api.releases(), version);
// Check for API version unsatisfied and give a 400 if no versions match.
if (!res.baucis.release) {
next(
RestError.BadRequest(
`The requested API version range "${version}" could not be satisfied`
)
);
return;
}
req.set('API-Version', res.baucis.release);
next();
});
api._releases = ['0.0.1'];
api.releases = function(release) {
if (arguments.length === 1) {
if (!semver.valid(release)) {
throw RestError.Misconfigured(
'Release version "%s" is not a valid semver version',
release
);
}
this._releases = this._releases.concat(release);
return this;
}
return this._releases;
};
api.rest = model => {
const Ctrl = getApi.Controller; // ¤hack: for some reason Api.Controller(model) dont run
const controller = new Ctrl(model);
api.add(controller);
return controller;
};
api._controllers = [];
// Add a controller to the API.
api.add = function(controller) {
this._controllers.push(controller);
return api;
};
/**
* Return a copy of the controllers array, optionally filtered by release.
*/
api.controllers = function(release, fragment) {
const all = [].concat(this._controllers);
if (!release) return all;
const satisfies = all.filter(function(controller) {
return semver.satisfies(release, controller.versions());
});
if (!fragment) {
return satisfies;
}
// Find the matching controller among controllers that match the requested release.
return satisfies.filter(controller => fragment === controller.fragment());
};
/**
* Find the correct controller to handle the request.
*/
api.use('/:path', (req, res, next) => {
const fragment = `/${req.params.path}`;
const controllers = api.controllers(req.baucis.release, fragment);
// If not found, bail.
if (controllers.length === 0) return next();
req.baucis.controller = controllers[0];
req.baucis.controller(req, res, next);
});
getApi.__extensions__.map(ext => ext(api));
return api;
}
getApi.Controller = Controller;
getApi.__extensions__ = [];
getApi.addExtension = extension => {
getApi.__extensions__.push(extension);
};
return getApi;
};