Welcome to docs|Projections|Using JSONPath handlers

Using JSONPath handlers

This page describes how to use JSONPath handlers in your projection definitions.

To create useful projections you need to process your events in different ways. Serialized supports a number of built-in functions that can be used to transform your events into the desired projection state. The built-in functions use JsonPath expressions to filter what events should be processed, extract data from the event and select the target field in the projection state where the result should be stored.

JsonPath is a simple and concise way to analyse, transform and select data out of a JSON document. With Serialized JsonPath functions you can process your events without writing the transformation code yourself.

It is advised to keep your projections relatively simple. If you find yourself writing complex logic in your projection definition, you should consider if that logic could be represented as a new event on the aggregate, or perhaps writing a custom function instead.

In a handler function you always have access to event data, the projection state and the event metadata.

Configuring a function

Each of the pre-defined functions have a specific purpose and are used to transform your events into the desired state in a specific way. Each function also has a number of supported attributes that can be used to configure the function.

The following is a list of attributes that the functions can support:

  • Event filter - Defines which events should be processed by the function, based on the event data and metadata. This is a JSONPath filter which is empty by default.
  • Event selector - Defines which part of the event should be used as input to the function. This is a JSONPath expression with default value of $.event.
  • Target filter - Looks into the current state of the projection and decides if the function should be executed. This is a JSONPath filter which is empty by default.
  • Target selector - The target selector defines where the result of the function should be stored in the projection state. This is a JSONPath expression with default value of $.projection.

The complete collection of supported pre-defined JSONPath functions and their supported attributes can be found in the reference section.

Event metadata

Additionally, there is also a $.metadata struct containing the fields aggregateId, timestamp, createdAt, updatedAt and optionally tenantId and reference.

Filtering based on event data

A common scenario is to filter events based on the event data. To implement this, you can configure the function to use an eventFilter. The event filter is a JSONPath filter expression that is evaluated against the event. If the expression evaluates to true, the event will be processed by the function. If the expression evaluates to false, the event will be ignored by the function.

For example, if you have an OrderPlaced event that contains the customer loyalty status, such as GOLD or SILVER, you can use that data to create a projection definition that only will process orders for gold members.

The following example will define a projection called gold-member-orders that will contain all placed orders for gold members. Any order placed by a silver member will be ignored.

var mergeGoldMemberOrders = newHandler("OrderPlaced")    .addFunction(merge()        .with(eventFilter("[?(@.customer.loyaltyStatus=='GOLD')]"))        .build())    .build();var definition = singleProjection("gold-member-orders")    .feed("order")    .addHandler(mergeGoldMemberOrders)    .build();projectionClient.createOrUpdate(definition)
const mergeGoldMemberOrders = {  eventType: 'OrderPlaced',  functions: [    {      function: 'merge',      eventFilter: '[?(@.customer.loyaltyStatus=='GOLD')]'    }  ],}const definition = {  feedName: 'order',  projectionName: 'gold-member-orders',  handlers: [mergeGoldMemberOrders]};await projectionsClient.create(definition)

Filtering based on projection state

To apply a condition based on the state of your projection, you need to use targetFilter. Target filters are used together with target selectors to decide if, and where the data should be written to the projection.

If the filtered expression does not match anything in the projection, the function will not be executed and the next function in the event handler will be evaluated.

Applying target filters is useful for many use cases. For example, if you want to create a projection that adds information to a certain part of a projection or if the projection is in a certain state.

The following example shows how to create a projection that only adds information to the customer part of the state if the customer is a GOLD member.

var addPointsForGoldMembers = newHandler("OrderPlaced")  .withIdField("customerId")  .addFunction(Functions.merge().build())  .addFunction(Functions.add()    .with(eventSelector("points"))    .with(new TargetSelector("$.projection[?].loyaltyPoints")))    .with(targetFilter("[?(@.loyaltyStatus=='GOLD')]"))    .build())  .build();  var definition = singleProjection("customer-loyalty")  .feed("order")  .addHandler(addPointsForGoldMembers)  .build();
const addPointsForGoldMembers = {  eventType: 'OrderPlaced',  idField: 'customerId',  functions: [    {      function: 'merge'    },    {      function: 'add',      eventSelector: '$.event.points',      targetSelector: '$.projection[?].loyaltyPoints',      targetFilter: '[?(@.loyaltyStatus=='GOLD')]'    }  ],}const projectionDefinition = {  feedName: 'order',  projectionName: 'customer-loyalty',  handlers: [addPointsForGoldMembers]};

Providing raw data

If you have a static value that you want to use as the data for the projection instead of some value that is provided in the event, you can use the rawData argument instead of the eventSelector.

You can then provide any JSON value which will be used as it is in the projection definition, number, strings, objects and arrays.

In the following example, a projection definition called customer-status is created. This definition will give newly registered customers the loyalty status of BRONZE.

var setBronzeStatusForNewCustomers = newHandler("CustomerRegistered")  .addFunction(Functions.set()      .with(targetSelector("loyaltyStatus"))      .with(rawData("BRONZE"))      .build())  .build();  var definition = singleProjection("customer-status")  .feed("customer-registrations")  .addHandler(setBronzeStatusForNewCustomers)  .build();  projectionClient.createOrUpdate(definition)
const setBronzeStatusForNewCustomers = {  eventType: 'CustomerRegistered',  functions: [    {      function: 'set',      targetSelector: '$.projection.loyaltyStatus',      rawData: 'BRONZE'    }  ],}const definition = {  feedName: 'customer-registrations',  projectionName: 'customer-status',  handlers: [setBronzeStatusForNewCustomers]};await projectionsClient.create(definition)

More on filters

All Filter Operators that are described here are supported.