Skip to main content
Version: Current

Day four

Day three recap
Here are the main takeaways from Day three.
  • We introduced Views and how you can use them.
  • We extended our application adding Position table.
  • We started writing some automated tests for our application.
  • We added calculated data (derived fields) into our main View.
  • Consolidators perform data aggregation and calculations, and here we created one to real-time feed the Positions table.
  • We added a grid in the UI to display the Positions using Grid Pro, with more flexibility to customize the HTML and CSS.
  • This day covers:

    State Management

    State Machines enable you to control workflow by defining the transitions from state to state. This example enables you to build a very simple state machine so that you can add new trades. You will create a new field called TRADE_STATUS, which can have three possible states: NEW, ALLOCATED, CANCELLED.

    • NEW can go to ALLOCATED or CANCELLED.
    • ALLOCATED and CANCELLED can’t go anywhere else.
    • NEW is the only state you can use to insert new records.

    Once we have added add a new field to the data model, we will edit the Event Handler file to add controlled transitions from one state to another.

    1. Data model

    Make sure you have added the TRADE_STATUS field to the TRADE table in the alpha-tables-dictionary.kts file.

    tables {
    table (name = "TRADE", id = 2000) {
    ...
    field("TRADE_STATUS", ENUM("NEW", "ALLOCATED", "CANCELLED")).default("NEW")
    }
    ...
    }

    If the TRADE_STATUS is missing, run generatefields to generate the fields, AND generatedao to create the DAOs.

    2. Create the State Machine

    Inside the alpha-eventhandler.kts file, implement the following state machine:

    import global.genesis.gen.dao.enums.alpha.trade.TradeStatus

    ...

    eventHandler {

    stateMachine(TRADE.TRADE_STATUS){

    // EVENT_TRADE_INSERT
    insertEvent {
    initialStates(TradeStatus.NEW)

    onEvent { event ->
    event.withDetails {
    enteredBy = event.userName
    tradeDate = now()
    }
    }
    }

    modifyEvent {
    mutableStates(TradeStatus.ALLOCATED, TradeStatus.CANCELLED)

    // EVENT_TRADE_ALLOCATED
    transitionEvent(TradeStatus.ALLOCATED){
    fromStates(TradeStatus.NEW)

    onEvent{ event, trade ->
    trade.enteredBy = event.userName
    trade.tradeDate = now()
    }
    }

    // EVENT_TRADE_CANCELLED
    transitionEvent(TradeStatus.CANCELLED){
    fromStates(TradeStatus.NEW, TradeStatus.ALLOCATED)

    onEvent{ event, trade ->
    trade.enteredBy = event.userName
    trade.tradeDate = now()
    trade.tradeStatus = TradeStatus.CANCELLED
    }
    }
    }
    }
    ...
    }

    This creates not only the states and the flow of your state machine, but also the events accordingly. The naming rule for each event created is: {NAME_OF_THE_ACTION}_{NAME_OF_THE_TABLE}_{NAME_OF_THE_STATE}

    Now let's look at the different elements that make up the state machine:

    • stateMachine(): you need to provide the field that will represent the states of your machine. Normally this is an enum field.
    • insertEvent { ... }: this creates the insert event of your state machine. This event is called: EVENT_TRADE_INSERT.
      • initialStates(): this defines the allowed states that can use the insert event. If a state is not defined here, then it won't be able to use this event.
      • onEvent { event , trade -> () }: This is where your event is committed. The trade variable will be inserted into the database as it is, so if you want to apply any logic or modify any field, you need to do that in this code block.
    • modifyEvent { ... }: this creates the modify events of your state machine. Unlike the insertEvent, each event will be created based on the transition event.
      • mutableStates(): this specifies the states from which the trade can be modified.
      • transitionEvent() { ... }: this creates the events themselves. Each transitionEvent will create a new event. The name of each event follows the same rule: EVENT_{TABLE}_{STATE}. So in our example we have: EVENT_TRADE_ALLOCATED and EVENT_TRADE_CANCELLED.
      • fromStates( ): this defines the flow of the state machine. Here you define which states can move to the state defined in the transitionEvent.

    Exercise 4.1: State Machines

    ESTIMATED TIME

    40 mins

    Modify the state machine. Remove the current rule when TradeStatus.NEW, and set the field trade.enteredBy to empty when TradeStatus.CANCELLED.

    UI CHANGES

    Open the home.ts file and add the TRADE_STATUS field in the const COLUMNS.

    ...
    //grid columns that will be showed
    const COLUMNS = [
    {
    ...defaultColumnConfig,
    field: 'TRADE_ID',
    headerName: 'Id',
    },
    {
    ...defaultColumnConfig,
    field: 'QUANTITY',
    headerName: 'Quantity',
    },
    {
    ...defaultColumnConfig,
    field: 'PRICE',
    headerName: 'Price',
    },
    {
    ...defaultColumnConfig,
    field: 'SYMBOL',
    headerName: 'Symbol',
    },
    {
    ...defaultColumnConfig,
    field: 'DIRECTION',
    headerName: 'Direction',
    },
    {
    ...defaultColumnConfig,
    field: 'TRADE_STATUS',
    headerName: 'Status',
    },
    ];

    @customElement({
    name,
    template,
    styles,
    })
    export class Home extends GenesisElement {
    ...
    constructor() {
    super();
    }
    }

    And add the deleteEvent to the home.template.ts file.

    ...
    export const HomeTemplate = html<Home>`
    <div class="split-layout">
    <div class="top-layout">
    <entity-management
    resourceName="ALL_TRADES"
    title = "Trades"
    entityLabel="Trades"
    createEvent = "EVENT_TRADE_INSERT"
    deleteEvent = "EVENT_TRADE_CANCELLED"
    :columns=${x => x.columns}
    :createFormUiSchema=${() => tradeFormCreateSchema}
    :updateFormUiSchema=${() => tradeFormUpdateSchema}
    ></entity-management>
    ...
    </div>
    </div>
    `;

    Remember to run build and deploy after the changes, and test it directly in the UI.

    Adding logic to the Event Handler

    We are going to change the code in the Event Handler so that:

    • It checks if the counterparty exists in the database (by checking COUNTERPARTY_ID field)

    Add the validation code

    Go to the alpha-eventhandler.kts file for the counterparty insert event.

    Add the verification by inserting a verify inside the onValidate block, before the onCommit block in COUNTERPARTY_INSERT. We can see this below, with separate lines checking that the Counterparty ID exists in the database. The new block ends by sending an ack().

    eventHandler<Counterparty>(name = "COUNTERPARTY_INSERT") {
    onValidate { counterparty ->
    verify {
    counterparty.details.counterpartyId.let { counterpartyId ->
    entityDb hasEntry Counterparty.byId(counterpartyId)
    }
    }
    ack()
    }

    onCommit { event ->
    entityDb.insert(event.details)
    ack()
    }
    }
    verify function

    The verify block you see above is part of the validation helper provided by the platform to make it easier to verify conditions against the database. Outside the verify block, you can write any Kotlin code to validate whatever you want to.

    info

    There is a limitation with the cloneNode() API and how it interacts with FAST bindings and event listeners. So you have to refactor out the components. A clean way to solve this issue is to wrap up your layout contents into individual components - as we are about to do.

    If your components were interacting with each other via the parent component, then we recommend that you change them to interact via the foundation-store utility.

    Exercise 4.2: adding onValidate to Event Handlers

    ESTIMATED TIME

    20 mins

    Add the same verification onValidate as in TRADE_INSERT to the COUNTERPARTY_MODIFY eventHandler codeblock.

    Implement and test the back end with Console or Postman. To do that, see the Day 2 example. Basically, you should create a POST request using the URL http://localhost:9064/EVENT_TRADE_MODIFY, and you must set the header accordingly (header with SOURCE_REF and SESSION_AUTH_TOKEN).

    Add logic to state machine

    In the previous section, we learnt how to implement the onValidate codeblock for eventhandler. Now we are going to learn how to implement in the state machine.

    The implementation is very similar. In the following example, we are adding a simple validation verify if the trade has a valid COUNTERPARTY_ID.

     insertEvent {
    initialStates(TradeStatus.NEW)

    permissions {
    auth(mapName = "ENTITY_VISIBILITY") {
    field { counterpartyId }
    }
    }

    onValidate{ trade ->
    verify {
    db hasEntry Counterparty.byId(trade.counterpartyId)
    }
    }

    onEvent { event ->
    event.withDetails {
    enteredBy = event.userName
    }
    }
    }

    In that example we are using:

    • verify {...}: It expects a boolean return (true | false).
    • db: it is similar to the entityDb. It is the interface so you can have access to the database.

    Auditing

    We want to be able to track the changes made to the various trades in the TRADE table, such that we can see the modifications, the times and the changing states made over the history of the trade. So, we are going to add basic auditing to the TRADE table.

    This can be useful for historical purposes, if you need to be able to produce an accurate course of events at a later date.

    Adding audit to the table dictionary

    For basic auditing, the first step is to change the relevant table dictionary. In this instance, we will make changes to the alpha-tables-dictionary.kts, in order to add the parameter audit = details() to the table definition. It should resemble the following:

        table (name = "TRADE", id = 2000, audit = details(id = 2100, sequence = "TR")) {
    field("TRADE_ID",STRING).sequence("TR").primaryKey()
    field("TRADE_DATE",DATE)
    field("ENTERED_BY",STRING).username().notNull()
    field("TRADE_STATUS", ENUM("NEW","ALLOCATED","CANCELLED")).default("NEW")
    field("COUNTERPARTY_ID",STRING).notNull()
    field("INSTRUMENT_ID",STRING).notNull()
    field("QUANTITY",INT)
    field("PRICE",DOUBLE).notNull()
    field("SYMBOL",STRING)
    field("DIRECTION", ENUM("BUY","SELL")).default("BUY")
    }

    The id parameter indicates the id of the newly created audit table, and must be different from any other table id.

    As we are using GPAL Event Handlers, this is sufficient to enable auditing on this table. A new table is created with the name of the original table, and the _AUDIT suffix added. In this instance, that would be the TRADE_AUDIT table.

    Don't forget to run build and remap.

    Exercise 4.3: testing an audited table

    ESTIMATED TIME

    20 mins

    Try to insert or modify a TRADE and see the auditing happening accordingly. You can use DbMon or Genesis Console to check the data in table TRADE_AUDIT.

    END OF DAY 4

    This is the end of day 4 of our training. To help your training journey, check out how your application should look at the end of day 4 here.