ac-koa
Version:
Modules for building Atlassian Connect and HipChat Connect add-ons
236 lines (206 loc) • 6.52 kB
JavaScript
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');
}