Expressions in Novulo

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:

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 the Count operator, which has List 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 datatype addresses. The filter expression has the context addresses and returns the datatype boolean. The sort expression has the context addresses 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.
afbeelding
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 is load(); 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 enumeration yesno the item with id 1 (yes)
persons:load(2) returns from the table persons 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 of yesno: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) returns false

  • 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.

5 Likes