This method allows you to define a new event handler for a specified event.
This method returns a function that can be invoked to unregister this handler.
Event handler options are as follows:
Object Structure | Type | Default | Notes |
---|---|---|---|
{ | |||
type: | string | required | An event type. |
from: | Collection | Used with link event handlers this defines which collection is adding the event. | |
handler: | async? Event => boolean | required | The event handling function. |
when: | 'pre' | 'post' | 'both' | 'pre' (or 'post' if only 'post' is supported) | Should the handler be called before or after the underling action being listened to.
If the event type only supports one type or the other (for example, find events only support post) then this does not need to be specified. |
order: | number | Number.POSITIVE_INFINITY | Handlers are called in increasing order or by the order in when they were encountered if the orders are the same. If no order is given, the order defaults to +infinity, so that handlers with no order are called after handlers with an order. |
} |
Event handlers can optionally be asynchronous by returning a promise.
If an event handler returns false, returns a promise of false, or calls Event.preventDefault() then the underlying action associated with the event will be canceled.
Note that actions can only be canceled in when: 'pre' (the default) handlers.
Exceptions
If an event handler throws an exception or returns a failed promise then the resulting exception or failed promise will be thrown to the client in addition to the event being canceled. For example, if you add a handler on the remove event type and throw an error inside the handler, that error will be thrown to the calling function that executed the remove method and the remove will not be performed.
If the call originated on the client, then the exception will be rethrown on the client as well.
Throw an AppError if the error is unexpected and not due to user the users actions, throw a SecureError if there is a problem with security, or throw a UserError if the problem is due to bad user input or data.
See Exceptions for more information.
High-level vs. Low-level interface
There are two styles of writing an event handler: the (1) high-level interface and the (2) low-level interface.
High-level interface
The high-level interface centers around the documents property:
const dereg = Tyr.byName.user.on({
type: 'change',
handler(event) {
for (const document in await event.documents) {
// handle processing of this document
}
}
The high-level interface will also do things like minimize extraneous queries. For example, say a findAndModify() happens, and there are three event handlers on that collection -- the await event.documents will have to do a findAll() to grab the documents since it was a findAndModify() but it will only do one find call despite there being three handlers. If those three handlers were all using the low-level interface, and all needed to access the data, the data would be queried three times.
Low-level interface
The low-level interface is more complicated, but gives you more insights into what database operation was performed.
const dereg = Tyr.byName.user.on({
type: 'change',
handler(event) {
if (event.document) {
// a single-document insert/update/save operation was performed, handle this document.
} else if (event.update) {
// something like an update() or findAndModify() was performed, examine the event.query and event.update
// properties to determine what you need to do. Note you do not have access to the actual Document objects
// in this case.
}
}
The low level interface is primarily intended for situations where:
- You do not need access to the underlying document
For example, you want to write out a record everytime data is changed by a user but do not care about what was actually changed. In this example, by avoiding the `await event.documents` operation, you are not doing any secondary reads. (Though if other high-level handlers exist the reads will still be done -- this benefit only applies if all handlers for the collection are low-level.) - You want extra insights around what actual database operation was performed
For example, say you wanted to implement a transactional log of everything done to the collection in that cased you might want to know when findAndModify’s/updates/etc. vs. single document saves were done.
example 1
Watching when an object is updated:
const dereg = Tyr.byName.user.on({
type: 'change',
handler(event) {
console.log('user changed for these objects:', event.query);
}
...
dereg(); // deregister the event handler
});
example 2
Modifying documents when they are read from the database:
Tyr.byName.user.on({
type: 'find',
async handler(event) {
for (const doc of await event.documents) {
doc['manufacturedId'] = 'ID' + doc._id;
}
}
});
link event handlers
If you want to recommend or require that collections that link to your collection implement lifecycle events you can define link event handlers.
An exception will be thrown when bootstrapping Tyranid if a collection links to a collection but does not implement its required link event handlers.
Object Structure | Type | Default | Notes |
---|---|---|---|
linkEvents: { | |||
type: | string | required | An event type. |
required: | boolean | false | Whether clients linking to this collection need to have the appropriate event handler defined. |
message: | string | required | The exception will have this string as its message field. |
} |