UNPKG

ac-koa

Version:

Modules for building Atlassian Connect and HipChat Connect add-ons

236 lines (206 loc) 6.52 kB
var _ = require('lodash'); var co = require('co'); var koa = require('koa'); var json = require('koa-json'); var mount = require('koa-mount'); var logger = require('koa-logger'); var compose = require('koa-compose'); var router = require('koa-router'); var helmet = require('koa-helmet'); var request = require('request'); var winston = require('winston'); var ac = require('ac-node'); var Config = ac.Config; var RedisStore = ac.RedisStore; var Tenants = ac.Tenants; var WebhookManager = ac.WebhookManager; var adapters = require('./adapters'); var util = require('./util'); var ack = module.exports = function (options, services) { options = options || {}; services = _.extend({}, services); var config = Config(options); services.store = createStore(services.store, config); services.httpClient = services.httpClient || request; var app = ack.app(config, services); app.addon = function (scope, descriptor) { if (typeof scope === 'object') { descriptor = scope; scope = null; } scope = scope || ''; if (!descriptor) { return adapters.factories(app, scope); } var scopePath = '/' + scope; var baseUrl = config.LOCAL_BASE_URL + (scope ? scopePath : ''); var addon = koa(); addon.logger = app.logger; addon.use(ack.middleware(app.env, baseUrl)); addon.use(router(addon)); app.use(mount(scopePath, addon)); var addonStore = services.store; if (scope) { addon.name = scope; addonStore = addonStore.narrow(scope); } var appEvents = app; var addonEvents = addon; var tenantStore = addonStore.narrow('tenant'); var tenants = Tenants(tenantStore); var adapterServices = { store: tenantStore, tenants: tenants, httpClient: services.httpClient, webhookManager: WebhookManager(addonStore.narrow('webhook')) }; var adapter = adapters.create(descriptor, baseUrl, adapterServices, options, config); if (scope) { // TODO: we only need to explicitly iterate a defined set of webhook events because EventEmitter // doesn't support wildcard subscriptions; look into using EventEmitter2 instead? adapter.webhookEvents.forEach(function (event) { addonEvents.on(event, function () { var key = scope + ':' + event; var args = [].slice.call(arguments); appEvents.emit.apply(appEvents, [key].concat(args)); }); }); } function onWebhook(event, definition, callback) { addonEvents.on(event, function (context) { var def = definition; co(function *() { if (!def) { var name = context.query.name; if (name) { def = yield adapter.webhookFor(addon.descriptor, context.tenant, name, context); } } var fire = true; if (def) { fire = adapter.prepareWebhook(def, context); } if (fire) { if (util.isGeneratorFunction(callback)) { co(callback).call(context); } else { callback.call(context); } } }).call(context); }); } var builder = adapter.builder(descriptor, { addWebhook: function (definition, callback) { if (callback) { onWebhook(definition.event, definition, callback); } }, ready: function (builtDescriptor) { var finalDescriptor = adapter.transform(builtDescriptor); adapter.validate(finalDescriptor); addon.descriptor = finalDescriptor; var middleware = adapter.ready(finalDescriptor, addonEvents); // descriptor redirect addon.get('/', function *() { if (this.accepts('html', 'json') === 'json') { this.redirect(baseUrl + adapter.descriptorPath); } }); // get descriptor addon.get(adapter.descriptorPath, function *() { this.body = finalDescriptor; }); // adapter-specific middleware addon.use(mount(middleware)); } }); addon.authenticate = function (options) { return function *(next) { yield adapter.authenticator(app.env, options); yield adapter.decorator(app.env, addon.descriptor); yield next; }; }; addon.onWebhook = function (event, callback) { onWebhook(event, null, callback); }; return _.extend(addon, builder); }; var listen = app.listen; app.listen = function (next) { return listen.call(app, config.PORT, function (err) { if (err) { app.logger.error(err.stack || err); if (next) { next(err); } return; } app.logger.info('Atlassian Connect add-on started at ' + config.LOCAL_BASE_URL); if (next) { next(); } }); }; return app; }; ack.require = function (adapterName, _adapter) { adapters.require(adapterName, _adapter); return this; }; ack.app = function (config, services) { var app = koa(); app.config = config; _.extend(app, services); if (app.env !== 'test') { app.use(logger()); } app.logger = new winston.Logger({ transports: [ new winston.transports.Console({ level: app.env === 'production' ? 'info' : (app.env === 'test' ? 'error' : 'debug'), colorize: app.env !== 'production' }) ] }); app.on('error', function (err) { if (app.env !== 'test' || !err.status || err.status >= 500) { app.logger.error(err.stack || err); } }); return app; }; ack.middleware = function (nodeEnv, localBaseUrl) { // configure a common stack of middleware as appropriate to AC addons return compose([ ack.helmet(nodeEnv, localBaseUrl), json({ pretty: nodeEnv !== 'production', param: 'pretty' }) // TODO: moar warez ]); }; ack.helmet = function (nodeEnv, localBaseUrl) { return function *(next) { if (nodeEnv === 'production') { // TODO: complete helmet config as appropriate to AC addons //yield helmet.xframe('allow-from', localBaseUrl); } yield next; }; }; function createStore(service, config) { if (typeof service === 'object') return service; var factory; if (typeof service === 'function') { factory = service; } else if (typeof service === 'string' && ac[service] && ac[service].create) { factory = ac[service].create; } if (!factory) { factory = RedisStore.create; } return factory(config, 'ack'); }