Expressions in Novulo
Novulo has its own expression language. It is used for the development of components and for the configuration of applications. It allows for the retrieval of data and the calculation of values. Novulo expressions are parsed into SQL SELECT statements during their execution. They can be built/displayed visually in the architect or as plain text.
Novulo has its own expression language, for several reasons:
- It allows for creating expressions visually using low code in the Architect
- It has a convenient syntax
- Evaluating a Novulo expressions is divided in useful steps
And more technical reasons that I wonât elaborate on for now:
- It can work with the Reference Architecture
- It can get data from the database, IIS webserver and Javascript
- Itâs independent of database software and version
Our expression language is used everywhere: in development and implementation. You can approach the expression language from different angles, but in this article Iâll start with the development angle, as the most fundamental aspects of the language will make the most sense.
Novulo Expressions in the Architect
In development youâll use expressions to:
- Give properties to elements
Example of a visibility property - Create functions
Example of functions - Fill in parameters of processes.
Example of the parameters of a process component
Every expression can be created using the low code Expression Editor, where the palette shows all the available operators and the context shows all the available data
Example of a function in the Expression Editor
Datatype
Depending on the datatype of your expression, you can choose the appropriate operators. For instance, if your expression has to return the datatype string (text)
, you can use the operator Concat
to concatenate multiple string values.
The datatype of your expression depends on where the expression is used. If your expression is a function, you can set the type as you wish.
Setting the datatype for a new function
If your expression is used for a property or a parameter, the type is predetermined and your expression needs to adhere to the property or parameter. For instance, a visibility condition always has the datatype Boolean
, meaning you can use operators like Equals
, GreaterThan
, And
, Or
, If
, etc.
We can seperate datatypes in 5 categories:
- Primary datatypes
- Are readable for humans
int
,string
,date
,money
, etc.
- Enumerations
- Fixed tables with descriptions and idâs
- Are not readable for humans; we either read the description or the id
- Can be created in the Architect
YesNo
,DraftFinal
,ContactType
, etc.
- Record datatypes or Recordtypes
- Dynamic tables with defined columns and undefined rows
- Are not readable for humans; we only read the columns with primary datatypes
- Are all created in the Architect
contacts
,products
, etc.
- Lists
- Lists of the same datatype
- Can be made with fixed items of a dynamic range of items
LiteralList
,List<int>
,List<YesNo>
,List<products>
, etc.
- Tuples
- Pairs of values of any datatype
- Often the first value is a string, which functions as a âkey valueâ
Tuple<string,products>
, etc,
You can always see the expected datatype in the expression editor at the top or top-right of the grey input box. Furthermore, if you select the box, your palette will grey out all operators that wonât work and leave the available operators blue.
A
String
function can use Concat
, but not Contains
Most operators have parameters, which are again specified in the grey boxes.
The
StringJoin
operator has a String
parameter called âseparatorâ and a List<String>
parameter called âinputâ
Context
Depending on where you make your expression, you have access to different data. For instance, if you add a function to the Person page, you can use the data from the Persons record. The Persons record is the context of this expression.
The function âTelephone numberâ uses the data
mobile
and telephone
from the Persons record
This applies to every expression.
- Expressions on a page can use the data of that record;
- Functions on the page
- Properties of elements (visibility, editability, validation)
- Expressions on a grid can use the data of that list of records;
- Filter property of the table
- Function columns
- Expressions in a process use the data of that process (trigger, action return values);
- Parameters of actions and decisions
Every expression has a context, and some expressions have different layers of context. If you are working in a context, and include a list of different data, then that list will have itâs own context. A couple of examples are:
- When grids are added to a detail page. The first context is the record of the page, the second context is the records in the grid.
On the Organization page, the Addresses table is added. The first context is the Organization record and the second context is the Addresses records* - When a dropdown field or a search link is added to a detail page. The first context is the record of the page, the second context is the records of the data source of the dropdown field/search link.
On the Person page, the dropdown field âPrimary employerâ is added. The first context is the Person record and the second context is the data source of the dropdown field âConnection contactsâ* - When a list expressions is used in another expression.
The function âHas user accountâ on the Person page uses theCount
operator, which hasList of N_ApplicationLoginAccount
, a list of âUser accountsâ, as input. The first context is the Person record and the second context is the User account records*
Note that I sometimes refer to context as one record, and sometimes as multiple records. Technically, the context of an expression is always one record. If an expression is made in the context of a list, you have to imagine the expression being evaluated for every one record within that list seperately.
Parent context
Usually, when an expression is in a second layer of context, you need to refer to the upper layer. We call this referring to the parent context. You do this with the ParentContext
operator. The most common example is filtering your list based on the parent context.
In the previous 3 examples, all lists are filtered: the Addresses grid only shows addresses that belong to the Organization; the Primary employer dropdown only shows connection contacts that belong to the Person and the âHas user accountâ function only counts user accounts that belong to the person.
Notice how the context in the Expression Editor changes depending on the selected parameter
If you want to test the expressions that you build in the architect you can of course deploy the component and view the result in your application. However, it is also possible to debug architect expressions without having to wait for whole deployment cycle. This can significantly speed up your development process.
Novulo Expressions in your application
If you want to use Novulo Expressions in your configuration, you need to know where the expressions are used, and how.
Expression field
In the configuration of your application, youâll use expressions in every instance of an expression field. These are implemented in various components with various functionalities. These expression fields have a predefined datatype, and a predefined context. This means that the expression field knows which expressions are (in)valid, and this allows for the expression field to have auto-complete functionality.
Examples of implementations of the expression field are:
- Novulo Export
In an export dataset, the proofing record, filter expression and sort expression are all expression fields. The proofing record has no context, and returns the datatypeaddresses
. The filter expression has the contextaddresses
and returns the datatypeboolean
. The sort expression has the contextaddresses
and returns the datatype[sortfunctions]
([]
meaning a list of sortfunctions)
Auto-complete
The auto-complete functionality is triggered by Ctrl + Space
. Youâll get a list of all the available fields and functions within the context of the expression field. If the context of your expression changes, the auto-complete follows.
The auto-complete of this filter expressions shows the fields and functions of the
addresses
datatype
Error handling
Before saving your record, the expression field will validate your expression. It will check whether:
- The expression returns the correct datatype
- The fields and functions used in the expression exist within the context
- Correct syntax is used
If your expression is invalid, you can read the error by hovering over the orange triangle.
The filter expression requires the datatype boolean
, but the expression "this is not a boolean expression"
returns the datatype string
Expression debugger
Every application is deployed with the expression debugger. This tool has a couple of features that help you to write expressions more easily.
You can find the expression debugger in the toolbar at the top-right of your application.
- Evaluate expressions on the left
- Open the data model to get an overview of all primary, enumeration and record datatypes, and all fields and functions that exist for those datatypes
Use the âEvaluateâ button to get a result for your written expression, and use the âDatamodelâ button to open the data model on the right.
Make sure to test out the examples used in the next chapter in the expression debugger of your application!
Syntax
Iâll divide the syntax of our expression language into two categories: evaluating a single expression vs. evaluating multiple expressions. If you know how to evaluate a single expression, you can use this expression in a list or tuple format to evaluate multiple expressions at the same time (returning a list or tuple of results)
Syntax for evaluating a single expression
An expression can be evaluated with or without context, where evaluating an expression with context is merely a shortcut of evaluating without context. As such, weâll start by looking at the syntax for expressions without context, and then look at what shortcuts are provided by context.
- The basis of every expression starts by declaring a data type, followed by a colon, and then calling a function, which always ends in two brackets:
DataType:function()
. The most basic function isload()
; allowing you to select a specific value from a data type
int:load(1)
returns the integer number 1
money:load(1)
the value 1 for the current currency (i.e. âŹ1,00)
yesno:load(1)
returns from the enumerationyesno
the item with id 1 (yes)
persons:load(2)
returns from the tablepersons
the record with id 2
datetime:now()
returns the current date and time
string:getnull()
returns an empty string (not to be mistaken with the string""
, which is not empty!
- After calling a function, you can expand on the result of your function with a period, and then calling a field or another function:
.field
or.function()
. This can happen as often as needed
yesno:load(1).description
returns the description ofyesno:load(1)
, âYesâ
persons:load(2).age()
returns the age of the person with id 2
persons:load(2).primary_address.country.iso_code
returns the ISO code of the country of the primary address of the person with id 2
- Sometimes, a function requires one or more parameters. The amount and data type of the parameters is determined by the function, and by the data type from which the function is called. As seen in the examples above,
load()
usually requires one integer parameter. However,date:load()
requires a string parameter in the format âYYYY-MM-DDâ
date:load("2024-06-12")
returns the date of writing this chapter
date:load("2024-06-12").adddays(int:load(-1))
returns the date of the first Novulo Composable Futures event
int:load(1).add(int:load(1))
returns the integer number 2
persons:load(2).primary_address.country.iso_code.substring(0,1)
returns the first letter of the ISO code of the country of the primary address of the person with id 2
- Sometimes basic functions for primary values can be skipped
int:load(1).add(1)
returns the integer number 2
money:load(1).add(1)
returns the value 2 for the current currency (i.e. âŹ2,00)
Usually, an expression is written in context. This means that before evaluating your expression, the expression field will select a record for you to evaluate the expression in; so you can skip selecting a record with the load()
function.
You can simulate this in the expression debugger, by selecting a record in the âContextâ area. The following examples assume that the context is persons:load(2)
.
this
returns the current context
this
returns the person with id 2
this.age()
returns the age of the person with id 2
persons:load(3).equals(this)
returnsfalse
- In most cases, you can skip
this
in your expressions
age()
returns the age of the person with id 2
primary_address.country.iso_code
returns the ISO code of the country of the primary address of the person with id 2
Sometimes, your expression also has parent context. This can be simulated in the expression debugger by selecting another record in the context area, seperated by a comma. The following examples assume that the context is persons:load(2)
and that the parent context is organizations:load(1)
.
parent
returns the current parent context
parent
returns the organization with id 1
parent.is_my_organization
returns the value of the check box âIs my organizationâ of the organization with id 1
this.primary_connection.connection_to.organization().equals(parent)
returns the answer to the question âIs organization 1 the primary employer of person 2?â
Syntax for evaluating multiple expressions
Now that you know how to evaluate a singular expression, you can create lists or tuples. Lists can be static or dynamic, and tuples are always a static pair of two expressions.
- Static lists are created by putting expressions, comma seperated, between square brackets:
[Expression1, Expression2, ...etc.]
[1,2,3]
returns a list of numbers 1-3
[1,"2",money:load(3)]
returns a list of the number 1, the string â2â, and the value 3 for the current currency (i.e. âŹ3,00)
[persons:load(2).age(),persons:load(3).age(),persons:load(4).age()]
returns a list of the age of the persons with id 2-4
- Dynamic lists are created by declaring the data type, the expression you want to evaluate, the filter of your data, and finally the sorting of your data:
{DataType, Expression, Filter, Sorting}
.
The expressions used in this list, always have the data type as context. The filter and sorting are optional.
{persons,this}
returns a list of all persons
{persons,age()}
returns a list of the age of all persons
{persons,name, id.isgreater(2)}
returns a list of the names of all persons with an ID higher than 2
{persons,name, true, id.desc}
returns a list of the names of all persons without filtering. The list is sorted by ID descendiing. The person with the highest ID is the first one in the returned list.