Event Handler - basics
Let's make things really simple.
- The Event Handler is the component that enables the application to write to the database.
- You define your application's Event Handler in a Kotlin script file:
server/appname/src/main/genesis/scripts/appname-eventhandler.kts - In this file, you define specific
eventHandler
codeblocks, each of which has full access to the database. - Each
eventHandler
can be invoked from the front end, from othereventHandler
codeblocks, or from custom components in the application. - If you use AppGen to build from your dictionary, then a basic .kts file will be built automatically for you, creating basic insert, modify and delete
eventHandler
code blocks for all the tables and views in your data model. You can edit this file to customise the component. - Otherwise, you can build your .kts by defining each
eventHandler
codeblock from scratch. - For complex Event Handlers, you might prefer to use the Event Handler API to implement a set of classes.
On this page, we'll take you through the basics of creating an Event Handler in simple stages:
- a very simple example file
- giving each
eventHandler
codeblock a name - adding validation
- returning a value
- transactional
eventHandler
codeblocks - the process flow for
eventHandler
codeblocks - more information on validation
- context Event Handlers
By the end of that, you'll have some very useful knowledge, and you'll be ready to move on to the advanced page.
It is possible to define an Event Handler in Java, and we have included Java examples. However, we strongly advise you to create your Event Handler using Kotlin; it is a considerably more efficient method.
A simple example of an Event Handler
Here is a simple example of an Event Handler file. It defines a single eventHandler
, which inserts a counterparty into the database, using the entityDb.
- First, note that there is an
eventHandler
statement for containing all youreventHandler
codeblocks. This is necessary whether the file contains oneeventHandler
or multiple ones. - For this example, the file defines a single
eventHandler
of type<Counterparty>
. (<Counterparty>
is an object that was created from the COUNTERPARTY table in the schema; this object defines the input that should be expected, such as Counterparty name and Counterparty ID.) - The
eventHandler
performs a single action - it inserts a counterparty as a new row into the database, using the entityDb.
eventHandler {
eventHandler<Counterparty> {
onCommit { event ->
val counterparty = event.details
entityDb.insert(counterparty)
ack()
}
}
}
Adding a name
Every eventHandler
in your .kts must have a unique name. If you do not provide one, it will be allocated automatically. In the previous example, the eventHandler
will automatically be named EVENT_COUNTERPARTY.
It is good practice to provide your own name for each eventHandler
. For example, if you have insert and modify codeblocks for the same table and you don't name them, then the platform will probably generate identical names for both - which will give you a problem.
Note that the prefix EVENT_
is automatically added to the name that you specify.
If you do not define a name, a default name will be allocated automatically. The default name will be EVENT_<message type name>
. So, for a message type declared as OrderInsert
, declaring an Event Handler block as eventHandler<OrderInsert>{}
automatically registers the event with the name EVENT_ORDER_INSERT
.
So, below, we modify our previous example by defining the name of the eventHandler
: COUNTERPARTY_INSERT. This will automatically become EVENT_COUNTERPARTY_INSERT.
eventHandler<Counterparty>(name = "COUNTERPARTY_INSERT") {
onCommit { event ->
val counterparty = event.details
entityDb.insert(counterparty)
ack()
}
}
Adding validation
So far, we have provided an onCommit
block in our eventHandler
. This is where the active instructions are; these are usually database changes.
If you want to provide some validation before the action, you need to have an onValidate
block before the onCommit
. The last value of the code block must always be the return message type.
In this case, we have used the kotlin require function to check that the counterparty
field is not empty.
The onCommit
block will only be executed if the counterparty
field is not null.
eventHandler<Counterparty>(name = "COUNTERPARTY_INSERT") {
onValidate { event ->
val counterparty = event.details
require(counterparty.name != null) { "Counterparty should have a name" }
ack()
}
onCommit { event ->
val counterparty = event.details
entityDb.insert(counterparty)
ack()
}
}
Returning a value
The onCommit
block must always return either an ack()
or nack(...)
. In the previous examples, it has always been an ack()
.
Now consider a scenario where you might want to return a nack(...)
. In this case, there is no onValidate
block.
In the onCommit
block:
- if the counterparty field is empty, the
eventHandler
returns anack
, along with a suitable message. - if the counterparty field has content, then the
eventHandler
returns anack
eventHandler<Counterparty>(name = "COUNTERPARTY_INSERT") {
onCommit { event ->
val counterparty = event.details
if (counterparty.name == null) {
nack("Counterparty should have a name")
} else {
entityDb.insert(counterparty)
ack()
}
}
}
Default reply types
So far, we have seen ack
and nack
. There is a third type: warningNack
. Let's stop and look at the specifications for all three default reply types:
ack
: used to signify a successful result.ack
takes an optional parameter ofList<Map<String, Any>>
. For example,ack(listOf(mapOf("TRADE_ID", "1")))
.nack
: used to signify an unsuccessful result.nack
accepts either aString
parameter or aThrowable
. For example,nack("Error!")
ornack(myThrowable)
.warningNack
: used to warn the client.warningNack
, likenack
, accepts either aString
parameter or aThrowable
. For example,warningNack("Provided User alias $userAlias will override Username $username.")
orwarningNack(myThrowable)
.
Transactional Event Handlers (ACID)
If you want your eventHandler
to comply with ACID, you can declare it to be transactional = true
. Any exception returned will result in a complete rollback of all parts of the onCommit
and onValidate
(the transaction also covers read commands) blocks. While an exception will trigger a rollback, the transaction will commit if a nack
or ack
is returned.
eventHandler<Counterparty>(transactional = true) {
onCommit { event ->
val counterparty = event.details
entityDb.insert(counterparty)
ack()
}
}
Whether it is a database update or uploading a report to a third party. It will be called when an event message is received with validate = false
and has successfully passed the onValidate
block. The last value of the code block must always be the return message type.
Processing onValidate and onCommit
The incoming message that triggers an Event Handler can have validate
set to true
or false
. This controls whether the Event Handler simply performs some validation or it executes its complete set of processing.
validate = true
The key thing about this setting is that it means that only the onValidate
block is executed, not the onCommit
block. Here is the precise process flow:
validate = false
With this setting, both the onValidate
codeblock and the onCommit
codeblock will be executed. Here is the precise process flow:
More information about onValidate
As you will have seen above, an onValidate
codeblock will be executed whether the incoming message has validate=true
or validate=false
.The onValidate
block is optional if you are using the default reply message type (EventReply
) and will automatically be successful if not defined.
However, note that an onValidate
codeblock is mandatory when using custom reply message types; the script will not compile if there is no onValidate
codeblock. See the simple example below:
eventHandler<Company>(name = "COMPANY_INSERT") {
onValidate { event ->
val company = event.details
require(company.companyName != "MY_COMPANY") {
"We don't accept your company"
}
ack()
}
onCommit { event ->
val company = event.details
val result = entityDb.insert(company)
ack(listOf(mapOf("COMPANY_ID" to result.record.companyId)))
}
}
Kotlin’s require
method throws an exception with a message if the boolean expression is not what is expected, and the Event Handler automatically converts that exception into a corresponding EventNack
.
Context Event Handlers
In order to optimise database look-up operations, you might want to use data obtained by the onValidate
block inside your onCommit
block. To do this, use context Event Handlers, as shown below:
contextEventHandler<Company, String>(name = "CONTEXT_COMPANY_INSERT") {
onValidate {
val company = it.details
if(company.companyName == "MY_COMPANY") {
validationAck(validationContext = "Best company in the world")
} else {
validationAck()
}
}
onCommit { event, context ->
val parsedContext = context ?: "Missing context"
val company = event.details
val result = entityDb.insert(company)
ack(listOf(mapOf("VALUE" to parsedContext)))
}
}
As the example shows, there is an additional type defined for the context Event Handler. This is a String
. It gives you the option of returning a String
value from the onValidate
block (see validationAck logic), which can then be captured it in the onCommit
block (see context lambda parameter).
Because the example creates a validation context, the function validationAck()
is used at the end of the onValidate
block, and not just ack()
.
Handling delete events
There are two ways of handling a delete event:
- you can use an index entity as metadata
- you can create a separate class
Using the index entity reduces code and is also more robust in case of changes to the fields in the key.
Here is an example of using an index entity to handle a delete event:
eventHandler<Trade.ById>(name = "TRADE_DELETE") {
permissioning {
permissionCodes = listOf("TRADER")
}
onValidate { event ->
ack()
}
onCommit { event ->
val trade = event.details
entityDb.delete(trade)
ack()
}
}
Auto-generated REST endpoints
As we have mentioned before, all events created in the eventhandler.kts file are automatically exposed as a REST endpoints.
Below you see an example of accessing a custom end-point called EVENT_TRADE_INSERT running locally. It was automatically generated by the Genesis platform when you defined the event TRADE_INSERT in the app_name-eventhandler.kts.
[POST] http://localhost:9064/EVENT_TRADE_INSERT/
You can see how to manipulate and work with auto-generated endpoints in our pages on REST endpoints.
Custom Log messages
Genesis provides a class called LOG
that can be used to insert custom log messages. This class provides you with
5 methods to be used accordingly: info
, warn
, error
,trade
,debug
. To use these methods, you need to provide, as an input,
a string that will be inserted into the Logs.
Here is an example where you can insert an info message into the log file:
LOG.info("This is an info message")
The LOG
instance is a Logger from SLF4J.
In order to see these messages, you must set the logging level accordingly. You can do this using the logLevel command.