Expression API

Learn about hydration and filtration on Expression's flexible API.

Overview

The XPR API is based on REST standards, and comprises numerous Resources available for use in Datasources, Serverside Scripts as well as over HTTP when logged in as an admin. With a few exceptions, all resources are CRUD resources and support querying and manipulation via equivalent interfaces. Resource representation is based on the HAL (Hypertext Application Language) specification. This surfaces primarily when dealing with related objects.

For example, a User will always have a related Person record. The JSON for a specific User will look like this:

{  
    ... User Properties ...  

    _embedded: {
        Person : {  

            ... Person Properties ...  

            }
    }
}  

Non Hydrated objects will return with only an Id property. It is inefficient to pull every related object for every API request, so the API exposes control over which embedded objects are retrieved with Hydration Control.

Retrieving Single Objects


Every CRUD object has a primary Id property. To retrieve an object, make a GET request against a uri consisting of the resource namespace, followed by a slash and then the Id.

Example:/articles/1

Retrieving Collections of Objects


Collections of Objects can be retrieved via the / endpoint, i.e. /articles/.

Pagination

The default page size is 25 items. If you need more, or less, you can adjust the page size by specifying a __per_page__ query parameter.

You can also target a specific page by specifying a page parameter.

Via HTTP, the total size (ignoring pagination) is included as a top level property for HAL resources of type "Collection". In Datasources, this, along with other HAL metadata, is not included to simplify enumerating collections.

In serverside scripts, you can choose to bypass this simplification (called embed unrolling) via issuing a query from the with the parseHAL option, e.g.

api.call({
    method: "GET",
    uri: "/articles/",
    parseHAL: false
});  

Ordering

Responses can be ordered by any of the first class fields for the API object represented by the collection uri. For example, a request to /articles/ can be ordered by any field on the Article object.

This is done by passing in an order_fields parameter which is a common delimited list of fields. Direction can be controlled by supplying an order_dirs parameter.

e.g.

api.call({
    method: "GET",
    uri: "/articles/",
    params: {
        order_fields: "Title, Slug",
        order_dirs: "ASC,DESC",
    }
}); 

Child objects, for example Article Categories

Data Mining

Each core field on a given API can be filtered against to reduce the collection size and target specific objects in a set. This makes it possible to grab all the users in a certain group, or all articles pertaining to a specific section.

Filtering is accomplished by providing the field name, postfixed with a double underscore "__", followed by the filter type, setting the value of the query parameter to the value desired.

Using a field named Title as an example, you could filter for articles matching an exact string by using Title__eq.

Filter Description
__eq Equal To
__ne, __neq Not Equal To
__gt Greater Than
__gte Greater Than or Equal to
__lt Less Than
__lte Less Than or Equal to
__like Like, for strings can use % for wildcards e.g. __like="Dav%" to match 'Davis', 'David'
__notlike Not like
__in In list, e.g. in=1,2,3 or in=Rhonda,Carlos
__notin,__nin Not in list

Filtering by a Related Object's Property

If you need to filter a collection by a property on a related object, the same filter syntax applies but is prefixed by related_{RelationName}. e.g. /users/?related_UserGroup_Name__eq=Site Developer

Related filters can be chained thru multiple relationships, e.g. /articles/?related_Section_Category_Name__eq=Winter

Searching Collections

Most resources expose some or all of their data as a searchable property. Any collection endpoint, e.g. /events/ can search all of its searchable fields by supplying a q parameter, i.e.

api.call({
    method: "GET",
    uri: "/events/",
    params: {
        q: "Vancouver"
    }
});   

Searching Collections by a Specific Field

You can also search a collection endpoint by specific fields, instead of the aggregate of searchable fields, i.e.

api.call({
    method: "GET",
    uri: "/events/",
    params: {
        q_Location: "Vancouver",  
        q_Html: "banner-div"
    }
});  

This results in a search which matches Location and Html based on the query values.

You can also perform or style filtering by the same value like so:

api.call({
    method: "GET",
    uri: "/articles/",
    params: {
        q_Title_Description: "News",
    }
});  

This query would return items in which either Title or Description contain the string "News".

A handful of resources which are notated as Object Searchable in the API docs can do more advanced searching with the ObjectSearch API.

Hydration Control

Objects often have a multi-level relational hiearchy, and it is desirable to control what portions of the hiearchy are populated.

For example: A request to /users/{id}, if the user indicated by {id} has Address objects attached, they will be in the _embedded.Addresses objects, but the response will only include the Id of the related object. At the next level, Addresses have an _embedded.Region and _embedded.Country

Now, it is possible to hydrate this data manually, by performing multiple requests using the Ids in the response; but it is preferable to populate the relationships you need on request.

This is accomplished via the with query param. E.g.:

var hydratedUser = api.call({  
    method: "GET",
    uri: "/users/1",
    params: {
        with: "Addresses(Region,Country)"
    }
});

The relations can be nested to the required depth. For example, to hydrate multiple levels of a section hiearchy:

var hydratedUser = api.call({  
    method: "GET",
    uri: "/sections/1",
    params: {
        with: "Children(Children(Children))"
    }
});

Ordering Related Objects

_embedded object collections can also be sorted similar to how primary collections are sorted with the order_fields and order_dirs parameters.

This is done by specifying an order_{ParentObject}_{ChildObject}_fields and associated dirs list.

var sections = api.call({
    method: "GET",
    uri: "/sections/",
    params: {
        with: "Children",
        order_Section_Children_fields: "Title",
        order_Section_Children_dirs: "ASC"
    }
});

//sections._embedded.Children will be sorted by Title

Creating and Modifying Objects

Fields on objects can be modified with PATCH requests:

var patchedUser = api.call({  
    method: "PATCH",
    uri: "/users/1",
    params: {},
    data: {
        "FirstName" : "Oscar"
    }
});  

New objects can be created by issuing a POST to the collection uri:

var newUser = api.call({  
    method: "POST",
    uri: "/users/",
    params: {},
    data: {
        "Username"  : "oscarfourpaws",
        "Password"  : "123456",
        "FirstName" : "Oscar",
        "LastName"  : "Fourpaws",
        "Active"    : true,
        "_embedded" : {
            "UserGroup" : {
                "_linkFirst" : {
                    "Name__like" : "Site Developer"
                }
            }
        }
    }
});

newUser.Id!==null; //true

_linkFirst

In the above example, posting an _embedded.UserGroup.Id : 1 value would create the link between the newly created User and and UserGroup 1. Specifying _linkFirst lets you, instead of specifying an Id, specify an query using the same data mining syntax in use for collection filtering, and will link the first result. If no result is found, no link will be made.

_linkRange

For 1:M or M:M relationships, _linkRange operates like _linkFirst but will attach all objects matching the query specified in _linkRange.

Relation Deserialization


1:1, 1:M, and M:M relationships are similiarily deserialized. We've already seen _linkFirst, but typically you will be attaching objects by their Ids.

1:1

{
    _embedded : {
        UserGroup: {
            Id: 1
        }
    }
}

1:M, M:M

{
    _embedded: {
        AuxiliaryGroups: [
            { Id: 2 },
            { Id: 3 },
            { Id: 4 }
        ]
    }
}

One caveat: A given POST or PATCH will only link the first level of relationships, for example:

var newUser = api.call({
    method: "PATCH",
    uri: "/users/1",
    data: {
       _embedded: {
            Address: {
                Id: 1,
                _embedded: {
                    Region: {
                        Id: 1  //ignored
                    }
                }
            }
        }
    }
});

This will have the effect of linking Address 1 to User 1, but the next level (_embedded.Region) will be ignored. To set the Region requires a secondary call to the Addresses API.

Extending Objects with Custom Fields


Most core objects can be extended with Custom Fields.

Custom Fields add additional properties attached to all objects of a given set of types. These custom properties are populated on GET via with, i.e. ?with=CustomFields.

For example, defining a Custom Field of type 'Text' named 'Foo' on both Users and Articles means that when retrieving objects in /articles/ or /users/ with ?with=CustomFields you will see the following pattern in the responses:

{  
    Id: 1,  
    /* ... */
    _embedded : {
        CustomFields: {
            Foo: ""
        }
    }  
}

Being able to define a Custom Field which applies to multiple types, (e.g. User and Articles) allows for powerful cross-referencing.

Custom Fields are typically created via the XPR Backend. They can also be created programatically via the API if needed for a data migration or similar type of script.

Defining Custom Fields has implications beyond raw API data. The XPR Backend will render appropriate widgets for managing the data attached based on its type.

Custom Fields have the following types available for core object extension:

Type Description Value Interface
Text A simple text value CustomFields.{{Name}}
TextArea Like Text but with a larger input widget CustomFields.{{Name}}
RTE Rich text editable via WYSIWIG CustomFields.{{Name}}
Integer A simple Integer CustomFields.{{Name}}
Date Date in YYYY-MM-DD Format CustomFields.{{Name}}
List A list of options defined on the Custom Field of which a single item can be selected CustomFields._embedded.{{Name}} -> { Id:"", Value:""}
Multiple List A list of options defined on the Custom Field of which multiple items can be selected CustomFields._embedded.{{Name}} -> [ { Id: "", Value:"" } .. ]
Image Link to item from the Media Explorer CustomFields._embedded.{{Name}} -> API File Object
M:M Foreign Key A collection of foreign entities, e.g. mapping an object type to a list of items of a different object type CustomFields._embedded.{{Name}} -> [ {Id: "" ... }, ]

Filtering based on Custom Fields

Filtering on Custom Fields works the same as regular API relation filtering, e.g.

related_CustomField_{{FieldName}}__[eq,neq,in...etc]

ObjectSearch API


For APIs which support Object Searching ( Article, Section, Product ), the objects can be searched and combined into the same collection through the ObjectSearch api.

Example :

var combinedSearchResults = api.call({
    "uri" : "/objectSearch/",
    "params" : {
        "searchResources" : "Section,Article",
        "q" : "Umbrellas",
        "searchOrderDir" : "DESC",  
    }
});

This API query will return Section and Article objects together, as a collection of formatted search results

The objects to search can be constrained further by passing query params to the underlying Section or Article objects, by prefixing them. For example, to constrain the searched Article objects to those inside of Section Id:1, use the query param: Article_related_Section_Id__eq:1.

Search results are scored based on a normalized value ranging from 0 to 1. It is possible to equalize the disparate object types by supplying bias and gain options, according to the formula: usedScore = bias + (normalizedScore * gain). This is done by passing the params {ResourceName}_searchBias and {ResourceName}_searchGain, e.g. Article_searchBias.

Typically, you will want results ordered by the search score. Alternatively, they can be ordered with the searchOrderFieldparam, which can be either Title or Date.

API Exploring / Autodocumentation

The API can be explored by navigating to /api/docs/ when logged into the XPR Backend. Each resource is explorable in detail including any additional end points not covered by the generic CRUD routes.