Query language

Describe the query language, which is shared by the REST API as well as the Javascript API

Overview

The query language enables you to search for records within an entity using complex criteria. This language is used by both the REST API and the Javascript API. However, please note that the method of specifying queries differs between the two.

For example, a simple query to retrieve 10 active companies using the REST API looks like this:

GET https://app.{{ site.slingr_domain }}/prod/runtime/api/data/companies?state=active&_size=10

In contrast, with the Javascript API, it would look something like this:

var records = sys.data.find('companies', {state: 'active', _size: 10});
while (records.hasNext()) {
  // do something
}

In the Javascript API, you can also utilize the query builder), which provides helpful tools for constructing queries:

var query = sys.data.createQuery('companies')
    .field('state').equals('active')
    .size(10);
var records = sys.data.find(query);
while (records.hasNext()) {
  // do something
}

Generally, when dealing with more intricate query criteria, the query builder is easier to use than the simple map form because it offers conversion and parsing features. Additionally, there are queries that cannot be expressed using the query map version. For instance:

var query = sys.data.createQuery('companies').query(function(q) {
  return q.or(
    q.field('name').like('test'),
    q.field('name').equals('abc')
  );
});

The above query cannot be expressed using the query map version because you cannot use the same key in the map. It would look like this:

var query = {
  name: 'like(test)',
  name: 'like(abc)'
};

In this case, the key name appears twice in the map and won’t work as expected.

Another consideration is exception handling. When using the query builder, exceptions may be thrown during query construction. For example:

try {
  var query = sys.data.createQuery('companies')
      .field('doesNotExist').equals('test');
  log('count: '+sys.data.count(query);
} catch (e) {
  log('error: '+e.message());
}

The script above will throw an exception when calling the field() method because the field doesNotExist is not a valid field in the companies entity. Similar exceptions will occur with other improper query configurations, so be aware that exceptions may arise during query construction.

On the other hand, when using the query map, exceptions won’t occur during query construction but rather when executing the query:

try {
  var query = {doesNotExist: 'test'};
  var count = sys.data.count('companies', query);
  log('count: '+count);
} catch (e) {
  log('error: '+e.message());
}

In this case, the exception will be thrown when calling the sys.data.count() method.

This documentation section provides samples in all three versions for comparison. If a feature only applies to specific versions, it will be explicitly indicated.

Simple queries

Simple queries allow you to filter records using the AND logical operator exclusively, although you can still specify multiple filters. Here are some simple query examples:

// finds a company by exact name 
var records_00 = sys.data.find('companies', {name: 'Blogpad'});
log('total: '+records_00.count());
while (records_00.hasNext()) {
    log(records_00.next().label());
}
// finds a company by exact name 
var query_00 = sys.data.createQuery('companies')
    .field('name').equals('Blogpad')
var records_00 = sys.data.find(query_00);
log('total: '+records_00.count());
while (records_00.hasNext()) {
    log(records_00.next().label());
}
// finds a company by exact name 
GET /data/contacts?name=Blogpad

// filters by companies with more than 200 employees; limits results to 10 and use offset of 20 
var records_01 = sys.data.find('companies', {numberOfEmployees: 'greater(200)', _size: 10, _offset: 20});
log('total: '+records_01.count());
while (records_01.hasNext()) {
    log(records_01.next().label());
}
// filters by companies with more than 200 employees; limits results to 10 and use offset of 20 
var query_01 = sys.data.createQuery('companies')
    .field('numberOfEmployees').greater(200).size(10).offset(20)
var records_01 = sys.data.find(query_01);
log('total: '+records_01.count());
while (records_01.hasNext()) {
    log(records_01.next().label());
}
// filters by companies with more than 200 employees; limits results to 10 and use offset of 20 
GET /data/contacts?numberOfEmployees=greater(200)&_size=10&_offset=20

// filters by two fields 
var records_02 = sys.data.find('companies', {type: 'a', isCustomer: false});
log('total: '+records_02.count());
while (records_02.hasNext()) {
    log(records_02.next().label());
}
// filters by two fields 
var query_02 = sys.data.createQuery('companies')
    .field('type').equals('a').field('isCustomer').equals(false)
var records_02 = sys.data.find(query_02);
log('total: '+records_02.count());
while (records_02.hasNext()) {
    log(records_02.next().label());
}
// filters by two fields 
GET /data/contacts?type=a&isCustomer=false

// filters by a nested field 
var records_03 = sys.data.find('companies', {'addresses.state': 'CA'});
log('total: '+records_03.count());
while (records_03.hasNext()) {
    log(records_03.next().label());
}
// filters by a nested field 
var query_03 = sys.data.createQuery('companies')
    .field('addresses.state').equals('CA')
var records_03 = sys.data.find(query_03);
log('total: '+records_03.count());
while (records_03.hasNext()) {
    log(records_03.next().label());
}
// filters by a nested field 
GET /data/contacts?addresses.state=CA

// retrieves only name and type of customers 
var records_04 = sys.data.find('companies', {isCustomer: true, _fields: 'name,type'});
log('total: '+records_04.count());
while (records_04.hasNext()) {
    log(records_04.next().label());
}
// retrieves only name and type of customers 
var query_04 = sys.data.createQuery('companies')
    .field('isCustomer').equals(true).includeFields('name', 'type')
var records_04 = sys.data.find(query_04);
log('total: '+records_04.count());
while (records_04.hasNext()) {
    log(records_04.next().label());
}
// retrieves only name and type of customers 
GET /data/contacts?isCustomer=true&_fields=name,type

In the query map and REST API, parameters starting with _ are system parameters, while others are fields. The next section explains all the options for filtering by fields.

Filters by field

You can filter results by field value. For instance, if your entity has a field called name, you can make the following query:

//  
var records_05 = sys.data.find('companies', {name: 'Blogpad'});
log('total: '+records_05.count());
while (records_05.hasNext()) {
    log(records_05.next().label());
}
//  
var query_05 = sys.data.createQuery('companies')
    .field('name').equals('Blogpad')
var records_05 = sys.data.find(query_05);
log('total: '+records_05.count());
while (records_05.hasNext()) {
    log(records_05.next().label());
}
//  
GET /data/contacts?name=Blogpad

You can also provide several values (comma-separated), and it will act like an OR:

//  
var records_06 = sys.data.find('companies', {name: 'Blogpad,Jazzy'});
log('total: '+records_06.count());
while (records_06.hasNext()) {
    log(records_06.next().label());
}
//  
var query_06 = sys.data.createQuery('companies')
    .field('name').equals('Blogpad,Jazzy')
var records_06 = sys.data.find(query_06);
log('total: '+records_06.count());
while (records_06.hasNext()) {
    log(records_06.next().label());
}
//  
GET /data/contacts?name=Blogpad,Jazzy

If the value you are matching already contains a comma, you should wrap it in double quotes:

//  
var records_07 = sys.data.find('companies', {name: '"ACME, Inc"'});
log('total: '+records_07.count());
while (records_07.hasNext()) {
    log(records_07.next().label());
}
//  
var query_07 = sys.data.createQuery('companies')
    .field('name').equals('"ACME, Inc"')
var records_07 = sys.data.find(query_07);
log('total: '+records_07.count());
while (records_07.hasNext()) {
    log(records_07.next().label());
}
//  
GET /data/contacts?name="ACME, Inc"

You can filter by nested fields as well:

//  
var records_08 = sys.data.find('companies', {'contactInformation.email': 'dwelchdp@apple.com'});
log('total: '+records_08.count());
while (records_08.hasNext()) {
    log(records_08.next().label());
}
//  
var query_08 = sys.data.createQuery('companies')
    .field('contactInformation.email').equals('dwelchdp@apple.com')
var records_08 = sys.data.find(query_08);
log('total: '+records_08.count());
while (records_08.hasNext()) {
    log(records_08.next().label());
}
//  
GET /data/contacts?contactInformation.email=dwelchdp@apple.com

If the nested group field is multi-valued, you can still perform a query like this:

//  
var records_09 = sys.data.find('companies', '{'_or': {'type': 'a', '_and': {'type': 'b', 'isCustomer': true}}}');
log('total: '+records_09.count());
while (records_09.hasNext()) {
    log(records_09.next().label());
}
//  
var query_09 = sys.data.createQuery('companies')
    '.query(function(q) { return q.or(q.field('type').equals('a'), q.and(q.field('type').equals('b'), q.field('isCustomer').equals(true))); })'
var records_09 = sys.data.find(query_09);
log('total: '+records_09.count());
while (records_09.hasNext()) {
    log(records_09.next().label());
}
//  
GET /data/contacts?_query=or(filter('type','a'),and(filter('type','b'),filter('isCustomer','true')))

If the entity has a geo point type, a near operator can be used to perform a query. The operator takes 4 parameters:

  • latitude
  • longitude
  • minDistance(optional)
  • maxDistance(optional)

// sorts companies by proximity to [ lat: 10, long: 22 ] that are at a min distance of 5000 m and at a maximum of 10000 m 
var records_sample = sys.data.find('companies', {'location':'near(5,10,5000,10000)'});
log('total: '+records_sample.count());
while (records_sample.hasNext()) {
    log(records_sample.next().label());
}
// sorts companies by proximity to [ lat: 10, long: 22 ] that are at a min distance of 5000 m and at a maximum of 10000 m 
var query_sample = sys.data.createQuery('companies')
    .field('location').near(5,10,5000,10000)
var records_sample = sys.data.find(query_sample);
log('total: '+records_sample.count());
while (records_sample.hasNext()) {
    log(records_sample.next().label());
}
// sorts companies by proximity to [ lat: 10, long: 22 ] that are at a min distance of 5000 m and at a maximum of 10000 m 
GET /data/contacts?near(5,10,5000,10000)

Available operators for filtering:

equals(value)
notEquals(value)
like(value)    // options: /value, value/ or /regexp/
between(from,to)
greater(value)
greaterOrEquals(value)
less(value)
lessOrEquals(value)
empty()
notEmpty()

Each field type supports different operators. Refer to the documentation for each type to see which operators are supported.

The following operators are available for all types:

equals(value)
notEquals(value)
empty()
notEmpty()

If you don't specify any operator, the default one is equals().

It's a good practice to consult the documentation for each type when filtering by field values, as each type may support different querying methods and alternative formats for simplifying usage.

Complex queries

Complex queries are similar to regular queries, but they allow you to mix and nest OR and AND expressions. They are separated because they require a different syntax, especially in the REST API and query builder.

Here’s a sample complex query, highlighting the differences between the three versions:

// Query on companies 
var records_10 = sys.data.find('companies', { _or: { type: 'a', _and: { type: 'b', isCustomer: true }}});
log('total: '+records_10.count());
while (records_10.hasNext()) {
    log(records_10.next().label());
}
// Query on companies 
var query_10 = sys.data.createQuery('companies')
    .field('type').equals('a').or().field('type').equals('b').field('isCustomer').equals(true)
var records_10 = sys.data.find(query_10);
log('total: '+records_10.count());
while (records_10.hasNext()) {
    log(records_10.next().label());
}
// Query on companies 
GET /data/contacts?_query=or(filter('type','a'),and(filter('type','b'),filter('isCustomer','true')))

The query map version is almost the same as simple queries; it uses the _or and _and operators. However, in the REST API and query builder, due to interface limitations, complex queries must be written differently.

For the REST API, you must pass a _query parameter, where you can use these operators:

  • or(...): evaluates to true if any of the criteria passed as arguments is true. It accepts a list of filter criteria that can be and(), filter(), or another or().
  • and(...): similar to or(), but it evaluates to true if all the criteria passed as arguments are true.
  • filter(field,value): a simple field filter, like those in simple queries. Always enclose the name and value with single quotes. When using the query builder, you should use the query() method, which accepts a function as a parameter. This function receives a helper object and should return a query criteria.

Here are some examples of complex queries in the REST API and query builder:

// Query on companies 
var query_11 = sys.data.createQuery('companies')
    .and(q.field('isCustomer').equals(true), q.or(q.field('addresses.state').equals('CA'), q.field('numberOfEmployees').greater(100)))
var records_11 = sys.data.find(query_11);
log('total: '+records_11.count());
while (records_11.hasNext()) {
    log(records_11.next().label());
}
// Query on companies 
GET /data/contacts?_query=and(filter('isCustomer','true'),or(filter('addresses.state','CA'),filter('numberOfEmployees','greater(100)')))

// Query on companies 
var query_12 = sys.data.createQuery('companies')
    .or(q.field('isCustomer').equals(true), q.field('numberOfEmployees').greater(900), q.field('addresses.state').equals('CA,CO,NJ'))
var records_12 = sys.data.find(query_12);
log('total: '+records_12.count());
while (records_12.hasNext()) {
    log(records_12.next().label());
}
// Query on companies 
GET /data/contacts?_query=or(filter('isCustomer','true'),filter('numberOfEmployees','greater(900)'),filter('addresses.state','CA,CO,NJ'))

Query parameters

In addition to field filtering, you can specify query parameters to control how the query is executed.

Limit size

You can set a maximum number of records to fetch using the _size parameter or the size() method:

// fetches 10 records at most 
var records_13 = sys.data.find('companies', {_size: 10});
log('total: '+records_13.count());
while (records_13.hasNext()) {
    log(records_13.next().label());
}
// fetches 10 records at most 
var query_13 = sys.data.createQuery('companies')
    .size(10)
var records_13 = sys.data.find(query_13);
log('total: '+records_13.count());
while (records_13.hasNext()) {
    log(records_13.next().label());
}
// fetches 10 records at most 
GET /data/contacts?_size=10

It’s possible that the query returns fewer records than the specified number.

The REST and Javascript APIs will return the total number of records matched by the query. Please refer to the documentation for each API for more information.

For the REST API, the maximum value you can specify is 1,000, with a default value of 20 if not specified. These limits and defaults do not apply to the Javascript API, as it uses a cursor to iterate over the results.

Skip records

You can provide an offset to skip some records in your query. You can use a number to indicate how many records to skip, or you can use an ID.

If you use a number, that number will indicate how many records to skip. Use the _offset parameter or the offset() method:

// skips the first 5 records and the first record retrieved will be the 6th one 
var records_14 = sys.data.find('companies', {_offset: 5});
log('total: '+records_14.count());
while (records_14.hasNext()) {
    log(records_14.next().label());
}
// skips the first 5 records and the first record retrieved will be the 6th one 
var query_14 = sys.data.createQuery('companies')
    .offset(5)
var records_14 = sys.data.find(query_14);
log('total: '+records_14.count());
while (records_14.hasNext()) {
    log(records_14.next().label());
}
// skips the first 5 records and the first record retrieved will be the 6th one 
GET /data/contacts?_offset=5

If there are fewer records than the specified offset, the query will return no results.

The other option for specifying an offset is an ID. This form can only be used if you are either explicitly sorting by the id field or if you have omitted sorting options in your query (the default is id).

For instance, if you query 5 records and receive the following record IDs:

[
  '57fd2d65e4b0ce322b0c8665',
  '57fd2d65e4b0ce322b0c8565',
  '57fd2d65e4b0ce322b0c8547',
  '57fd2d65e4b0ce322b0c84b1',
  '57fd2d64e4b0ce322b0c8349'
]

To fetch the next 5 records, you should make the following query, using the last record’s ID as the offset:

// starts after record with ID 57fd2d64e4b0ce322b0c8349 
var records_15 = sys.data.find('companies', {_offset: '57fd2d64e4b0ce322b0c8349'});
log('total: '+records_15.count());
while (records_15.hasNext()) {
    log(records_15.next().label());
}
// starts after record with ID 57fd2d64e4b0ce322b0c8349 
var query_15 = sys.data.createQuery('companies')
    .offset('57fd2d64e4b0ce322b0c8349')
var records_15 = sys.data.find(query_15);
log('total: '+records_15.count());
while (records_15.hasNext()) {
    log(records_15.next().label());
}
// starts after record with ID 57fd2d64e4b0ce322b0c8349 
GET /data/contacts?_offset=57fd2d64e4b0ce322b0c8349

This query will not return the record with ID 57fd2d64e4b0ce322b0c8349 but rather the next 5 records after it.

If you need to iterate over all records using the REST API and want to avoid fetching the same record twice, it's advisable to use IDs as offsets. Using numbers might result in duplicated or missed records due to modifications of records affecting your query.

Sorting

You can sort results based on a field. By default, the id field and desc direction are used if no sorting options are specified. However, you can change this behavior using the _sortField and _sortType parameters or the sortBy() method:

//  
var records_16 = sys.data.find('companies', { state: 'active', _sortField: 'name', _sortType: 'asc' });
log('total: '+records_16.count());
while (records_16.hasNext()) {
    log(records_16.next().label());
}
//  
var query_16 = sys.data.createQuery('companies')
    .field('state').equals('active').sortBy('name', 'asc')
var records_16 = sys.data.find(query_16);
log('total: '+records_16.count());
while (records_16.hasNext()) {
    log(records_16.next().label());
}
//  
GET /data/contacts?state=active&_sortField=name&_sortType=asc

You can even use a nested field for sorting:

//  
var records_17 = sys.data.find('companies', { _sortField: 'contactInformation.email', _sortType: 'asc' });
log('total: '+records_17.count());
while (records_17.hasNext()) {
    log(records_17.next().label());
}
//  
var query_17 = sys.data.createQuery('companies')
    .sortBy('contactInformation.email', 'asc')
var records_17 = sys.data.find(query_17);
log('total: '+records_17.count());
while (records_17.hasNext()) {
    log(records_17.next().label());
}
//  
GET /data/contacts?_sortField=contactInformation.email&_sortType=asc

The _sortField parameter specifies the field to sort by, and _sortType specifies the sorting order, which can be one of the following:

  • asc (ascending)
  • desc (descending)

Keep in mind that you cannot use multi-valued or nested fields as the sorting field.

So far you cannot specify many fields for sorting. In some cases you can achieve the same result by adding a calculated field that concatenates the fields you want to use for sorting.

Include fields

By default, all fields are returned in the results. If you only want to retrieve specific fields, you can use the _fields parameter or the includeFields() method:

// only name and email fields will be fetched 
var records_18 = sys.data.find('companies', { _fields: 'name,email' });
log('total: '+records_18.count());
while (records_18.hasNext()) {
    log(records_18.next().label());
}
// only name and email fields will be fetched 
var query_18 = sys.data.createQuery('companies')
    .includeFields('name', 'email')
var records_18 = sys.data.find(query_18);
log('total: '+records_18.count());
while (records_18.hasNext()) {
    log(records_18.next().label());
}
// only name and email fields will be fetched 
GET /data/contacts?_fields=name,email

You can select nested fields like addresses.state. If you put the parent field, like addresses, all inner fields will be included.

// you can also indicate nested fields 
var records_19 = sys.data.find('companies', { _fields: 'name,addresses.state' });
log('total: '+records_19.count());
while (records_19.hasNext()) {
    log(records_19.next().label());
}
// you can also indicate nested fields 
var query_19 = sys.data.createQuery('companies')
    .includeFields('name', 'addresses.state')
var records_19 = sys.data.find(query_19);
log('total: '+records_19.count());
while (records_19.hasNext()) {
    log(records_19.next().label());
}
// you can also indicate nested fields 
GET /data/contacts?_fields=name,addresses.state

System field will always be returned (id, version, label).

This parameter only functions with the REST API; it will not have any effect when used with the Javascript API.

If you wish to reduce the number of requests to the server, you can retrieve a record and its related records by using the _relationshipsToFetch parameter. This parameter accepts a comma-separated list of relationship fields.

// Fetches the primary records along with company information 
GET /data/contacts?_relationshipsToFetch=company&_size=10

// You can specify multiple relationship fields separated by commas, even if the relationship field is multi-valued 
GET /data/contacts?_relationshipsToFetch=company,skills&_size=10

This is only allowed for entities with global search enabled.

When querying using the global search feature, it will attempt to match the provided string in any field within the entity, rather than targeting a specific field. For example:

// Finds records where any field contains 'lacinia' or 'erat' 
var records_22 = sys.data.find('companies', { _globalSearch: 'lacinia erat' });
log('total: '+records_22.count());
while (records_22.hasNext()) {
    log(records_22.next().label());
}
// Finds records where any field contains 'lacinia' or 'erat' 
var query_22 = sys.data.createQuery('companies')
    .globalSearch('lacinia erat')
var records_22 = sys.data.find(query_22);
log('total: '+records_22.count());
while (records_22.hasNext()) {
    log(records_22.next().label());
}
// Finds records where any field contains 'lacinia' or 'erat' 
GET /data/contacts?_globalSearch=lacinia erat

Note that to match the entire phrase, you should enclose it with double quotes:

// finds record where any field has the tense 'lacinia erat' 
var records_23 = sys.data.find('companies', { _globalSearch: '"lacinia erat"' });
log('total: '+records_23.count());
while (records_23.hasNext()) {
    log(records_23.next().label());
}
// finds record where any field has the tense 'lacinia erat' 
var query_23 = sys.data.createQuery('companies')
    .globalSearch('"lacinia erat"')
var records_23 = sys.data.find(query_23);
log('total: '+records_23.count());
while (records_23.hasNext()) {
    log(records_23.next().label());
}
// finds record where any field has the tense 'lacinia erat' 
GET /data/contacts?_globalSearch="lacinia erat"

Please be aware that all searches are case-insensitive.

Format

This parameter only works with the REST API; it will not have any effect when used with the Javascript API.

In the REST API, you can utilize the _format parameter to specify the format of the returned records. There are two options:

  • native: Returns the “raw” values for each field. For example, a date-time field will be returned as milliseconds. This is the default format.
  • plainText: Returns field values converted to strings using the display options of each field. For example, a date-time field will be returned as a string based on the field’s display options.

The plainText format is useful when you need to display information to users in an external application outside of the Slingr runtime, and you do not want to handle formatting.

// Companies query sample 
GET /data/contacts?_format=plainText&_size=10

Format using one specific user’s settings

This can be achieved using the _formatForUser parameter and is only meaningful when using the plainText option in _format.

You should provide either the user’s ID or email that will be used to format the record. For instance, if the user has configured a different time zone, date-time fields will use that setting to format dates. This is useful when you need to send emails to individuals residing in different time zones.

// Companies query sample 
GET /data/contacts?_format=plainText&_formatForUser=test1.docs@slingr.io&_size=10