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 -> ERRORActiveQL 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 function | Description |
|---|---|
no | replacement for not |
at | get | get the value at a path |
value | exactly this value |
atIndex | true when the path is at a given index |
nil | true when the value is null or undefined |
notNil | true when the value is not null or undefined |
eq | true when two values are the same |
neq | true when two values are not the same |
lt | true when one value is lower than another value |
lte | true when one value is lower than or equal to another value |
gt | true when one value is greater than another value |
gte | true when one value is greater than or equal to another value |
map | replaces values in a list |
includes | true when a list contains a value |
notIncludes | true when a list does not contain a value |
filter | filters a list for items that have a certain attribute/property |
age | the age as of today or another date for a birthdate |
upper | converts a string to upper cases |
lower | converts 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 falsewhich is the same as
if age( at('birthdate'), at('contract.start') ) >= 18 and notNil( at('lastname') ) then true else falsevalue
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:
| Function | Expression | Description |
|---|---|---|
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" ) )' // -> 2100Or 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' // -> trueExcel-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.