Skip to main content
Skip table of contents

HCMS Schema - Asset Relations

Explains the use of asset relations in Headless CMS schemas.

Asset relations can be mapped in the schema by specifying the id of the relation type and the direction (child or parent) using the properties "cs:relation.key" and "cs:relation.direction".

There are several possible ways to map relations to a JSON document that can be specified in the schema by setting the property "cs:relation.$ref_type". If this property is not specified the following defaults are applied based on the JSON value type: asset_id for integer, link for string and inline for object. In addition to inline all formats supported for asset reference features are also available.

Relations often allow higher cardinality than one and must in this case be mapped as an array instead as an single value. If the non-array representation is chosen for a relation with multiple values the result is undefined. The order of the elements in the array is not persisted unless the flag "cs:relation.$sorting": true is set for the mapping. Then the element order is retained using the sorting attribute of the relation.

Identifier based Mapping

Non-inline mappings are very similar to asset reference features. The value in entity JSON document is derived from the asset unique identifier (id, id_extern or asset resource key feature).

Mapping as Asset id

The most straightforward way of mapping is the direct mapping of the asset id, which is the default for type "integer".

JSON
{
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "type": "integer",
        "cs:relation.key": "user.",
        "cs:relation.direction": "child"
      }
    }
  }
}

Example entity:

JSON
{
  "members": [
    14910,
    14820,
    14850
  ]
}

Mapping as id_extern

Note that the common prefix corpus: is removed during mapping using "cs:relation.$ref_prefix": "corpus:"

JSON
{
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "type": "string",
        "cs:relation.key": "user.",
        "cs:relation.direction": "child",
        "cs:relation.$ref_type": "id_extern",
        "cs:relation.$ref_prefix": "corpus:"
      }
    }
  }
}

Example entity:

JSON
{
  "members": [
    "14910",
    "14820",
    "14850"
  ]
}

Mapping as Asset Resource Key

Example schema:

JSON
{
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "type": "string",
        "cs:relation.key": "user.",
        "cs:relation.direction": "child",
        "cs:relation.$ref_type": "asset_key"
      }
    }
  }
}

Example entity:

JSON
{
  "members": [
    "censhare:person.peter",
    "censhare:person.jim",
    "censhare:person.john"
  ]
}

Asset relations can also be represented as URLs referencing the target assets as entities in the Headless CMS REST API by setting the $ref_type to link.

JSON
{
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "type": "string",
        "cs:relation.key": "user.",
        "cs:relation.direction": "child",
        "cs:relation.$ref_type": "link"
      }
    }
  }
}

Example entity:

JSON
{
  "members": [
    "http://localhost:8080/hcms/v2.1/entity/person/14910",
    "http://localhost:8080/hcms/v2.1/entity/person/14820",
    "http://localhost:8080/hcms/v2.1/entity/person/14850"
  ]
}

Note that the linked entity might be present in several different schemas at once and only one of them can be chosen. This is always done by checking asset type, with following rules:

  1. Schemas specified by cs:relation.$ref_schema property are checked first, in given order. First one that exists and is applicable for the target asset type is chosen.
  2. All known schemas are processed in order specified by their cs:$priority value (ascending, default is 0). First one that is applicable for the target asset type is chosen.
    • If several schemas with the same priority match, it is not specified which one is actually used.

Master File URL

Besides the $ref_type link, there is also content_link, which provides a read only URL to the first master storage items binary data of the related asset. The value is considered missing if the target asset has no storage item.

Schema resolution algorithm is exactly the same as in the case of link, see above.

JSON
{
    "type": "object",
    "properties": {
        "media": {
          "type": "array",
          "items": {
            "type": "string",
            "cs:relation.key": "user.media.",
            "cs:relation.direction": "child",
            "cs:relation.$ref_type": "content_link"
          }
        }
    }
}

Example entity:

JSON
{
  "media": [
    "http://localhost:8080/hcms/v2.1/entity/image/storage/MDA2ODgyNS8xOC9wcmV2aWV3",
    "http://localhost:8080/hcms/v2.1/entity/video/storage/Njg4MjUvMjcvdGh1bWJuYWls"
  ]
}

Mapping to object with nested mappings

All scalar mappings can be also declared as JSON object, with value in one sub-property. Name of this sub-property must be defined by`"cs:relation.$value_property".

This mapping allows further nested sub-properties to define asset relation feature mapping of the relation.

Example mapping:

JSON
{
    "pictures": {
        "type": "array",
        "items": {
            "type": "object",
            "cs:relation.$ref_type": "asset_id",
            "cs:relation.key": "user.main-picture.",
            "cs:relation.direction": "child",
            "cs:relation.$value_property": "id",
            "properties": {
                "crop": {
                    "type": "string",
                    "cs:feature.key": "censhare:image-crop.key"
                },
                "id": {
                    "type": "integer"
                }
            }
        }
    }
}

Example entity:

JSON
{
    "pictures": [
        {"id": 12345, "crop": "3-4"},
        {"id": 12345, "crop": "16-9"},
        {"id": 33423}
    ]
}

Inline Mapping

This mapping type allows to hide the general graph nature of the database and merge many assets into a single document, which can be utilized to reduce the number of REST calls to fetch a data structure.
When exporting the entity, the target is completely serialized as a JSON object. When importing, the target asset is overwritten (or even created, if needed) by the data from the JSON object.

The inlined mapping always require "cs:asset.type" directive, but its value is used only to create new assets and not for filtering, as might be expected. The target asset is always serialized regardless of its asset type. Filtering can be added by special directive "cs:relation.$filter.asset_type" (see later).

Warning: Using the inline mapping for writing (PUT or POST operations on the entities) is quite dangerous. In most cases, special directives "cs:relation.$selection_key" and "cs:relation.$inline_import" should be used (both of them). See the next sections for detailed description of the writing semantics and how to configure it.

Embedded Schema

Schema with person objects inlined:

JSON
{
  "title": "Guest list",
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "cs:relation.direction": "child",
        "cs:relation.key": "user.",
        "cs:relation.$selection_key": [
            "id"         
        ],
        "cs:asset.type": "person.",
        "type": "object",
        "properties": {
          "name": {
            "cs:feature.key": "censhare:asset.name",
            "type": "string"
          },
          "id": {
            "minValue": 0,
            "maxValue": 9007199254740991,
            "cs:feature.key": "censhare:asset.id",
            "type": "integer"
          }
        }
      }
    },
    "name": {"type": "string"}
  }
}

Example entity:

JSON
{
  "members": [
    {
      "name": "Karl",
      "id": 14910
    },
    {
      "name": "Jill",
      "id": 14820
    },
    {
      "name": "Jack",
      "id": 14850
    }
  ],
  "name": "my guest list",
  "id": 14884
}

Update Behaviour

Inlined mappings are very convenient and straightforward for reading, but there are several possible ways the write operation can proceed and the exact behavior might be very unexpected. In the established computer science terminology, it depends on the nature of the relation: Aggregation, Composition, Association.

When you create or update the data, the system needs to correctly identify the assets that are used to store the inline entities without using their ids. This is achieved by using the property "cs:relation.$selection_key", which contains the array of property names used to identify the embedded entities. All the properties in the array need to match in order to identify the asset as matching. It is recommended that you use the asset id (feature "censhare:asset.id") to identify the entities.

The update behavior can be further limited by setting the property cs:relation.$inline_import. Its default behaviour, described above, is selected using the value update. Setting it to select disables the updating of assets matched by the selection key, and setting it to select_only disables the creation of new assets if no match can be found and the result is an error. These modes are helpful for applications where the inlining is used for more convenient read access to data in related assets.

By default, a related asset is updated if it can be matched to a JSON object using the selection. If no asset can be matched, a new one is created and for already related assets that can not be matched, the relation is deleted. By setting the property cs:relation.$cascade to true, unmatched related assets are deleted.

To sum it up, there are three aspects of the behavior, controlled by three directives:

  • Attaching to existing assets is only possible when "cs:relation.$selection_key" is set (and the asset is actually found). Otherwise, new assets are created (which is desired in some cases, but a significant bug in the others).
  • When attaching to existing assets, they are updated with the given values, unless cs:relation.$inline_import is set (note that this directive has no effect without "cs:relation.$selection_key"). The default value "update" is not desired in most cases, but unfortunately cannot be changed without breaking backwards compatibility.
  • When de-attaching an asset, it might be desired to delete it automatically - which can be enabled by directive "cs:relation.$cascade". The default (false) is safe in this case: detached assets are just left as they are.

The schema below uses the names of the person object to identify them:

JSON
{
  "title": "Guest list",
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "cs:relation.direction": "child",
        "cs:relation.key": "user.",
        "cs:asset.type": "person.",
        "cs:relation.$selection_key": [
            "name"         
        ],
        "type": "object",
        "properties": {
          "name": {
            "cs:feature.key": "censhare:asset.name",
            "type": "string"
          },
          "id": {
            "minValue": 0,
            "maxValue": 9007199254740991,
            "cs:feature.key": "censhare:asset.id",
            "type": "integer"
          }
        }
      }
    },
    "name": {
      "type": "string"
    }
  }
}

Initial content with only three members:

JSON
{
  "members": [
    { "name": "Karl", "id": 14910 },
    { "name": "Jill", "id": 14820 },
    { "name": "Jack", "id": 14850 }
  ],
  "name": "my guest list"
}

An update with only two members (and in different order):

JSON
{
  "members": [
    { "name": "Karl" },
    { "name": "Jill" }
  ],
  "name": "my guest list"
}

results in the following JSON. Note that assets ids are still the same. The relation to the person asset for Jack was removed. If cs:relation.$cascade is set to true in the schema, the asset for Jack (id 14850) itself is deleted.

JSON
{
  "members": [
    { "name": "Karl", "id": 14910 },
    { "name": "Jill", "id": 14820 }
  ],
  "name": "my guest list"
}

Removed member can be added again (without any change), and a completely new user can be created too:

JSON
{
  "members": [
    { "name": "Karl" },
    { "name": "Phil" },
    { "name": "Jack" }
  ],
  "name": "my guest list"
}

Resulting in:

JSON
{
  "members": [
    { "name": "Karl", "id": 14910 },
    { "name": "Phil", "id": 51931 },
    { "name": "Jack", "id": 14850 }
  ],
  "name": "my guest list"
}

Multiple Selection Keys

Instead of just having one selection key array, it is possible to provide an array of prioritized selection key arrays. The lookup is attempted starting with first till the last until a match is achieved.
In that case, "cs:relation.$selection_key" is array of arrays of strings, or an array of selection keys.

A version of the guest list, with id and name as a possible keys:

JSON
{
  "title": "Guest list",
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "cs:relation.direction": "child",
        "cs:relation.key": "user.",
        "cs:asset.type": "person.",
        "cs:relation.$selection_key": [
            ["id"],         
            ["name"]         
        ],
        "type": "object",
        "properties": {
          "name": {
            "cs:feature.key": "censhare:asset.name",
            "type": "string"
          },
          "uuid": {
            "cs:feature.key": "censhare:uuid",
            "type": "string"
          },
          "id": {
            "minValue": 0,
            "maxValue": 9007199254740991,
            "cs:feature.key": "censhare:asset.id",
            "type": "integer"
          }
        }
      }
    },
    "name": {
      "cs:feature.key": "censhare:asset.name",
      "type": "string"
    }
  }
}

Initial group data:

JSON
{
  "members": [
    { "name": "Richard", "id": 51936, "uuid": "30e0e610-5a7c-11e8-a03f-acde48001122" },
    { "name": "Albert", "id": 51937, "uuid": "30e13430-5a7c-11e8-a03f-acde48001122" },
    { "name": "Werner", "id": 14910, "uuid": "f48d1a50-57c5-11e5-a188-be923a50c134" }
  ],
  "name": "my guest list"
}

And example of several updates at once:

JSON
{
  "members": [
    {
      "name": "Dr. Richard",
      "id": 51936,
      "uuid": "30e0e610-5a7c-11e8-a03f-acde48001122"
    },
    {
      "name": "Dr. Albert Einstein",
      "id": 51937,
      "uuid": "30e13430-5a7c-11e8-a03f-acde48001122"
    },
    {
      "name": "Werner"
    }
  ],
  "name": "my guest list"
}

gives a new result below. Note that asset 51937 was matched by id while the name was updated and asset 14910 was matched by name and stayed in the members list.

JSON
{
  "members": [
    {
      "name": "Dr. Richard",
      "id": 51936,
      "uuid": "30e0e610-5a7c-11e8-a03f-acde48001122"
    },
    {
      "name": "Dr. Albert Einstein",
      "id": 51937,
      "uuid": "30e13430-5a7c-11e8-a03f-acde48001122"
    },
    {
      "name": "Werner",
      "id": 14910,
      "uuid": "f48d1a50-57c5-11e5-a188-be923a50c134"
    }
  ],
  "name": "my guest list"
}

Referencing Schemas

When the target asset mapping is shared with other places, such as standalone schemas of that asset as top-level entity, the property "$ref" can used to point to that schema. The string value assigned to "$ref" contains the name under which the schema is registered suffixed by "-schema.json#/". All the relation-specific mapping properties (starting with cs:relation.) are collected as siblings beside "$ref" and are not taken from the referred document.

Example person schema to reference:

JSON
{
  "type": "object",
  "cs:asset.type": "person.",
  "properties": {
    "name": {
      "cs:feature.key": "censhare:asset.name",
      "type": "string"
    },
    "uuid": {
      "cs:feature.key": "censhare:uuid",
      "type": "string"
    },
    "id": {
      "minValue": 0,
      "maxValue": 9007199254740991,
      "cs:feature.key": "censhare:asset.id",
      "type": "integer"
    }
  }
}

Example referencing the person schema:

JSON
{
  "title": "Guest list",
    "type": "object",
    "properties": {
      "members": {
        "type": "array",
        "items": {
          "type": "object",
          "cs:relation.direction": "child",
          "cs:relation.key": "user.",
          "cs:relation.$selection_key": [
              ["id"],         
              ["name"]         
          ],
          "$ref": "person-schema.json#/"
        }
      }
    }
}

All mapping directives (properties starting with cs:) present at the same level as $ref are merged into the result and can overwrite directives from the target schema. All other properties are silently ignored.

Please keep in mind, that the usage of $ref must not result in a recursive schema structure.

Localized mapping

Relations to assets with different content languages can also be mapped as an object with the locale codes as property keys instead of an array. An article asset for instance can reference multiple text assets, each containing the content in a different language.

Note: this kind of mapping should not be confused with '→Locale filter', which is fundamentally different. These two features also cannot be used at once (because any ordering is lost in JSON object).

Schema:

JSON
{
    "content": {
        "type": "object",
        "cs:feature.$localized": true,
        "additionalProperties": false,
        "patternProperties": {
            "^[a-z]{2}(_[A-Z]+)?$": {
                "cs:feature.$locale_property": "language",
                "cs:relation.key": "user.main-content.",
                "cs:relation.direction": "child",
                "type": "object",
                "$ref": "article_content-schema.json#/"
            }
        }
    }
}

Note that the "cs:feature.$localized": true must be at the "type":"object" level (no such marker is needed for arrays).

Example entity:

JSON
{
"content": {
        "de": {
            "assetId": 11717,
            "name": "Main Content DE",
            "language": "de",
            "title": "Lorem ipsum dolor sit amet"
        },
        "en": {
            "assetId": 11730,
            "name": "Main Content EN",
            "language": "en",
            "title": "Lorem ipsum dolor sit amet",
        }
    }
}

Important note about the pattern properties: although it accepts any locale code, only configured censhare languages are accepted during import.

Relation filter

This function allows you to restrict the displayed links to certain censhare asset types and/or assets with some specific feature value.

Warning: Using filtered relation mappings has some negative impacts:

  • When updating entities with relation filter, relations to "filtered out" assets can be lost. Also, sort order can be changed.
  • Relation sorting ("cs:relation.$sorting": true) is not allowed at all, because the behaviour would be completely undefined.

For these reasons, filtered relation mapping should be used only for read-only views of existing data structure. Preferably, it should not be used at all (if possible).

Asset type filter

The restrictions are specified in the "cs:relation.$filter.asset_type" property, which contains either an expression interpreted as a string or an array of allowed expressions interpreted as strings. The expressions are either censhare asset types or an asset type followed by an asterisks *, in which case the type and all its child values are accepted.

Example: list of members that contains only relations to asset types starting with person. (including also person.account. and person.webuser.):

JSON
{
  "type": "object",
  "properties": {
    "members": {
      "type": "array",
      "items": {
        "cs:relation.$filter.asset_type": ["person.*"],
        "type": "integer",
        "cs:relation.key": "user.",
        "cs:relation.direction": "child"
      }
    }
  }
}

Feature value filter

The restrictions are specified by the "cs:relation.$filter.feature" property, which can contain one or more feature filter definitions. Each filter definition contains:

  • "cs:feature.key": feature key, just like in a feature mapping
    • The feature must exist and its type must be representable as a single string (which means that pair values are not allowed).
    • Feature value is always used as its default string representation. No mapping directives are allowed.
    • Reference values are always used in its raw form: asset id (as a string) for ASSET_REF and the key value for ASSET_KEY_REF. Unlike the normal mapping of reference features, the target asset is not needed.
      • This is actually one of the main reasons why this special filtering exists in the first place: it works even if the target asset is not available.
  • "values": array of values used for filtering
    • Required and must not be empty.
    • Single-item array can be replaced by simple string.
  • "negate": true if the logic should be reversed == only assets without one of selected values are presented.
    • Optional, false is default.
    • Note that assets without specified feature are also matched in this case.
JSON
{
    "nowash": {
        "type": "array",
        "items": {
            "type": "string",
            "cs:relation.$filter.feature": [
                {
                    "cs:feature.key": "demo:product.laundry.washing",
                    "values": [
                        "demo:product-feature-item.laundry.washing.not-wash"
                    ]
                }
            ],
            "cs:relation.$ref_type": "link",
            "cs:relation.key": "user.",
            "cs:relation.direction": "child"
        }
    }
}

Locale filter

Any relation mapping can be marked with "cs:relation.$filter.locale" to sort the related assets by their locale ("language" attribute). If the mapping is singular, only the single best match is actually present in the result.

Locale preference can be specified either by query parameter locale (multiple values are allowed, order is important) or by the standard Accept-Language request header (only languages are allowed).

Assets are ordered according to the order of matching languages. If more than one asset matches the same language, the asset with matching country is ordered before assets that do not. Among assets that do not match any locale, the asset(s) without any language are sorted before assets that do have language.

change

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.