Webhook / Integration Event bus (#499)

* First bash at an event bus for webhooks and integrations

* Refactoring

* poc

* Revert too wide ranging changes
Move to two-queues
This commit is contained in:
Tom Moor 2018-01-13 10:46:29 -08:00 committed by GitHub
parent 33261ba7c7
commit 9d441fc51a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 121 additions and 15 deletions

View File

@ -12,6 +12,7 @@ REDIS_URL=redis://redis:6379
URL=http://localhost:3000
DEPLOYMENT=hosted
ENABLE_UPDATES=true
DEBUG=sql,cache,presenters,events
# Third party credentials (required)
SLACK_KEY=71315967491.XXXXXXXXXX

View File

@ -39,7 +39,7 @@ In development you can quickly get an environment running using Docker by follow
To enable debugging statements, set the following env vars:
```
DEBUG=sql,cache,presenters
DEBUG=sql,cache,presenters,events
```
## Migrations

View File

@ -48,7 +48,7 @@ class Settings extends Component {
name: this.name,
avatarUrl: this.avatarUrl,
});
invariant(res && res.data, 'Document list not available');
invariant(res && res.data, 'User response not available');
const { data } = res;
runInAction('Settings#handleSubmit', () => {
this.props.auth.user = data;
@ -56,7 +56,7 @@ class Settings extends Component {
this.timeout = setTimeout(() => (this.isUpdated = false), 2500);
});
} catch (e) {
this.props.errors.add('Failed to load documents');
this.props.errors.add('Failed to update user');
} finally {
this.isSaving = false;
}
@ -153,7 +153,7 @@ const AvatarContainer = styled(Flex)`
&:hover div {
opacity: 1;
background: rgba(0, 0, 0, 0.75);
color: #ffffff;
color: ${color.white};
}
`;
@ -165,4 +165,4 @@ const StyledInput = styled(Input)`
max-width: 350px;
`;
export default inject('auth', 'errors', 'auth')(Settings);
export default inject('auth', 'errors')(Settings);

View File

@ -16,7 +16,7 @@ import HelpText from 'components/HelpText';
import Subheading from 'components/Subheading';
@observer
class Settings extends Component {
class Tokens extends Component {
@observable name: string = '';
props: {
settings: SettingsStore,
@ -96,4 +96,4 @@ const Table = styled.table`
}
`;
export default inject('settings')(Settings);
export default inject('settings')(Tokens);

46
server/events.js Normal file
View File

@ -0,0 +1,46 @@
// @flow
import Queue from 'bull';
import debug from 'debug';
import services from '../services';
import Document from './models/Document';
import Collection from './models/Collection';
type DocumentEvent = {
name: 'documents.create',
model: Document,
};
type CollectionEvent = {
name: 'collections.create',
model: Collection,
};
export type Event = DocumentEvent | CollectionEvent;
const log = debug('events');
const globalEventsQueue = new Queue('global events', process.env.REDIS_URL);
const serviceEventsQueue = new Queue('service events', process.env.REDIS_URL);
// this queue processes global events and hands them off to service hooks
globalEventsQueue.process(async function(job) {
const names = Object.keys(services);
names.forEach(name => {
const service = services[name];
if (service.on) {
serviceEventsQueue.add({ service: name, ...job.data });
}
});
});
// this queue processes an individual event for a specific service
serviceEventsQueue.process(async function(job) {
const event = job.data;
const service = services[event.service];
if (service.on) {
log(`Triggering ${event.name} for ${service.name}`);
service.on(event);
}
});
export default globalEventsQueue;

View File

@ -1,11 +1,12 @@
// @flow
import _ from 'lodash';
import slug from 'slug';
import randomstring from 'randomstring';
import { DataTypes, sequelize } from '../sequelize';
import { asyncLock } from '../redis';
import events from '../events';
import Document from './Document';
import Event from './Event';
import _ from 'lodash';
// $FlowIssue invalid flow-typed
slug.defaults.mode = 'rfc3986';
@ -93,6 +94,20 @@ Collection.associate = models => {
});
};
// Hooks
Collection.addHook('afterCreate', model =>
events.add({ name: 'collections.create', model })
);
Collection.addHook('afterDestroy', model =>
events.add({ name: 'collections.delete', model })
);
Collection.addHook('afterUpdate', model =>
events.add({ name: 'collections.update', model })
);
// Instance methods
Collection.prototype.getUrl = function() {

View File

@ -7,6 +7,7 @@ import Plain from 'slate-plain-serializer';
import isUUID from 'validator/lib/isUUID';
import { DataTypes, sequelize } from '../sequelize';
import events from '../events';
import parseTitle from '../../shared/utils/parseTitle';
import Revision from './Revision';
@ -204,6 +205,20 @@ Document.searchForUser = async (
return _.sortBy(documents, doc => ids.indexOf(doc.id));
};
// Hooks
Document.addHook('afterCreate', model =>
events.add({ name: 'documents.create', model })
);
Document.addHook('afterDestroy', model =>
events.add({ name: 'documents.delete', model })
);
Document.addHook('afterUpdate', model =>
events.add({ name: 'documents.update', model })
);
// Instance methods
Document.prototype.getSummary = function() {

View File

@ -1,7 +0,0 @@
// @flow
const Slack = {
id: 'slack',
name: 'Slack',
};
export default Slack;

22
services/index.js Normal file
View File

@ -0,0 +1,22 @@
// @flow
import fs from 'fs-extra';
import path from 'path';
const services = {};
fs
.readdirSync(__dirname)
.filter(file => file.indexOf('.') !== 0 && file !== path.basename(__filename))
.forEach(name => {
const servicePath = path.join(__dirname, name);
// $FlowIssue
const pkg = require(path.join(servicePath, 'package.json'));
// $FlowIssue
const hooks = require(servicePath).default;
services[pkg.name] = {
...pkg,
...hooks,
};
});
export default services;

10
services/slack/index.js Normal file
View File

@ -0,0 +1,10 @@
// @flow
import type { Event } from '../../server/events';
const Slack = {
on: (event: Event) => {
console.log(`Slack service received ${event.name}, id: ${event.model.id}`);
},
};
export default Slack;

View File

@ -0,0 +1,4 @@
{
"name": "slack",
"description": "Hook up your Slack to your Outline."
}