emissary-router
This package facilitates the decision process for turning an event liked "document created" into one or more notifications.
For example, with the appropriate configuration, the below code could end up queuing an SMS message to user #1, a push notification to user #2, and a webhook POST
to a third-party integration.
1EmissaryRouter.emit('document created', { 2 someKey:'someVal' 3});
How it works
Configuration
This package uses the dispatch:configuration
package under the hood to facilitate schema-agnostic, inherited configuration. The configuration schema specific to the router is defined, and the defaults are initialized, when you run EmissaryRouter.init
. Here's an example (the properties are described below):
1Emissary.emit({ 2 events:['<event 1>', '<event 2>'], 3 notificationTypes:[{ 4 type:'push', 5 multi:false, 6 formatter:function(recipient) { 7 return { 8 userId:recipient._id 9 } 10 } 11 }, { 12 type:'sms', 13 multi:false, 14 formatter:function(recipient) { 15 return recipient.phoneNumber; 16 } 17 }, { 18 type:'email', 19 multi:true, 20 formatter:function(recipient, recipientConfig) { 21 return recipientConfig.conf.emailAddress; 22 } 23 }], 24 receivePreferences:[{ 25 type:'always', 26 check:function(){ 27 return true 28 } 29 }, { 30 type:'at night', 31 check:function(recipient) { 32 return someTimeFunction.isItNightTime(recipient.timezone); 33 }], 34 prefix:'myNotifications', 35 getPotentialRecipientsForEvent:function(){...}, 36 retrieveEntity:function(entityType, entityId) {...}, 37 generateTemplateData:function() {...}, 38 transformJob:function(job) {...} 39});
events
These are the types of events that can possibly be sent. Because this package allows entity-level configuration on a per event basis, they must be defined and added to the schema here. For example:
1events:['userLoggedIn', 'todoListCreated', 'todoItemCompleted']
IMPORTANT! - event names are used as keys in the configuration document in MongoDB. As such, they cannot have dots/periods in them nor can they start with a dollar sign.
notificationTypes
Notification types in this package should match their type
properties one-to-one with the types registered with Emissary.registerType
. The built-in Emissary types (sms
, email
, push
, webhook
) are already registered, with webhook
being a multi:true
notification type (see below).
formatter
Function
This function returns the value that is passed as the to
property to Emissary.queueTask
. Obviously this should be something that the workers on the other side of the equation can use. It receives four arguments:
recipient
- the value returned by yourretrieveEntity
functionrecipientConfig
- see beloweventName
- the name of the event sent withEmissaryRouter.send
eventData
- the data passed along with the event name toEmissaryRouter.send
The recipientConfig
argument is the combined type-level configuration AND type/event-level configuration (more specific). The type/event-level configuration overrides individual keys in the type-level configuration. For example, given the following:
1{ 2 "notifications":{ 3 "sms":{ 4 "config":{ 5 "foo":"bar", 6 "nested":{ 7 "other":"field" 8 } 9 }, 10 "events":{ 11 "event1":{ 12 "config":{ 13 "nested":{ 14 "other2":"field2" 15 } 16 } 17 } 18 } 19 } 20 } 21}
...the value of recipientConfig
will be:
1{ 2 "foo":"bar", 3 "nested":{ 4 "other":"field", 5 "other2":"field2" 6 } 7}
The multi
option
The multi
property defines if there could potentially be multiple different configurations for this type. For example, you may want to use the email
transport but have different configurations for "work email" vs "personal email". In this case, the value of recipientConfig.conf
in the formatter
function is the value for the particular configuration set that should be sent. Take, for example, the following configuration data:
1{ 2 "notifications":{ 3 "email":[ 4 { 5 "when":{ 6 "always":["event1"], 7 "daytime_only":["event2"] 8 }, 9 "events":{ 10 "event1":{ 11 "templates":{...}, 12 "timing":{...}, 13 "config":{ 14 "emailAddress":"email1@test.com" 15 } 16 }, 17 "event2":{ 18 "templates":{...}, 19 "timing":{...}, 20 "config":{} 21 } 22 }, 23 "config":{ 24 "emailAddress":"default@test.com" 25 } 26 }, { 27 "when":{ 28 "night_only":["event3"] 29 }, 30 "events":{ 31 "event3":{ 32 "templates":{...}, 33 "timing":{...}, 34 "config":{} 35 } 36 }, 37 "config":{ 38 "emailAddress":"other@test.com" 39 } 40 } 41 ] 42 } 43}
With this configuration, if you send event1
and this recipient is returned by the getPotentialRecipientsForEvent
function, it will send an email to "email1@test.com"
. If you send event2
during the day, it will send an email to "default@test.com"
using the configuration in email[0].events.event2
, since the config is inherited and deep-extended. Likewise, if you send event3
at nighttime, it will send an email to "other@test.com"
.
Important: there is currently one caveat with using arrays with the dispatch:configuration
package - arrays are extended using full replacement. Meaning, if the above recipient can inherit from another entity's configuration, then it will not inherit any of the elements in the notifications.email
array unless its own notifications.email
array is not defined.
receivePreferences
Your recipients may want to receive different messages for different events, well, differently. EmissaryRouter
lets you define one or more "receive preference" types with a function to determine if that type is currently valid. Those functions are then used internally by the router to determine if the recipient should be sent a message for a particular event.
For example, you may want to send different message types for different events based on the time of day (let's say "always"
, "day"
, and "night"
are the options). In this case, you'd pass those three options to EmissaryRouter.init
:
1EmissaryRouter.init({ 2 (...) 3 receivePreferences:[ 4 { 5 type:'always', 6 check:function(){ return true;} 7 }, { 8 type:'night', 9 check:function(recipient) { 10 var timezone = recipient.timezone; 11 var time = getCurrentTimeInTimezone(timezone); 12 return time.hour >= 20 || time.hour < 5; 13 } 14 }, { 15 type:'day', 16 check:function(recipient) { 17 ver timezone = recipient.timezone; 18 var time = getCurrentTimeInTimezone(timezone); 19 return time.hour >= 5 && time.hour < 20; 20 } 21 } 22 ] 23 (...) 24});
The preferences
key of the configuration, then, will look like this (let's say you have events ["event1", "event2"]
and message types ["sms", "email"]
):
1{ 2 notifications:{ 3 preferences:{ 4 sms:{ 5 day:['event1'], 6 always:['event2'] 7 }, 8 email:{ 9 always:['event1', 'event2'] 10 } 11 } 12 } 13}
In this case, this recipient will receive an SMS for event1
only if it's during the daytime, but will always receive an SMS for event2
. Similarly, she will always receive an email for both event1
and event2
, regardless of the time of day.
prefix
The prefix property allows you to configure the key on which all emissary-specific configuration is stored via the dispatch:configuration
package. It defaults to notifications
.
getPotentialRecipientsForEvent
See Deciding who to potentially notify - this is the function that does the decision logic. There are three basic functions this package performs: deciding who to potentially notify when an event occurs, deciding how to notify them, and queuing those notifications with Emissary.
retrieveEntity
This is a function used by the router to fetch the entity document from the entity tuple (["<entity type>", "<entity id>"]
). The return value is not used internally by the router, but is passed to several user-provided functions like the "to formatters" and the skipFilter
function. For example:
1EmissaryRouter.init({ 2 (...) 3 retrieveEntity:function(type, id) { 4 return myCollectionMap[type].findOne(id); 5 } 6 (...) 7});
generateTemplateData
This function is used to generate the templateData
object written to the Emissary message queue (which is in turn used to fill out the templates using Handlebars). Similar to the getPotentialRecipientsForEvent
function, there's nothing we can really do to abstract this functionality since it really depends on your application.
For example:
1EmissaryRouter.init({ 2 (...) 3 generateTemplateData:function(eventName, eventData) { 4 if(eventName === 'listCreated') { 5 return { 6 list:ListCollection.findOne(eventData.id) 7 } 8 } 9 } 10 (...) 11});
transformJob
This is an optional function that you can use to modify the vsivsi:job-collection Job entity before it is written to the queue. See the job API reference for that package for more information (the Job
is the single argument passed to the transform function and it expects the mutated (or unmutated) job in return).
skipFilter
This optional function lets you define which message types to skip given a recipient and an event. For example, you could use this as a failsafe to make sure that push notifications are only attempted to be sent to one specific type of user. In that case, the function should return ["push"]
to skip push notifications, even if the configuration dictates otherwise.
For example:
1EmissaryRouter.init({ 2 (...) 3 skipFilter:function(recipient, recipientConfig, eventName) { 4 if(recipient.type !== 'special type') { 5 return ['push']; 6 } 7 return []; 8 } 9 (...) 10});
Decision Logic
Deciding who to potentially notify
From what we can tell, there's no good way to abstract this function out of your application code and into this package - there are just too many options for the decision logic. For that reason, the meat of this function comes from the [getPotentialRecipientsForEvent](#getPotentialRecipientsForEvent)
function passed as a configuration option when running EmissaryRouter.init()
.
That function is passed two arguments, the eventName
and eventData
('document created'
and {someKey:'someVal'}
in the above example, respectively), and is expected to return an array of tuples (IE ['<entity type>', '<entity ID>']
readable by the dispatch:configuration
package. For example:
1return [ 2 ['user', '12345'], 3 ['user', '55555'], 4 ['integration_partner', '54321'] 5];
In the above example, your application code has determined that based on the event and event data, user#12345
, user#55555
, and integration_partner#54321
should potentially be notified. The key here is the potentiality - the actual decision process for whether to actually send those entities a notification comes later.
Deciding whether to notify them
This decision is made based on the configuration for each potential recipient. The preferences
configuration key provided during EmissaryRouter.init
(see receivePreferences). For each recipient, the router reads the configuration preferences and uses the preference.check
skipFilter
functions to end up with an array of notification types to send, e.g. ["sms", "email"]
Deciding how to notify them
In this step, the recipient document is passed through the corresponding "to formatter" for each notification type, and the payload is constructed from that, the template data returned by the getTemplateData
function, the templates defined in the template
section of the configuration schema, and the timing (delay, timeout) defined in the timing
section of the configuration schema.
These are written as jobs to the Emissary queue using Emissary.queueTask
, and from there, your registered workers will take over.