Expressions
The expression formfield-attribute is a powerful feature in AnsibleForms that allows you to create dynamic forms with JavaScript expressions.
Introduction
The expression attribute can be used on multiple formfield types:
enumfield - to populate dropdown options dynamicallyexpressionfield - to grab or generate any form of datatablefield - to populate table data dynamically
JavaScript expressions are evaluated either on the server side (default) or on the client side using the field property runLocal.
- On the server side (
runLocal: falseor omitted) - Code is limited to pre-defined functions, mainly to get external data from REST APIs, files, databases, etc. - On the client side (
runLocal: true) - Code leverages the full JavaScript engine in the browser sandbox for calculations, transformations, and manipulations.
Security Concerns
Since expressions are evaluated using JavaScript’s eval function, there can be a concern for code injection:
- Server-side expressions are limited to predefined functions - no arbitrary code injection is possible
- Client-side expressions are evaluated in the browser sandbox and can be considered safe
Local Expressions
The following examples run with the field property runLocal: true.
They execute in the browser sandbox and can leverage the full JavaScript engine.
Tip: Use the type
localas an alias fortype: expression, runLocal: true, hide: true, noOutput: true
Tip: Use the typelocal_outas an alias fortype: expression, runLocal: true, hide: true
Naming Convention
Use string manipulations to apply naming conventions.
'$(field1) $(field2)'.replace('-','_').toUpperCase()
Calculation
Use math to make calculations.
round($(field1)*$(field2)*Math.PI) // Math.PI is native javascript
Conversions
Convert bytes to gigabytes.
($(size_bytes)/1024/1024/1024).toFixed(2) // toFixed is native javascript method
Convert Array of Objects to HTML Table
fnToTable($(my_array_field),{
tableClass = '',
escapeHtml = true,
emptyCell = '',
includeHeader = true
})
// output : an html-table representation of the array of objects taken from another field called "my_array_field"
// tip : add tableClass = 'table table-striped table-bordered' for bootstrap styling
Get a Name with Incremental Numbering
When working with auto numbering, you might want to find the next name in an array of strings that contains numbering.
Also available as server side expression (prefixed with fn.).
fnGetNumberedName(['server001','server002','server005'],'server###','server001',false)
// result : "server006"
fnGetNumberedName(['server001','server002','server005'],'server###','server001',true)
// result : "server003"
fnGetNumberedName($(fieldlist),'server###','server001',true)
// use another expression field as input for the array
// fnGetNumberedName(array,pattern,default,fillgaps)
// This function searches for a numbered pattern in a list of string,
// increases the highest number and returns a name like the pattern
// - array : an array object, you can use a placeholder to an expression where you know it's an array
// - pattern : a string that hold the # as a digit
// - default : if no value is found, return this default
// - fillgaps : a boolean to indicate it can fill gaps in the numbers, 1,2,3,6 => 4
Object-Array Manipulation
When working with object-arrays, you’ll want to control and manipulate that data - filtering, altering, sorting.
To make your life easy, AnsibleForms comes with a custom helper to keep your code clean.
/*
fnArray.from($(your_array_field)) // custom helper library
.filterBy({property1:'value1'},{property2:'value2'}, ...}} // filters the array by property value (* wildcards allowed)
.regexBy({property1:'regex1'},{property2:'regex2'}, ...}} // filters the array by property matched against regex
.distinctBy('property1','property2', ...) // will make the array entries unique by property
.selectAttr({prop1:'property1',prop2:'property2'}) // only selects a certain property, and you can relabel them
.sortBy('property1','-property2', ...) // will order the array. To have descending add a "-" (minus) before the property
mylist:
[
{
name:'Spiderman',
has_ability: true,
ability: 'Can do whatever a spider can'
},
{
name:'Superman',
has_ability: true,
ability: 'Superstrong,Flying,Laserbeams'
},
{
name:'FamilyGuy',
has_ability: false
},
{
name:'Wolverine',
has_ability: true,
ability: 'Enhanced healing'
}
]
*/
fnArray.from($(mylist)) // take data from another field 'mylist'
.regexBy({name:'.*man$'}) // name must end with 'man'
.filterBy({has_ability:true}) // must have abilities
.selectAttr({name:'name',ability:'ability'}) // only take properties name and ability
.sortBy('-name') // sort by name descending
/*
result :
[
{name:'Superman',ability:'Superstrong,Flying,Laserbeams'},
{name:'Spiderman',ability:'Can do whatever a spider can'}
]
*/
Object-Array Manipulation with Vanilla JavaScript
You can still use all the vanilla JavaScript features to filter and alter your data - such as map, forEach, find, filter, and reduce.
// source : https://medium.com/@jeff_long/understanding-foreach-map-filter-and-find-in-javascript-f91da93b9f2c
mylist=
[
{
name:'Bob',
age: 5
},
{
name:'Tom',
age: 10
},
{
name:'Paul',
age: 30
},
{
name:'Tom',
age: 40
}
]
// simple filter
$(mylist).filter((x) => x.age<=20) // filter age<=20
/* result:
[
{name:'Bob',age:5},
{name:'Tom',age:10}
]
*/
// filter and addition
$(mylist)
.filter((x) => x.age<=20) // filter age<=20
.map((x) => { return {...x,diff:20-x.age} }) // add new property diff
/* result:
[
{name:'Bob',age:5,diff:15},
{name:'Tom',age:10,diff:10}
]
*/
// find
$(mylist).find((x) => x.name=='Tom').age // find age of first Tom
// result: 10
// make sum with reduce
$(mylist).reduce((sum,x) => sum+x.age,0) // calculate sum of ages
// result: 85
// make temp function
((arr,min)=>{
return arr
.filter(x => x.age>=min) // filter
.map(x => {
var preview=`${x.name} (${x.age})`; // make preview string
return {...x,preview:preview} }) // add new preview property
})($(mylist),20) // feed function with mylist and 20
/* result:
[
{name:'Paul',age:30,preview:'Paul (30)'},
{name:'Tom',age:40,preview:'Tom (40)'}
]
*/
Remote Expressions
The following examples run with the field property runLocal: false (default).
They are evaluated on the server side.
Due to the danger of evaluating code server-side, the expression is sanitized and limited to predefined functions that come with AnsibleForms.
Server-side expressions are meant to grab information from various datasources, such as files, REST APIs, SSH commands, and more.
All server-side functions are prefixed with fn. (e.g., fn.fnRestBasic)
Manipulate Date and Time
fn.fnTime().diff(fn.fnTime('2019-10-01'),'day') // number of days between now and 2019-10-01
// This is the core implementation of https://day.js.org
Get CIDR Info from IP
fn.fnCidr('172.16.0.1','255.255.0.0')
// fn.fnCidr(ip,netmask)
// - ip : an ip address
// - netmask : a netmask
//
// it will return the Cidr subnet information as well as expose a `contains` method to check whether an ip is part of this subnet.
// {
// "networkAddress":"172.16.0.0",
// "firstAddress":"172.16.0.1",
// "lastAddress":"172.16.255.254",
// "broadcastAddress":"172.16.255.255",
// "subnetMask":"255.255.0.0",
// "subnetMaskLength":16,
// "numHosts":65534,
// "length":65536,
// "contains":(ip)=>{return ...}
// }
Get SSH Output
fn.fnSsh('root','172.16.0.1','ls -la')
// fn.fnSsh(user,host,command,jq-expression)
// - user : the ssh user
// - host : the host
// - command : the command to trigger by ssh
// - jq-expression : an optional jq-expression (https://jsplay.org)
//
// You must use 'known_hosts' and 'public-key' to setup non-interactive password-less authentication.
// In the settings you can find the public-key and add your target-host to known_hosts
List Files in a Directory
fn.fnLs('/tmp',{ resursive: true, regex: '.*\\.log$', metadata: true })
// fn.fnLs(path,options)
// - path : path to directory
// - options : recursive:boolean, regex:string ,metadata:boolean
// - recursive : whether to list files recursively
// - regex : a regular expression to filter files
// - metadata : whether to include file metadata (size,mtime,... directories will be shown too)
Parse HTML Page
fn.fnParseHtmlWithRegex('https://ansibleguy.com','<h2.*?>(.*?)</h2>','g')
// fn.fnParseHtmlWithRegex(url,regex,flags)
// - url : url to html page
// - regex : a regular expression with at least one group ( )
// - flags : regex flags (such as g,i,m)
//
// it will grab the html source and return an array with all the group matches
Get DNS Info
fn.fnDnsResolve('ansibleguy.com','A')
// fn.fnDnsResolve(fqdn,type)
// - fqdn : a fully qualified domain name
// - type : the type of dns record (A,AAAA,MX,NS,CNAME,TXT,SRV,PTR)
Read JSON File
fn.fnReadJsonFile('/tmp/file.json','.[].name')
// fn.fnReadJsonFile(path,jq-expression)
// - path : path to json file
// - jq-expression : an optional jq-expression (https://jsplay.org)
Read YAML File
fn.fnReadYamlFile('/tmp/file.yaml','.[].name')
// fn.fnReadYamlFile(path,jq-expression)
// - path : path to yaml file
// - jq-expression : an optional jq-expression (https://jsplay.org)
REST API with Basic Authentication
fn.fnRestBasic(
'get',
'https://resturl/api/',
'',
'name_of_credential_in_database',
'[.records[] | {name:.name, email:.email, spouse:.relations.spouse}'],
'name',
false
)
// output : a full json object coming from rest, json transformed with jq, result sorted and transformed by javascript.
// fn.fnRestBasic(method,url,body,credentialname,jq-expression,sort-object)
// - method : get,post,put,patch,delete
// - url : url to restapi (can for example contain a placeholder like 'https://$(serverfield.fqdn)/api/'
// - body : in case of post and put
// - credential-name : it will lookup the credentials as you have saved in the gui
// - jq-expression : an optional jq-expression (https://jsplay.org)
// - sort object : a sort object to order the result
// - hasBigInt : a boolean indicating if it should convert Int64 to string
REST API with Token Authentication
fn.fnRestJwt(
'get',
'https://resturl/api/',
'',
'your_jwt_token_in_text',
'[.records[] | {name:.name, age:.age}]',
['age','name'],
false
)
// output : a full json object coming from rest, json transformed with jq, result sorted, first by age, then by name
// fn.fnRestJwt(method,url,body,token,jq-expression,sort-object)
// - method : get,post,put,patch,delete
// - url : url to restapi (can for example contain a placeholder like 'https://$(serverfield.fqdn)/api/'
// - body : in case of post and put
// - token : it will be add as a Bearer Authorization header
// - jq-expression : an optional jq-expression (https://jsplay.org)
// - sort-object : a sort object to sort the result
// - hasBigInt : a boolean indicating if it should convert Int64 to string
// - tokenPrefix : a prefix, defaults to 'Bearer' (v5.0.0)
REST API with Secured Token Authentication
fn.fnRestJwtSecure(
'get',
'https://resturl/api/',
'',
'name_of_credential_in_database',
'[.records[] | {name:.name, age:.age}]',
['age','name'],
false
)
// output : a full json object coming from rest, json transformed with jq, result sorted, first by age, then by name
// fn.fnRestJwtSecure(method,url,body,token,jq-expression,sort-object)
// - method : get,post,put,patch,delete
// - url : url to restapi (can for example contain a placeholder like 'https://$(serverfield.fqdn)/api/'
// - body : in case of post and put
// - credential_name : Encrypted credential will be looked up. The password will be the token.
// - jq-expression : an optional jq-expression (https://jsplay.org)
// - sort-object : a sort object to sort the result
// - hasBigInt : a boolean indicating if it should convert Int64 to string
// - tokenPrefix : a prefix, defaults to 'Bearer' (v5.0.0)
REST API with Custom Headers
fn.fnRestAdvanced(
'get',
'https://resturl/api/',
'',
{'a_custom_http_header':'your_value','Authorization':'basic base64(my_rest_credential)'},
'.records[].name',
{name:{ignoreCase:true,direction:'desc'}},
false,
false
)
// output : a full json object coming from rest, json transformed with jq, result sorted descending by name
// fn.fnRestAdvanced(method,url,body,{myheader:'value'},jq-expression,sort-object)
// - method : get,post,put,patch,delete
// - url : url to restapi (can for example contain a placeholder like 'https://$(serverfield.fqdn)/api/'
// - body : in case of post and put
// - headers: an object of headers
// - jq-expression : an optional jq-expression (https://jsplay.org)
// - sort-object : a sorting object to order the results
// - hasBigInt : a boolean indicating if it should convert Int64 to string
// - raw : return data and response_headers
// 3 function-placeholders are allowed in the headers:
// - base64(credential_name) : will create a base64 encoded "username:password". prefix with "basic" if required
// - username(credential_name) : will add the username of the credential
// - password(credential_name) : will add the password of the credential
Get Credentials
fn.fnCredentials('credentialname_or_regex','fallback_credentialname_or_regex')
// Try to use `fnRestBasic` and `fnRestJwtSecured` if possible, this avoids passwords and tokens going over the network.
// output : a credential object with all properties (username, password, host, port)
Sort an Array
fn.fnSort(['a','b','z','q','c'],{'':{direction:'asc'}})
// output : sorts the flat array
// fn.fnSort(arrayinput,sort-object)
// - array, coming from expression or rest or database
// - sort-object : a sorting object to order the results
The Sorting Object
In all the data-fetching functions you have the option to sort your data using this sorting object.
// The sorting object can have multiple forms, for example :
'name' // will sort ascending on property name
['name','email'] // will first sort on name, then on email
{name:{direction:'desc'}} // will sort on name descending
{name:{ignoreCase:true}} // will sort on name ascending, ignoring case
['name',{email:{ignoreCase:true,direction:'asc'}}] // will sort on name first, then on email, ignoring case
{'':{}} // a special case for sorting a flat array which has no headername (properties)
{'':{direction:'desc'}} // sort a flat array descending
Run a JSON Query (jq) on an Object
fn.fnJq($(settings),'.mapping | keys',{name:{ignoreCase:true,direction:'desc'}})
// output : a full json object taken from another field called "settings", converted by jq, and sorted desc on property "name"
// fn.fnJq(object, jq, sort-object)
// - object, for example read from rest or yaml file
// - jq-expression : an optional jq-expression (https://jsplay.org)
// - sort-object : a sorting object to order the results
Built-in JQ Functions
In all the data-fetching functions you have the option to add a JSON query (jq) - a powerful language to manipulate data objects or arrays.
While manipulating data, you might need to convert bytes to KB, MB, GB, or round numbers.
That’s why AnsibleForms comes with a few custom handy JQ functions:
- fn2KB: Bytes to KB
- fn2MB: Bytes to MB
- fn2GB: Bytes to GB
- fnRound0: Round with 0 decimals
- fnRound1: Round with 1 decimal
- fnRound2: Round with 2 decimals
- fnRound: Round with 2 decimals
fn.fnRestBasic('get','https://youruri','','CREDS',
'[.records[] | {"Available Capacity":.storage_capacity.available | fn2GB | fnRound }]')
// note you need to pipe into the functions
Get a Name with Incremental Numbering
In an array of strings containing incremental numbers (server001, server002, …), you might want to search for the next available name.
Also available as local expression (without the fn. prefix).
fn.fnGetNumberedName(['server001','server002','server005'],'server###','server001',false)
// result : "server006"
fn.fnGetNumberedName(['server001','server002','server005'],'server###','server001',true)
// result : "server003"
fn.fnGetNumberedName($(fieldlist),'server###','server001',true)
// use another expression field as input for the array
// fn.fnGetNumberedName(array,pattern,default,fillgaps)
// This function searches for a numbered pattern in a list of string,
// increases the highest number and returns a name like the pattern
// - array : an array object, you can use a placeholder to an expression where you know it's an array
// - pattern : a string that hold the # as a digit
// - default : if no value is found, return this default
// - fillgaps : a boolean to indicate it can fill gaps in the numbers, 1,2,3,6 => 4