Event

Events are sent in response to changes to a database and can be used to implement lifecycle hooks and workflow rules.

types

Tyranid supports the following built-in event types:

NameNotesWhenEffect of Cancelation
(pre only)
insertA document is inserted (via insert, etc.)pre, postThe document is not inserted
updateA document is updated (via update, $update, findAndModify, etc.)pre, postThe document is not updated
changeA document is inserted OR updatedpre, postThe document is not changed
findA document is read (via find*(), byId*(), byUid*(), etc.). The handler is invoked after historical and population operations have been performed. The documents can be mutated inside the handler.post
removeA document is removedpre, postThe document is not removed
subscribeA query is being subscribed to.preThe subscription is not made.

You can also use your own custom types of events as well with the Event.fire() method.

event handlers

An event handler can be registered against an object using the following method (currently only collections can have event handlers attached to them.

on(options: object): () => void

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 StructureTypeDefaultNotes
{
type:stringrequiredAn event type.
from:CollectionUsed with link event handlers this defines which collection is adding the event.
handler:async? Event => booleanrequiredThe 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:numberNumber.POSITIVE_INFINITYHandlers 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.

It's recommended to define event handlers alongside the main model definition when possible. This makes it easier to understand what events will get fired when a collection is updated if they are centrally located. It also provides a basis for dealing with the case where one event handler might depend on a previous one running -- in this case, if both handlers are in the same file their logic can be merged into one handler that exposes the dependency inside the function.

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:

In general, prefer the high-level interface unless you know why you need the low-level interface. The handlers are much easier to write and easier to understand and more efficient in the typical use cases where you would want an event handler.

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 StructureTypeDefaultNotes
linkEvents: {
type:stringrequiredAn event type.
required:booleanfalseWhether clients linking to this collection need to have the appropriate event handler defined.
message:stringrequiredThe exception will have this string as its message field.
}
}

static

fire(event: Event): void

This method fires off an event.

The event is by default only sent to the current server's event handlers.

If the instanceId property is set then the event will be sent to only that server (not the current server).

If the broadcast property is set then the event will be sent to all Tyranid instance servers (including the current server).

Also see Collection.fire().

instance

broadcast: boolean

If present and true this indicates that when this event is fired by the Event.fire() method it should be sent to all Tyranid server instances in the cluster, not just the current server.

collectionId: string

The collection ID that this event is attached to.

collection: Collection

This contains the collection associated with this event.

dataCollectionId: string

The collection ID associated this event's data (the document/s and query properties.

If dataCollectionId is not specified it will default to collectionId.

dataCollection: Collection

This contains the collection associated with this event's data.

This defaults to collection if dataCollection/Id is not specified.

date: Date

This contains the time the event was fired.

document: Document

This contains the document associated with this event.

If the event refers to multiple documents being affected, then document might not be populated. In that case, you should work off of the query property or the documents method.

documents: Document[]

If document is available this method will return an array containing that document.

Otherwise, the query will be executed to retrieve back the list of documents. This result is cached so only one query will be executed even if multiple event handlers access this method.

instanceId: string

If present this indicates that when this event is fired by the Event.fire() method it should be sent to the specified Tyranid server instance in the cluster (and not the current server).

The current server's instance ID is located at Tyr.instanceId.

A list of all instances is maintained in the Tyranid tyrInstance collection (the instance IDs are present in the _id property).

opts: options object

This contains the options object that was passed to the underlying operation the event is in response to. This options object can be used to access things like auth (to get the user who made the request).

preventDefault(): void

This cancels the event and prevents the default action associated with this event and throws an EventCancelError.

query: object

This contains the mongo query matching the event.

If there is no regular query and the event affects a single document then a query matching the _id will be created. For example:

{
  _id: object id of the document
}
update: object

Valid on findAndModify() events, this contains the findAndModify() update clause.

when: 'pre' | 'post'

Useful primarily for when: 'both' event handlers, this indicates whether the event is occuring before or after the action being listened to.

type: string

This contains the type of the event.