Filterable properties

A filterable property is a property that can be used to narrow down the results returned from a request to retrieve a resource collection. Many properties are filterable in the base configuration, but there might come a time when you need to add a custom filter, either to extend the base configuration or when adding a custom entity. There are two ways to add a custom filter, depending on the type of property.

  • Schema files. For properties directly on a data model entity, you can make the property filterable by updating the appropriate schema file.

  • Resource files. Properties with values that are either generated at run-time or derived from multiple data model entities can be made filterable by updating the resource file of the collection entity on which you’re filtering. (Note that this applies only when you are filtering a query-backed collection.)

Keep in mind that if a base configuration property is filterable, you cannot make it unfilterable.

Performance considerations

Before getting into the details of how to add filters, it’s a good idea to think about the size of the collection you’re filtering on. Filtering on very large collections can come with performance costs.When you update the schema to specify a property as filterable, the filter is automatically assigned all possible filter operators, such as equals (eq), starts with (sw), greater than (gt), and so on. Using some of these filters on large collections can sometimes be problematic because they can create performance issues.

To avoid this problem, if you’re adding a filter to a query-backed collection that has or is expected to have a large amount of data, it might be better to create the filter using resource files rather than the schema file, regardless of what type of property it is. This will enable you to specify which operators are allowed on the property when you filter.

Adding filterable properties through the resource file also enables you to specify whether the filter is case sensitive for a given operator. Making a filter case sensitive is another way to potentially improve performance when filtering large collections.

Query-backed collections vs stream-backed collections

The way in which you make a property filterable depends not only on the type of property, but also on the type of the collection. Collections can be query-backed or stream-backed.

  • Query-backed collections extend RestQueryCollectionResource.

  • Stream-backed collections extend RestStreamCollectionResource.

  • Resources that have both stream-backed and query-backed collections extend RestDualCollectionResource.

For all collection types, use the schema file to make a property filterable when the property exists directly on the data model entity.

For query-backed collections, use resource files to filter on a property that is derived at run time, derived from a join of multiple data model entities, or when you want to limit the filter operators.

For stream-backed collections, always use the schema files. If you want to sort based on a property that’s accessed through a join of data model entities, create an enhancement property with the join, then add that property to the schema file (as well as the mapping file).

Note: See Gosu Reference Guide for more information on enhancements. See Cloud API Developer Guide for information on adding properties to mapping files.

For dual collections, perform the required actions for both query-backed and stream-backed. For example, if you create a filter based on a property that requires a join between multiple data model entities, you need to update the resource file (for query-backed) and create an enhancement and update the schema file (for stream-backed).

Adding filters through the schema file

Properties that are directly on a data model entity (or created through an enhancement for stream-backed collections) can easily be made filterable by updating the schema file associated with the API to which the entity belongs. For example, to make a property on the User data model entity filterable, you update the admin_ext-1.0.schema.json file.

Suppose you’ve created a CustomEntity_Ext entity with an ExpirationDate field, and you want to filter on that field. Within the schema file, you need to set the entity’s filterable property within the x-gw-extensions object to true:

"definitions": {
  "CustomEntityExt": {
    ...
      "properties": {
	    ...
	    "expirationDate": {
	      "x-gw-extensions": {
	        "filterable": true
	      }
	    }

Now when retrieving a collection of CustomEntity_Ext instances, the caller application can opt to filter the collection based on the expiration date using a call such as this:

GET /common/v1/customentity-exts?filter=expirationDate:gt:2020-05-11T07::00::00.000Z

Adding filters through resource files

Some properties cannot be made filterable by simply updating the schema file, either because their values are calculated at run-time and not stored in the database, or their values are the result of a join on multiple datasets. In these cases, updating the schema file won’t make the property filterable. Instead, you need to create a new filter class and instantiate it from the resource file associated with the collection on which you want to filter. (This applies only to query-backed collections.)

As an example, suppose you want to filter users based on their employee number. You would need to use the /admin/v1/users endpoint to retrieve the collection of users, then apply a filter to that collection. However, although the Users collection returns an employeeNumber (in the default configuration), that number does not exist on the User data model entity itself. Instead, EmployeeNumber is being pulled from the UserContact data model entity, which User references through a foreign key to the UserContact#ID. So in order to create a filter, you need to do the same thing: query for EmployeeNumber in UserContact by matching the User#Contact field to the UserContact#ID field.

Here’s an example of how to do that.

package gw.rest.ext.cc.admin.v1.user

uses gw.api.database.IQueryBuilder
uses gw.api.database.InOperation
uses gw.api.database.Queries
uses gw.api.database.Relop
uses gw.api.filters.IQueryFilter
uses gw.api.filters.StandardQueryFilter
uses gw.api.modules.rest.framework.v1.query.filters.QueryFilterOp
uses gw.api.modules.rest.framework.v1.query.filters.RestQueryFilter

class EmployeeNumberFilter_Ext extends RestQueryFilter {

   override function createQueryFilter(propertyName : String, queryFilterOp : QueryFilterOp, parseValue : Object) : IQueryFilter {

 return new StandardQueryFilter("employeeNumber_Ext", \q -> {
   var empQuery = Queries.createQuery(entity.UserContact).compare(UserContact#EmployeeNumber, Relop.Equals, parseValue)
   q.subselect(User#Contact, InOperation.CompareIn, empQuery, UserContact#ID)
 })
}

   override property get AllowedOperators() : Set<QueryFilterOp> {

     return {
         QueryFilterOp.eq

     }
   }

   override function parseValue(op : QueryFilterOp, employeeNumber : String) : Object {
     if (employeeNumber == "null" && op.getAllowNulls()) {
       return null;
     }

     return employeeNumber
   }

}

Walking through the example a step at a time, here’s how it works.

The first thing you need to do is create a new class that extends RestQueryFilter.

class EmployeeNumberFilter_Ext extends RestQueryFilter {

When you create the class, you need to decide in which package to store it. In this example, it’s stored in the same place as the resource file that’s being extended:

package gw.rest.ext.cc.admin.v1.user

Next you need to add references to the objects you’re going to use:

uses gw.api.database.IQueryBuilder
uses gw.api.database.InOperation
uses gw.api.database.Queries
uses gw.api.database.Relop
uses gw.api.filters.IQueryFilter
uses gw.api.filters.StandardQueryFilter
uses gw.api.modules.rest.framework.v1.query.filters.QueryFilterOp
uses gw.api.modules.rest.framework.v1.query.filters.RestQueryFilter

Within this new class, you override the createQueryFilter function, which is where you’ll define the query that retrieves the employee number.

override function createQueryFilter(propertyName : String, queryFilterOp : QueryFilterOp, parseValue : Object) : IQueryFilter {

In this example the query is created as a StandardQueryFilter. In creating this filter you first give it a name (in this case exmployeeNumber_Ext) and then a query. This query retrieves the UserContact information for each User, matching the Contact property foreign key to the ID, then compares the UserContact EmployeeNumber to the filter value (parseValue).

return new StandardQueryFilter("employeeNumber_Ext", \q -> {
   var empQuery = Queries.createQuery(entity.UserContact).compare(UserContact#EmployeeNumber, Relop.Equals, parseValue)
   q.subselect(User#Contact, InOperation.CompareIn, empQuery, UserContact#ID)
 })

Notice in the query that UserContact#EmployeeNumber is being compared to the filter value with Relop.Equals. This means that any filter applied will look only for employee numbers that exactly match the filter value. To enforce this restriction, you must also include getAllowedOperators and specify the equality operator:

getAllowedOperators and specify the equality operator:

   override property get AllowedOperators() : Set<QueryFilterOp> {

     return {
         QueryFilterOp.eq
     }
   }

By specifying only the eq operator as an allowed operator, any other operator used with this filter will produce an error.

See “Add filter operators” below for an example of how to allow multiple operators.

Next, you need to include the parseValue function. This function ensures that the filter value entered into the query string is converted to the proper datatype. (For example, a value of 2023-06-13T20:07:44.243Z would need to be parsed from a string to a Date object in order to filter on a Date field.) In this example, there is also a check for null to ensure the string "null" is parsed to an actual null (or empty) value rather than interpreted as the literal string "null".

override function parseValue(op : QueryFilterOp, employeeNumber : String) : Object {
  if (employeeNumber == "null" && op.getAllowNulls()) {
     return null;
  }

  return employeeNumber
}

The final step is to update the resource file associated with the collection. For this example you’ll update the UsersExtResource.gs file.

In the resource file, override getCustomFilters to enable the new filter:

override property get CustomFilters() : Map<String, RestQueryFilter> {

  var customFilters = new HashMap<String, RestQueryFilter>(super.getCustomFilters())
  customFilters.put("employeeNumber_Ext", new EmployeeNumberFilter_Ext())
  return customFilters;
}

Be sure to call super first, to ensure you don’t overwrite any existing filters that could already be in place. Then add your new filter, giving it a name (employeeNumber_Ext) and an instance of the new class you just made.

You can now filter on a specific employee number within the Users collection, like this:

GET /admin/v1/users?filter=employeeNumber_Ext:eq:1001

Add filter operators

The preceding example created a filter that allows you to filter on an exact match of an employee number. But in some cases, you might not want an exact match, you might want to find all values that are greater than or less than a certain value, or that contain or start with a string of characters. The following example demonstrates how to allow multiple operators with your filter.

Note: If the collection you’re applying the filter to is or could potentially become very large, adding certain operators could cause performance issues. For large collections, consider limiting your available operators.

This example modifies the previous example by adding the “starts with” (sw) operator to the allowed operators when filtering on an employee number.

class EmployeeNumberFilter_Ext extends RestQueryFilter {

   override function createQueryFilter(propertyName : String, queryFilterOp : QueryFilterOp, parseValue : Object) : IQueryFilter {

     var eFilter : StandardQueryFilter

     switch (queryFilterOp) {
       case QueryFilterOp.eq:
         eFilter = new StandardQueryFilter("employeeNumber_Ext", \q -> {
           var empQuery = Queries.createQuery(entity.UserContact).compare(UserContact#EmployeeNumber, Relop.Equals, parseValue)
           q.subselect(User#Contact, InOperation.CompareIn, empQuery, UserContact#ID)
         })
         break
       case QueryFilterOp.sw:
         eFilter = new StandardQueryFilter("employeeNumber_Ext", \q -> {
           var empQuery = Queries.createQuery(entity.UserContact).startsWith(UserContact#EmployeeNumber, String.valueOf(parseValue), true)
           q.subselect(User#Contact, InOperation.CompareIn, empQuery, UserContact#ID)
         })
         break

     }
     return eFilter
   }

   override property get AllowedOperators() : Set<QueryFilterOp> {
     return {
         QueryFilterOp.sw,
         QueryFilterOp.eq
     }
   }

   override function parseValue(op : QueryFilterOp, employeeNumber : String) : Object {
     if (employeeNumber == "null" && op.getAllowNulls()) {
       return null;
     }

     return employeeNumber
   }

}

When you allow multiple operators on a filter, you need to create a separate query for each filter. This example uses a switch statement to return the query that matches the operator that is being used.

switch (queryFilterOp) {
Note: For a list of valid operators, see Cloud API Consumer Guide

The first case applies to the eq operator, and is identical to the employee number query shown previously.

case QueryFilterOp.eq:
  eFilter = new StandardQueryFilter("employeeNumber_Ext", \q -> {
    var empQuery = Queries.createQuery(entity.UserContact).compare(UserContact#EmployeeNumber, Relop.Equals, parseValue)
    q.subselect(User#Contact, InOperation.CompareIn, empQuery, UserContact#ID)
  })
  break

The second case applies to the sw operator. This query is similar to the equality case, but instead of performing a query using Relop.Equals, you need to use the startsWith method on the query.

case QueryFilterOp.sw:
  eFilter = new StandardQueryFilter("employeeNumber_Ext", \q -> {
    var empQuery = Queries.createQuery(entity.UserContact).startsWith(UserContact#EmployeeNumber, String.valueOf(parseValue), true)
    q.subselect(User#Contact, InOperation.CompareIn, empQuery, UserContact#ID)
  })
  break

Note that the last parameter in the call to startsWith is set to true. This parameter specifies whether the filter is case insensitive. Set this value to false to make the filter case sensitive.

In addition to adding a new query in the createQueryFilter function, you also need to add the additional operator to the list of AllowedOperators:

override property get AllowedOperators() : Set<QueryFilterOp> {
  return {
     QueryFilterOp.eq,
     QueryFilterOp.sw
  }
}

The rest of the code is created in the same way as in the earlier example.

Now you can use endpoints like either of the following to retrieve users based on their employee number:

GET /admin/v1/users?fields=lastName,employeeNumber&filter=employeeNumber_Ext:eq:US-10011

GET /admin/v1/users?fields=lastName,employeeNumber&filter=employeeNumber_Ext:sw:US