Relator Properties

Cloud CMS provides support for properties that auto-manage their relationships between multiple nodes in the content graph. Whenever you intend to connect two nodes together, it is often convenient to model a property on one or both of the nodes involved in the relationship such that the properties maintain information locally on the endpoints about the relationship.

Such properties are known as "relator properties".

Consider a Store that sells Books. Books are written by Authors. We could model out a Book type as well as an Author type. And then we can use a relator property to link them together via an association.

The graph might look like this:

Book (store:book) -> Authored By (store:authored-by) -> Author (store:author)

Working backwards, here is what an Author might look like. An Author is a stand-alone entity:

{
    "title": "Author",
    "_qname": "store:author",
    "_type": "d:type",
    "_parent": "n:node",
    "type": "object",
    "properties": {
        "title": {
            "type": "string",
            "title": "Title"
        },
        "firstName": {
            "type": "string",
            "title": "First name"
        },
        "lastName": {
            "type": "string",
            "title": "Last Name"
        }
    }
}

The Authored By association definition might look like this:

{
    "title": "Authored By",
    "_qname": "store:authored-by",
    "_type": "d:association",
    "type": "object"
}

And finally, the Book definition may appear thusly:

{
    "title": "Book",
    "_qname": "store:book",
    "_type": "d:type",
    "type": "object",
    "properties": {
        "title": {
            "type": "string",
            "title": "Title"
        },
        "summary": {
            "type": "string",
            "title": "Summary"
        },
        "author": {
            "type": "object",
            "title": "Author",
            "_relator": {
                "associationType": "store:authored-by",
                "nodeType": "store:author"
            }
        },
        "recommendations": {
            "type": "array",
            "title": "Recommendations",
            "items": {
                "type": "object"
            },
            "_relator": {
                "associationType": "store:suggests",
                "nodeType": "store:book"
            }
        }
    }
}

The Book definition defines an author property of type object. It then informs Cloud CMS that this is a relator property and tells Cloud CMS about the relationship. In this case, it defines a relationship between this Book and an Author (store:author) via an association of type store:authored_by).

This underlying association is managed for you by Cloud CMS. When the author property is populated with relator JSON (using a picker from within Cloud CMS), the relator automatically creates or removes the underlying association that links this Book to its Author in the graph. The author property is of type object and so it describes a single, 1-1 relationship.

You can also model one-to-many (1-N) relationships using an array. The recommendations property above defines a one-to-many relationship between a Book and other Books. The underlying association is of type store:suggests, allowing you to have Books maintain relationships with other books that may be suggested to an end user. This is often useful for web sites or other promotional front-ends to which you wish to provision dynamically configurable content based on relationships.

Let's now imagine our Store. In it, we have Books and Authors. Here are the Authors:

[object Object]

In our Store, we have a Book for the title "Romeo and Juliet":

[object Object]

If we scroll down, we'll get to the relator properties section of the form. Cloud CMS renders Picker fields for us for each of our relator property fields:

[object Object]

Cloud CMS automatically interprets _relator fields and inserts Picker Fields into the form for you. You can either use the default Picker Fields (of field type node-picker) or you can extend and implement your own using Alpaca. Picker Fields render with a list of selected values and links. A Select button lets you pick from available nodes that match the constraint of the relator property:

[object Object]

When the node is saved, the underlying association graph is auto-maintained such that on each create, update or delete of relator-bearing content instances, the graph is updated and kept in sync. Here is what the graph might look like from within Cloud CMS:

[object Object]

And here is what the JSON for the Book would now look like:

{
    "title": "Romeo and Juliet",
    "summary": "Romeo and Juliet is a tragic play written early in the career of William Shakespeare about two teenage 'star-cross'd lovers' whose untimely deaths ultimately unite their feuding households. It was among Shakespeare's most popular plays during his lifetime and, along with Hamlet, is one of his most frequently performed plays. Today, the title characters are regarded as archetypal 'young lovers'.",
    "author": {
        "id": "5e56305e93ce2881ba86",
        "ref": "node://e36f6137c91eebe62a70/4448c692a87a2a3d9a3b/a54ca00a725e04f58ebc/5e56305e93ce2881ba86",
        "qname": "o:5e56305e93ce2881ba86",
        "typeQName": "store:author",
        "title": "William Shakespeare"
    },
    "recommendations": [
        {
            "id": "a2f9e132da518d6b62e3",
            "ref": "node://e36f6137c91eebe62a70/4448c692a87a2a3d9a3b/a54ca00a725e04f58ebc/a2f9e132da518d6b62e3",
            "qname": "o:a2f9e132da518d6b62e3",
            "typeQName": "store:book",
            "title": "Macbeth"
        },
        {
            "id": "83d9924f56df24023b47",
            "ref": "node://e36f6137c91eebe62a70/4448c692a87a2a3d9a3b/a54ca00a725e04f58ebc/83d9924f56df24023b47",
            "qname": "o:83d9924f56df24023b47",
            "typeQName": "store:book",
            "title": "Taming of the Shrew"
        }
    ]
}

This lets your business users work with easy-to-use Picker controls while ensuring that you content graph is leveraged and well-expressed for your runtime applications.

Relator Structure

Relator properties are modeled on top of any array or object properties or in your JSON object. Relator properties are declared by using the _relator attribute. For each relator property that you declare, there will be a single association maintained for you in the content graph that points from the node with the relator property to the related node.

This association will be endowed with the f:relator feature. The f:relator feature maintains to properties:

  • propertyHolder is either source or target and indicates which end of the association holds the relator property.
  • propertyPath is the path into the property holder of the relator property.

The related node will be endowed with the f:related feature. This is simply a tag to indicate that the node is associated with at least one relator property on a node elsewhere in the graph.

Empty Relators

At its simplest, a relator can be defined as empty like this:

"_relator": {}

An empty relator will let you pick from any kind of node in the branch. When you select a node to relate with, an a:linked association will be created with the relator property's node as the source and the related node as the target. The association will also be endowed with the f:relator feature as per above.

Unique Array Entries

If you are using array properties for a one-to-many relator setup, you might want to ensure that entries related items are unique for a given relator properties. To do this, add the uniqueEntries property to your config:

{
    "type": "array",
    "_relator": {
        "uniqueEntries": true
    }
}

Configure the Node Type

If you want to constrain the kind of node to relate with, you can do so using the nodeType setting, like this:

"_relator": {
    "nodeType": "{nodeTypeQName}"
}

The nodeType setting lets you constrain the selection list of target related nodes to a specific type.

Configure Association Type

You can also control the kind of association that gets maintained by using the associationTypeQName setting, like this:

"_relator": {
    "nodeType": "{nodeTypeQName}",
    "associationType": "{associationTypeQName}"        
}

By default, the association type is assumed to be a:linked. Associations will be maintained in the graph automatically for you. They will be endowed with the f:relator feature as per above.

Configure Association Direction

By default, associations are OUTGOING which means that the source of the association is the node with the relator property and the target is the related node. Relator properties can be configured to work with either OUTGOING or INCOMING directions.

Here is how you can configure for INCOMING:

"_relator": {
    "nodeType": "{nodeTypeQName}",
    "associationType": "{associationTypeQName}",
    "associationDirection": "INCOMING"
}

An incoming association is a directed association that originates from the related node and points back into the node with your relator property.

Property Mappings

A relator stores information about the relationship between a source node and a target node. The resulting relator object, by default, maps a minimal set of information in a relator block that looks like this:

{
    "id": "{_doc}",
    "ref": "{nodeReference}",
    "qname": "{nodeQName}",
    "typeQName": "{nodeTypeQName}",
    "title": "{nodeTitle}"
}

This is the default, minimal set of information. It provides enough so that the relator block is useful in terms of looking up the related node. When the related node changes (i.e. it is updated), the relator block is automatically kept in sync and these properties are re-mapped.

Mappings flow in the opposite direction of the association. It always flows in the direction that maps information onto the relator property.

  • If the association is OUTGOING (where source is the node with the relator property and target is the related node), then mapped data flows from the target to the source.

  • If the association is INCOMING (where target is the node with the relator property and source is the related node), then mapped data flows from the source to the target.

Custom Property Mappings

You may add your own property mappings to extend the set above by using a mappings block, like this:

"_relator": {
    "nodeType": "{nodeTypeQName}",
    "associationType": "{associationTypeQName}",        
    "mappings": [{
        "from": "target",
        "fromProperty": "{fromProperty}",
        "toProperty": "{toProperty}"
    }]
}

Zero or more mappings may be defined. Each mapping tells Cloud CMS how to copy values from one end of the association to the other. They are executed every time the related node is created, updated or deleted, allowing your relator property to keep in sync in real time.

The fromProperty and toProperty values support path-based identification:

  1. A simple string identifies a property relative to the root of the document.
  2. A path starting with / likewise identifies a property relative to the root of the document.
  3. A path starting with ./ is deemed relative to the current property.
  4. Paths may contain . and .. for relative referencing.

Paths are path-delimited. The following are examples of valid paths:

/order/customer/name
./name
../name

Mappings may optionally specify a from setting which indicates where on the association the fromProperty can be discovered (either source or target). If not specified, the from setting will be assumed to be target for OUTGOING associations and source for INCOMING associations.

The from setting is very powerful as it allows you to map properties from any location on one of end of the relator association to any location in the other end. They are very similar, in this respect, to the kind of full-mapping capabilities offered by the f:property-mapping feature.

Propagation and Circularity

Relator properties let you describe a relationship between Node A and Node B. The relationship may map properties from Node A to Node B or from Node B to Node A. If Node B has a relationship with Node C, it is possible for the mapping of properties from A to B to trigger another mapping from B to C. This is known as property propagation.

For example, consider a "loop" like this:

Node A -> Node B -> Node C

Where the property "id" is mapped from Node A to Node B by a relator property. And it is also mapped from Node B to Node C by a relator property. And it is also mapped from Node C to Node A by a relator property.

Suppose you update the value of id to abc on Node A. When the operation completes, you will find that Node A, Node B and Node C all have the value of abc for id. It propagates around the chain until things settle.

The mapping engine spots circular loops and will prevent mappings from executing twice. When all f:realtor associations have been mapped across, the property mapping is deemed complete.

Relator Mapping Example #1

As per our example above, suppose we have a Book and an Author. The book has an author property which is a relator property pointing to an Author. If we soup up the author property definition, we can have it copy the author node's first name and last name into the Book as root-scoped properties.

We adjust the Book's author property schema to look like this:

"author": {
    "type": "object",
    "title": "Author",
    "_relator": {
        "associationType": "store:authored-by",
        "nodeType": "store:author",
        "mappings": [{
            "fromProperty": "firstName",
            "toProperty": "authorFirstName"
        }, {
           "fromProperty": "lastName",
           "toProperty": "authorLastName"
       }]
    }
}

With this in place, our Book would look something like this:

{
    "title": "Romeo and Juliet",
    "summary": "Romeo and Juliet is a tragic play written early in the career of William Shakespeare about two teenage 'star-cross'd lovers' whose untimely deaths ultimately unite their feuding households. It was among Shakespeare's most popular plays during his lifetime and, along with Hamlet, is one of his most frequently performed plays. Today, the title characters are regarded as archetypal 'young lovers'.",
    "authorFirstName": "William",
    "authorLastName": "Shakespeare",
    "author": {
        "id": "5e56305e93ce2881ba86",
        "ref": "node://e36f6137c91eebe62a70/4448c692a87a2a3d9a3b/a54ca00a725e04f58ebc/5e56305e93ce2881ba86",
        "qname": "o:5e56305e93ce2881ba86",
        "typeQName": "store:author",
        "title": "William Shakespeare"
    }
}

Relator Mapping Example #2

Let's suppose now that we want to do the same thing except we'd like the properties written into our node to be stored within the relator property. Recall that the relator property automatically maps id, ref, qname, typeQName and title. Let's have it also map firstName and lastName alongside those.

To do so, use the ./ prefix for relative paths. This tells the property mapping engine to store the resulting key/value relative to the current property (the relator property).

We adjust the Book's author property schema to look like this:

"author": {
    "type": "object",
    "title": "Author",
    "_relator": {
        "associationType": "store:authored-by",
        "nodeType": "store:author",
        "mappings": [{
            "from": "target",
            "fromProperty": "firstName",
            "toProperty": "./firstName"
        }, {
           "from": "target",
           "fromProperty": "lastName",
           "toProperty": "./lastName"
       }]
    }
}

With this in place, our Book would look something like this:

{
    "title": "Romeo and Juliet",
    "summary": "Romeo and Juliet is a tragic play written early in the career of William Shakespeare about two teenage 'star-cross'd lovers' whose untimely deaths ultimately unite their feuding households. It was among Shakespeare's most popular plays during his lifetime and, along with Hamlet, is one of his most frequently performed plays. Today, the title characters are regarded as archetypal 'young lovers'.",
    "author": {
        "id": "5e56305e93ce2881ba86",
        "ref": "node://e36f6137c91eebe62a70/4448c692a87a2a3d9a3b/a54ca00a725e04f58ebc/5e56305e93ce2881ba86",
        "qname": "o:5e56305e93ce2881ba86",
        "typeQName": "store:author",
        "title": "William Shakespeare",
        "firstName": "William",
        "lastName": "Shakespeare"            
    }
}

Relator Mapping Example #3

You can use the . and .. path elements to construct relative paths. Paths are computed relative to the current property (the relator property).

Here is an example where we use relative paths to store the properties within a subobject off the root.

We adjust the Book's author property schema to look like this:

"author": {
    "type": "object",
    "title": "Author",
    "_relator": {
        "associationType": "store:authored-by",
        "nodeType": "store:author",
        "mappings": [{
            "fromProperty": "firstName",
            "toProperty": "../authorInfo/firstName"
        }, {
           "fromProperty": "lastName",
           "toProperty": "../authorInfo/lastName"
       }]
    }
}

With this in place, our Book would look something like this:

{
    "title": "Romeo and Juliet",
    "summary": "Romeo and Juliet is a tragic play written early in the career of William Shakespeare about two teenage 'star-cross'd lovers' whose untimely deaths ultimately unite their feuding households. It was among Shakespeare's most popular plays during his lifetime and, along with Hamlet, is one of his most frequently performed plays. Today, the title characters are regarded as archetypal 'young lovers'.",
    "authorInfo": {        
        "firstName": "William",
        "lastName": "Shakespeare",
    },
    "author": {
        "id": "5e56305e93ce2881ba86",
        "ref": "node://e36f6137c91eebe62a70/4448c692a87a2a3d9a3b/a54ca00a725e04f58ebc/5e56305e93ce2881ba86",
        "qname": "o:5e56305e93ce2881ba86",
        "typeQName": "store:author",
        "title": "William Shakespeare"       
    }
}