Domain configuration
FEEL expressions

FEEL Expressions

In some dynamic configuration parameters you can use an expression using the FEEL Language (opens in a new tab)

We suggest familiarizing with this expression language e.g. here: https://learn-dmn-in-15-minutes.com/learn/the-feel-language.html (opens in a new tab)

ActiveQL supports the use of FEEL for evaluating mutation input or other use cases with some enhancements.

Let's assume we have data in the following form (that might come from an operation input):

{
  rental: {
    date: '2023-12-12',
    car: {
      brand: "Mercedes",
      power: 200,
      color: "black", 
      accessories: [
        { category: "interior", name: "floor mats", price: 80 },
        { category: "exterior", name: "rear spoiler", price: 310 },
        { category: "exterior", name: "sport tyres", price: 2100 },
        { category: "electronics", name: "speed trap detector", price: 1990 },
      ]
    },
    driver: {
      firstname: "Thomas", 
      lastname: "Thompson",
      birthdate: "2002-11-11",
      deliveryAddress: {
        zip: "80354", 
        city: "Munich", 
        street: "Karlsplatz 1",             
      }, 
      invoiceAddress: {
        zip: "20354", 
        city: "Hamburg", 
        street: "Jungfernstieg 1"
      }
    }
  }
}

Standard FEEL expressions

Here are some examples how to apply a standard FEEL expression:

    'rental.car.brand' // -> 'Mercedes'
    'if rental.car.power > 100 then "YES" else "NO"' // -> 'YES'
    'some accessory in rental.car.accessories satisfies accessory.price > 300' // -> true
    'string length( rental.driver.lastname )' // -> 8
    'count( rental.car.accessories )' // -> 4
    'if vehicle then vehicle.color else null' // rental.notes is undefined -> null
    'rental.driver.age >= 18' // age is undefined -> ERROR

ActiveQL functions

As you might have seen from the last two examples it is sometimes inconvenient to access data without error, especially when you can not be sure how the data look like. This makes comparisons and other expressions complicated. Therefore ActiveQL allows you to use the following functions in your FEEL expressions.

ActiveQL functionDescription
noreplacement for not
at | getget the value at a path
valueexactly this value
atIndextrue when the path is at a given index
niltrue when the value is null or undefined
notNiltrue when the value is not null or undefined
eqtrue when two values are the same
neqtrue when two values are not the same
lttrue when one value is lower than another value
ltetrue when one value is lower than or equal to another value
gttrue when one value is greater than another value
gtetrue when one value is greater than or equal to another value
mapreplaces values in a list
includestrue when a list contains a value
notIncludestrue when a list does not contain a value
filterfilters a list for items that have a certain attribute/property
agethe age as of today or another date for a birthdate
upperconverts a string to upper cases
lowerconverts a string to lower cases

at | get | @

When dealing with nested data the FEEL syntax becomes complicated since you must always check if an object exist in the data access it's properties. For this you can use the at function. The function takes a path and tries to resolve this path somewhere in the data starting at root. If no such path exists in the data at returns undefined without logging any error.

If you provide the whole path it acts the same simply referring to the data at this path.

    'at( "rental.car.brand" )' // -> 'Mercedes'.

Different from the standard FEEL data access you don't get an error when there is no data at the path. This also works for nested paths. at will never complain about an invalid path.

    'at( "rental.vehicle.brand" )' // -> undefined; no error in log.

You don't even have to provide the whole path from the beginning. at tries to find a path somewhere inside the data. Let's assume in the data looks something like this:

{
  "rental": {
    "vehicle": {
      "brand": "Mercedes"
    }
  }
}

Since there is just one brand property somewhere in the data we could access it via:

    'at( "brand" )' // -> Mercedes.

When a path can occur more than once in your data at would return its first occurrence. You can however qualify the path to a path part that is unique enough and get the respective value

{
  "rental": {
    "deliveryAddress": {
      "city": "Munich"
    }, 
    "invoiceAddress": {
      "city": "Hamburg"
    }
  }
}
    'at( "invoiceAddress.city" )' // -> Hamburg.

The path can also access array values at a certain position.

    'at( "accessories.0.price" )' // -> 80.

@ shortcut

Instead of writing at(some.path) you can simply write @some.path it will be substituted with the method call before evaluation.

This works in any place, so this would be a valid expression:

if age( @birthdate, @contract.start ) >= 18 and notNil( @lastname ) then true else false

which is the same as

if age( at('birthdate'), at('contract.start') ) >= 18 and notNil( at('lastname') ) then true else false

value

value( value:unknown )

Returns exactly the value that this method gets as parameter

    'value( "Porsche" )' // -> Porsche.
    'value( 23 )' // -> 23.

atIndex

atIndex( index:number|number[] ): boolean

Sometimes an expression should give a result based on the index of the path. When there is a nested list in compares the index starting at the end.

In this example the attribute number should be required, but only for the first Document of the first Driver.

operation: 
  Rental: 
    input: 
      Car: 
        list: true
        assoc: 
          Driver: 
            list: true
            assoc: 
              Document: 
                list: true
                attributes: 
                  name: String
                  number: 
                    type: String
                    required: 
                      expression: atIndex( [0, 0] )

no

no( value:PathOrExpression ): boolean

beware: not is not working the evaluation of FEEL expression is done by js-feel (opens in a new tab). The not expression in this library seems not to work and produces errors. Therefore we added a no expression with the same syntax and behavior.

Negates the expression. If expression is not a boolean this always returns false .

' no( true ) ' // false.
'no( rental.car.power > 300 )' // false.

nil

nil( value:PathOrExpression ): boolean

Returns true when the when the expression is either null or undefined;

notNil

notNil( value:PathOrExpression ): boolean

Returns true when the when the expression is neither null nor undefined;

eq

eq( left:PathOrExpression, right:PathOrExpression ): boolean

If you want to compare two value for equality you can do this in FEEL via =. If one of the values is missing you would get an error log entry. To prevent this you can use the function eq which behaves like the Lodash _.equal method. The values of this functions are either literal values or data value at a certain path. When a parameter starts with @ its value is determined by the at function.

You can compare a path value with a literal value

    'eq( @power, 200)' // -> true.
    'eq( @accessories.0.category, "interior")' // -> true.

You can evaluate the result further. In this example we negate the result. Please note the usage of no instead of not

    'no( eq( @deliveryAddress.zip, @invoiceAddress.zip ) )' // -> true.

neq

neq( left:PathOrExpression, right:PathOrExpression ): boolean

Negated result as for eq.

lt - lte - gt - gte

You can compare two values (either path or literal values) with these functions:

FunctionExpressionDescription
lt( left:PathOrExpression, right:PathOrExpression ): boolean<lower than
lte( left:PathOrExpression, right:PathOrExpression ): boolean<=lower than or equal
gt( left:PathOrExpression, right:PathOrExpression ): boolean>greater than
gte( left:PathOrExpression, right:PathOrExpression ): boolean>=greater than or equal

You can compare two path values with each other.

    'lt( @accessories.0.price, @accessories.1.price )' // -> true.

You can compare a path value with a literal value

    'lte( @power, 200)' // -> true.

You can also compare strings alphabetically

    'gt( @deliveryAddress.city, @invoiceAddress.city )' // -> true.

map

map( list:PathOrExpression, property:PathOrExpression ): List

You can map a list of values to a list of one property of this list. Let's we need a list of the names from the accessories list. We could map the list to its name property.

    'map( @accessories, "name" )' // -> ["floor mats", "rear spoiler", "sport tyres", "speed trap detector"]

With this it is also easy to get the maximum price of all accessories:

    'max( map( @accessories, "price" ) )' // -> 2100

Or e.g. a unique list of of the accessory categories

    'distinct values( map( @accessories, "category" ) )' // -> ["interior", "exterior", "electronics"]

includes

includes( first:PathOrExpression, second:PathOrExpression): boolean

You can test whether a value is included in a list of values. The list can be the 1st or 2nd parameter, as long as one of the parameters is a list, it is tested whether the other value is included in the list or not.

    'includes( distinct values( map( @accessories, "category" ) ), "interior" )' // -> true.

notIncludes

notIncludes( first:PathOrExpression, second:PathOrExpression): boolean

Negated result as for includes.

age

age( birthdate:PathOrExpression, atDate?:PathOrExpression ): int

A common requirement is to check the age of a person from which you know the birthdate. You can use the age function for that.

The age function calculates the age for the currentTime date in the context, which should be present and is set at request time by calling runtime.now()

    'age( @driver.birthdate )' // -> 20 (assuming this runs at the date 2023-10-10)

Alternatively you can provide as second parameter to the age function a date at which the age should be calculated. For example if you want to know whether the driver is 21 not now but at the rental date you could write:

    'age( @driver.birthdate, @rental.date ) >= 21' // -> true

Excel-like Formulas

You can use MS-Excel like formulas, which are especially useful for calculations. ActiveQL uses the https://formulajs.info (opens in a new tab) implementation. Please refer to the documentation for available Formulas (opens in a new tab).

Example

This expression, using the Formula ACCRINT (which returns the accrued interest for a security that pays periodic interest)

ACCRINT(@accrint.Issue, @First_interest, @Settlement, @Rate, @Par, @Frequency, @Basis)

and this evaluation context data

const data = {
  accrint: {
    Issue: '2012-01-01', 
    First_interest: '02/01/2012', 
    Settlement: new Date('2017-01-01'), 
    Rate: 0.1,
    Par: 1000, 
    Frequency: 1, 
    Basis: 0
  }
}

evaluates to 500. As you might have seen it recognizes date values as string (ISO or US) or as a Date object.

Custom functions

You can add your own functions that will become available in the context of an expression evaluation by adding a decorateFeelExpressionContextFn callback to your DomainConfiguration.

Example

const domainConfig:DomainConfiguration = {
  decorateFeelExpressionContextFn: (context) => {
    _.set( context, 'wordCount', (value:unknown) => _.words( _.toString(value) ).length )
  }
}

You can you use your custom function the same as any other function in any expression.

This expression

wordCount( @bar )

with this evaluation context

const context = 
{
  foo: { bar: 'bay (bax) baz' }
}

would evaluate to 3.