1. Introduction

The CaaS platform enables companies to flexibly provide and use content across various digital channels.

It consists of the REST Interface (caas-rest-api) for managing and querying content via HTTP, and the CaaS repository (caas-mongo), the internal database for storing content.

This document is intended for users of the REST Interface of the CaaS platform and describes its functions and usage options. Instructions and guidance for operating and technically administering the platform can be found in the separate Operations Guide.

2. Basics of the REST Interface

The REST Interface allows management and querying of content and media as JSON documents via HTTP. It supports authentication and authorization via API Key or security token and returns results in JSON format. In addition to CRUD operations on documents, databases, collections, API Keys, and indexes can also be managed. The interface also offers features such as pagination, filtering, and reference resolution. Additionally, data can be flexibly queried and managed via custom aggregations and GraphQL endpoints.

2.1. Authentication

Every request to the REST Interface must be authenticated, otherwise it will be rejected. The different authentication methods are explained below.

2.1.1. API Key method

Requests with API Key must include an HTTP header with a bearer token: Authorization: Bearer <key>.
The value of key should be the value of the key attribute of the API Key used.

For more information, see Validation of API Keys.

2.1.2. Security token method

It is possible to generate a short-lived (up to 24 hours) security token for an API Key. The token includes the same permissions as the API Key it was generated for.

There are two ways to generate and use these tokens:

  • Query parameter
    A GET request authenticated with an API Key to the endpoint /_logic/securetoken?tenant=<database> generates a security token. Such a token can only be issued for a specific database, regardless of whether the API Key has permissions for multiple databases. A parameter &ttl=<lifetime in seconds> is supported and optional. The security token is included in the JSON response.

    Any request to the REST Interface can optionally be authenticated via a query parameter ?securetoken=<token>.

  • Cookie
    A GET request authenticated with an API Key to the endpoint /_logic/securetokencookie?tenant=<database> generates a security token cookie. Such a cookie can only be issued for a specific database, regardless of whether the API Key has permissions for multiple databases. A parameter &ttl=<lifetime in seconds> is supported and optional. The response includes a Set-Cookie header with the security token.

    All requests with this cookie are automatically authenticated.

2.1.3. Evaluation precedence

If multiple authentication mechanisms are used in a request, only the first one found is evaluated. The precedence is as follows:

1. The query parameter securetoken
2. The Authorization header
3. The cookie securetoken

2.2. API Keys

API Keys enable authentication and authorization of requests to the REST Interface. They contain a list of permissions that define which actions are allowed on which resources.

API Keys can be managed at two levels: globally or locally per database. Accordingly, there are two types of API Keys, which differ in their scope:

  • Global API Keys
    Global API Keys are cross-database and are managed in the apikeys collection of the caas_admin database. They allow permissions to be defined for resources in multiple or all databases.

  • Local API Keys
    Local API Keys are defined per database and are managed in the apikeys collection of any database. Unlike global API Keys, local AAPI Keys can only define permissions for resources within the same database.

When authenticating with an API Key, the CaaS platform always searches for local AAPI Keys first. If no matching key is found, global API Keys are then evaluated.

2.2.1. Authorization model

Authorization of an API Key is performed based on each permission that is part of the key. The list of permissions is defined in the permissions attribute of the key.

A permission has the following attributes:

  • url: specifies the path to which the permission applies (e.g., a collection or endpoint)

  • permissionMode: determines the type of check, e.g., PREFIX, REGEX, or GRAPHQL

  • methods: lists the allowed HTTP methods (e.g., GET, POST, PUT)

When checking a permission, the url attribute is used. Its value is compared to the URL path of an incoming request. The type of check depends on the permissionMode of the permission.

There are three different permission modes:

  • PREFIX and REGEX
    In PREFIX mode, it is checked whether the value of the url attribute is a prefix of the URL path of an incoming request.

    In REGEX mode, a regular expression must be stored in the url attribute. The check is then performed by matching whether the URL path of an incoming request matches the pattern of the regular expression.

    Furthermore, for the permission modes PREFIX and REGEX, there is a fundamental distinction between the functionality of global and local API Keys. Global API Keys always check against the entire path of the request, while local API Keys check against the part of the path after the database. More information on global and local API Keys can be found in the example Difference between local and global API Keys or in the chapter API Keys.

  • GRAPHQL
    The GRAPHQL mode allows permission to execute a GraphQL application. The value of the url attribute is compared to the URI of a GraphQL application used by an incoming request. The URI of a GraphQL application is specified in the descriptor.uri attribute of the app definition. Further details can be found in the chapter GraphQL.

Difference between local and global API Keys

The following table illustrates the difference in authorization between local and global API Keys when using the permission modes PREFIX or REGEX.

Table 1. API Key authorization
Permission in API Key (url attribute) Type of API Key Request URL path Access allowed

/

global

/

yes

/project/

yes

/project/content/

yes

/other-project/

yes

/other-project/content/

yes

/project/

global

/

no

/project/

yes

/project/content/

yes

/other-project/

no

/other-project/content/

no

/

local in project

/

no

/project/

yes

/project/content/

yes

/other-project/

no

/other-project/content/

no

/content/

local in project

/

no

/project/

no

/project/content/

yes

/other-project/

no

/other-project/content/

no

2.2.2. Management via REST endpoints

The following endpoints are available for managing API Keys:

  • GET /<database>/apikeys

  • POST /<database>/apikeys
    Note: The parameters _id and key must be provided and have identical values

  • PUT /<database>/apikeys/{id}
    Note: The key parameter must have the same value as the {id} in the URL

  • DELETE /<database>/apikeys/{id}

The database to use depends on the type of API Key, see chapter API Keys.

An API Key can also be used as an authorization method for managing API Keys. In this case, the API Key used must have write permissions on the corresponding API Keys collection. This also applies to read-only requests and serves to prevent privilege escalation.

Example for creating a local API Key using curl:

curl "https://REST-HOST:PORT/<tenant>/apikeys" \
     -H 'Content-Type: application/json' \
     -u '<USER>:<PASSWORD>' \
     -d $'{
  "_id": "1e0909b7-c943-45a5-ae96-79f294249d48",
  "key": "1e0909b7-c943-45a5-ae96-79f294249d48",
  "name": "New-Apikey",
  "description": "Some descriptive text",
  "permissions": [
    {
      "url": "/<collection>",
      "permissionMode": "PREFIX",
      "methods": [
        "GET",
        "PUT",
        "POST",
        "PATCH",
        "DELETE",
        "HEAD",
        "OPTIONS"
      ]
    }
  ]
}'

The API Key shown in this example is of type "local" because it is defined in the apikeys collection under the database <tenant> and is therefore local to that database. The permission applies to all paths starting with /<tenant>/<collection>, since the permission mode PREFIX is used, and includes all HTTP methods listed in methods.

To create an API Key with REGEX mode, the above example must be adjusted as follows:

"url": "<regex>",
"permissionMode": "REGEX",

The apikeys collections are reserved for API Keys and cannot be used for normal content. They are automatically added to existing databases with a validation schema when the application starts, and also created during operation when databases are created or updated.

2.2.3. Validation of API Keys

Every API Key is validated against a stored JSON schema when created and updated. The JSON schema ensures the basic structure of API Keys and can be queried at /<database>/_schemas/apikeys.

Further validations ensure that no two API Keys with the same key can be created. Also, an API Key may not contain a URL more than once.

If an API Key does not meet the requirements, the corresponding request is rejected with HTTP status 400.

If the JSON schema was not successfully stored in the database beforehand, requests are answered with HTTP status 500.

The key attribute of an API Key should contain a valid UUID. The format of a UUID is strictly defined according to RFC 4122. This includes the use of lowercase letters. Although the CaaS platform does not currently validate this property, we reserve the right to activate this restriction in the future.

2.3. Push Notifications (Change Streams)

It is often desirable to be informed about changes in the CaaS platform. For this purpose, the CaaS platform offers Change Streams. This feature allows you to establish a WebSocket connection to the CaaS platform, through which events about various changes are published.

Change Streams are created by storing a definition in the metadata of a collection. If you use CaaS Connect, some predefined Change Streams are already created for you. You can also define your own change streams.

The format of the events corresponds to the standard MongoDB events.

When working with WebSockets, consider possible connection interruptions. Regular ping messages and a mechanism for automatic reconnection should be implemented.

You can find an example for using Change Streams in the browser in the Appendix.

2.4. Additional Information

Further information about the functionality of the REST Interface can be found in the official RESTHeart documentation.

3. REST API

3.1. Querying Documents and Media

Content is stored in so-called collections, which belong to databases. The following three-part URL schema applies:

Queries are performed using the HTTP GET method.

For binary content, special collections (so-called buckets) are provided, which always end with the suffix .files. The result document for a medium does not contain the binary data, but links that point to the URL with the actual binary data. For more information on binary content, see the RESTHeart documentation.

Please note that binary content is not transferred to the CaaS buckets in our SaaS offering.

If you use CaaS Connect, you can find more information in the CaaS Connect documentation.

3.1.1. Using Filters

Filters are used whenever documents should be retrieved based on their content rather than their ID. This allows both individual and multiple documents to be retrieved.

For example, the query for all English-language documents from the products collection has the following structure:

Beyond this example, there are further filtering options. More information can be found in the query documentation.

Please note that the query parameter np is automatically added to filter queries on collections. As a result, the response does not contain collection metadata, as this is usually irrelevant for this query type. This behavior can be disabled in on-premises installations, see Operations Guide.

3.1.2. Format of Result Documents

By default, the interface returns all JSON data in HAL format. Thus, they are not just raw data, as is traditionally the case with unstructured JSON content.

The HAL format offers the advantage of simple yet powerful structuring. In addition to the required content, the results contain additional meta-information about the structure of this content.

Example

{  "_size": 5,
   "_total_pages": 1,
   "_returned": 3,
   "_embedded": { CONTENT }
}

In this example, a filtered query was executed. Without knowing the exact content, its structure can be read directly from the meta-information. The REST Interface returns three results matching the filter criteria from a set of five documents and presents them on a single page.

If the requested element is a medium, the URL only retrieves its metadata.

3.1.3. Page Size of Queries

The results of the REST Interface are always paginated to avoid performance issues with large collections. To control the requested page and the number of documents per page, the HTTP query parameters page and pagesize can be used in GET requests.
The default value for the pagesize parameter is set to 20 in the CaaS platform, and the maximum is 100.
These values can be changed for on-premises installations (see Operations Guide).

More information can be found in the RESTHeart documentation.

3.1.4. Resolving References

CaaS documents can reference other CaaS documents. When processing documents, the referenced content is often needed directly. To avoid sequential queries in these cases, the query parameter resolveRef can be used.

The following two JSON documents illustrate this:

{
  "_id": "my-document",
  "fsType": "ProjectProperties",
  "formData": {
    "ps_audio": {
      "fsType": "FS_REFERENCE",
      "value": {
        "fsType": "PageRef",
        "url": "https://REST-HOST:PORT/my-db/col/my-referenced-document"
      }
    }
  }
}
{
  "_id": "my-referenced-document",
  "fsType": "PageRef",
  "name": "audio"
}

In the first document, the JSON under formData.ps_audio.value.url contains an absolute URL to another document in the CaaS. The following request shows how this reference is resolved in the same request that reads the document.

curl -X GET --location "https://REST-HOST:PORT/my-db/col/my-document?resolveRef=formData.ps_audio.value.url" \
    -H "Authorization: Bearer my-api-key"

The value of the query parameter must be the URL path specified in the JSON. The response then contains an additional attribute _resolvedRefs:

{
  "_id": "my-document",
  "fsType": "ProjectProperties",
  "formData": {
    "ps_audio": {
      "fsType": "FS_REFERENCE",
      "value": {
        "fsType": "PageRef",
        "url": "https://REST-HOST:PORT/my-db/col/my-referenced-document"
      }
    }
  },
  "_resolvedRefs": {
    "https://REST-HOST:PORT/my-db/col/my-referenced-document": {
      "_id": "my-referenced-document",
      "fsType": "PageRef",
      "name": "audio"
    }
  }
}

Reference resolution is affected by Configuration and limitations.

The resolveRef parameter is also supported for collection queries. In this case, references in all returned documents are resolved. The documents of the resolved references are collected in a new, additional document, which is then added to the response array.

curl -X GET --location "https://REST-HOST:PORT/my-db/col?resolveRef=formData.ps_audio.value.url" \
    -H "Authorization: Bearer my-api-key"
[
  {
    "_id": "my-document",
    "fsType": "ProjectProperties",
    "formData": {
      "ps_audio": {
       "fsType": "FS_REFERENCE",
       "value": {
         "fsType": "PageRef",
         "url": "https://REST-HOST:PORT/my-db/col/my-referenced-document"
       }
      }
    }
  },
  {
    "_id": "_resolvedRefs",
    "https://REST-HOST:PORT/my-db/col/my-referenced-document": {
      "_id": "my-referenced-document",
      "fsType": "PageRef",
      "name": "audio"
    }
  }
]

This additional document is always the last element in the response array and can be identified by the ID _resolvedRefs.

The pagesize query parameter does not affect this document, so the actual size of the array can be pagesize + 1.

Transitive References

Referenced documents may themselves contain further references. These can also be resolved in the original request. For this, the path to the next reference must also be specified in the request, including the prefix $i., where i is the depth of the reference resolution chain.

The following request shows this based on the previous example, using a reference in the attribute page.url of the document my-referenced-document:

curl -X GET --location "https://REST-HOST:PORT/my-db/col?resolveRef=formData.ps_audio.value.url&resolveRef=$1.page.url" \
    -H "Authorization: Bearer my-api-key"
Reference Path Syntax

Depth 0 describes the documents that are returned for the original request. The prefix $0. is optional for depth 0. Depth 1+ means all documents that were found by successfully resolving the references at the previous depth.

Depth JSON document resolveRef Explanation

0

{
  "data": {
    "url": "<url>"
  }
}

data.url

In all documents of depth 0, references are searched for under the path data.url and resolved.

$0.data.url

{
  "data": [
    { "url": "<url1>" },
    { "url": "<url2>" }
  ]
}

data[*].url

The array with the name data is searched in all documents of depth 0. * means that all objects in the array are searched. The value of url is resolved as a reference in these objects.

data[0].url

Resolves url only in the first object of the array.

data[1].url

Resolves url only in the second object of the array.

{
  "data": [
    [ {"url": "<url1>"} ],
    [ {"url": "<url2>"} ]
  ]
}

data[*][*].url

All arrays in the data array are searched. The value of url is resolved as a reference in these objects.

data[0][*].url

The first array in the data array is searched. In all objects of the array, url is resolved.

1

<JSON document>

$1.<path>

The paths must start with $1.. Otherwise, they can be specified in the same way as for depth 0.

n

<JSON document>

$<n>.<path>

The paths must start with $<n>., where <n> corresponds to the depth of the resolution chain. Otherwise, they can be specified in the same way as for depth 0.

Configuration and limitations
  • Only absolute URLs can be resolved.

  • No errors are thrown when resolving references if incorrect paths or URLs are specified. Incorrect paths and references are silently ignored.

  • Paths must be specified according to the Reference Path Syntax. The syntax is based on JsonPath, but only the documented operators are supported.

  • URLs in the _resolvedRefs document are not normalized. URL references that are not exactly identical are considered different documents, even if they point to the same document in the CaaS. This can be caused, for example, by an additional / at the end of the URL or by different query parameters.

  • By default, the maximum depth for reference resolution is 3. Thus, a maximum of $2. can be used as a path prefix for references.

  • A maximum of 100 different references can be resolved in a single request. Once this limit is reached, no further references are collected and therefore not part of the response.

  • Reference resolution is enabled by default.

  • The settings for maximum depth and reference limit, as well as reference resolution itself, can be changed in an on-premises installation; see Operations Guide.

3.1.5. Indexes for Efficient Query Execution

The runtime of queries with filters (see chapter Use of Filters) can increase as the number of documents in a collection grows. If it exceeds a certain value, the query will be answered by the REST Interface with HTTP status 408. More efficient execution can be achieved by creating an index on the attributes used in the relevant filter queries.

Detailed information about database indexes can be found in the documentation of MongoDB.

Predefined Indexes

If you are using CaaS Connect, predefined indexes are already created to support some common filter queries.

The exact definitions can be retrieved at https:/REST-HOST:PORT/<database>/<collection>/_indexes/.

Custom Indexes

If the predefined indexes do not cover your use cases and you observe long response times or even request timeouts during queries, you can create your own indexes. The REST Interface can be used to manage the desired indexes. The procedure is described in the RESTHeart documentation.

Please only create the indexes you actually need.

3.2. Modifying and Deleting Documents

To modify documents, you can use the HTTP methods POST, PUT, and PATCH. Additionally, documents can be deleted using the DELETE verb.

The following example shows how to create a document my-document within the collection my-collection in the database my-db.

curl --location --request PUT 'https://REST-HOST:PORT/my-db/my-collection/my-document' \
--header 'Authorization: Bearer my-api-key' \
--header 'Content-Type: application/json' \
--data-raw '{
    "data": "some-data"
}'

For more information on storing documents, refer to the corresponding sections in the RESTHeart documentation.

When storing documents using the POST, PUT, or PATCH verbs, the default write mode is upsert, which differs from RESTHeart. For more information on the write mode, see the RESTHeart documentation.

3.3. Managing Databases and Collections

Unlike document storage, managing databases and collections is limited to the HTTP methods PUT and DELETE.

The following example shows how to create the database my-db with a PUT request.

curl --location --request PUT 'https://REST-HOST:PORT/my-db' \
--header 'Authorization: Bearer my-api-key'

There are reserved databases that cannot be used to store regular content. These include caas_admin, _logic, and graphql. For more information on the purpose of the reserved database graphql, see section GraphQL.

For more information on managing databases, refer to the corresponding sections in the RESTHeart documentation.

Managing databases is not supported in our SaaS offering due to restricted access permissions.

A collection my-collection can be created in the database my-db with a PUT request as follows.

curl --location --request PUT 'https://REST-HOST:PORT/my-db/my-collection' \
--header 'Authorization: Bearer my-api-key'

There are reserved collections that cannot be used to store regular content. Reserved collection names include apikeys and gql-apps. For more information on the purposes of the reserved collections, see section Managing API Keys → REST Endpoints and GraphQL.

For more information on managing collections, refer to the corresponding sections in the RESTHeart documentation.

4. GraphQL

Unlike classic REST queries, GraphQL offers a flexible and typed interface that can retrieve complex, nested data structures in a single request. This simplifies frontend development, as fewer roundtrips and individual endpoints are needed. Additionally, GraphQL enables a clear definition of the data model and better documentation of available queries.

Mutations are currently not supported.

4.1. Authentication and Authorization

The authentication and authorization process for GraphQL queries works differently than for REST API queries. Unlike REST API queries, where the request path of the query is checked against a list of allowed URLs, GraphQL queries are authorized using one of the following access checks:

  1. Explicit execution permission
    Checks whether the API Key has explicit permission (GRAPHQL permission of the API Key) to send queries to the GraphQL application.

  2. Implicit execution permission
    Checks whether the API Key has access to all databases and collections. This is checked by matching the URL permissions (PREFIX and REGEX) of the API Key with the paths of all databases and collections that the GraphQL application allows access to.

For local API Keys, there is an additional condition that they may only access GraphQL applications of the same database.

For more information on the different types of permissions of a API Key, see chapter Authorization model.

4.2. Managing GraphQL applications

With REST Interface, you can create, update, and delete your own GraphQL applications. If you are using CaaS Connect, predefined applications are already created that are specifically tailored to querying FirstSpirit content.

A GraphQL application has an app definition that must be stored in the gql-apps collection. This collection is automatically created when a database is created/updated. However, if it does not yet exist in the database, it must first be created manually with a PUT request.

The gql-apps collections of all databases are reserved for GraphQL purposes and cannot be used for normal content.

When editing the GraphQL app definition (e.g., Creating/Updating an Application), the permissions of the API Key are checked. An operation can only be performed if the API Key has access to all databases and collections listed in the definition.

A quick start guide can be found in the appendix under Quickstart: GraphQL applications.

4.2.1. Creating/Updating an Application

To create or update a GraphQL application, send a PUT request with the app definition to the following URL:
https://REST-HOST:PORT/<tenant>/gql-apps/<app-uri>

Notes:

  • <app-uri> must have the value <tenant>___<my-appname>.

  • The descriptor.uri parameter of the app definition must match <app-uri> and, unlike the RESTHeart documentation, is not optional in CaaS.

This provisions an endpoint at https://REST-HOST:PORT/graphql/<app-uri>, which can then be used for GraphQL queries. Information about executing GraphQL queries can be found in chapter GraphQL API.

All operations on GraphQL applications are automatically mirrored in the /caas_admin/gql-apps collection for technical reasons. See chapter Resynchronizing existing GraphQL applications for information on how this mechanism can also be triggered manually if needed.

Providing the App Definition
There are two ways to provide the app definition in the PUT request:

  • Using the JSON body
    The complete app definition is sent in the request body (Content-Type application/json).

    An example of such a GraphQL app definition can be found in chapter GraphQL example application.

  • Using file upload
    Instead of sending the entire app definition, it can also be transmitted as a multipart upload (Content-Type multipart/form-data).
    The components of the app definition are split into two separate files, which must be sent as individual file parts in the request:
    Part app: File containing the descriptor and mapping sections in JSON format.
    Part schema: File containing the raw text version of schema in the GraphQL schema definition language.

    Uploading the files using curl
    curl -i -X PUT \
      -H "Authorization: Bearer $API_KEY" \
      -F app=@my-app-def.json \
      -F schema=@my-schema.gql \
      https://REST-HOST:PORT/<tenant>/gql-apps/<tenant>___<name>

    For fast feedback cycles during development, this can be combined with a file change monitoring tool to continuously update the GraphQL application:

    Continuous upload of files using fswatch and curl
    fswatch -o my-app-def.json my-schema.gql | xargs -n1 -I{} \
      curl -i -X PUT \
      -H "Authorization: Bearer $API_KEY" \
      -F app=@my-app-def.json \
      -F schema=@my-schema.gql \
      https://REST-HOST:PORT/<tenant>/gql-apps/<tenant>___<name>

Notes

  • A GraphQL application can query data using a MongoDB query or aggregation. The chapter Object Mappings in the RESTHeart documentation contains more detailed information. An example of a GraphQL application that uses an aggregation can be found in the appendix.

    Variables within authorization-relevant attributes of aggregation stages (such as database or collection names) are not permitted.

  • Complex mappings with multiple foreign key relationships can result in increased response times for queries. For more efficient query execution, we recommend using indexes. Configuring batching and caching can also help optimize response times. For details, see this documentation.

4.2.2. Deleting the application

To delete a GraphQL application, a DELETE request is sent to the following URL:
https://REST-HOST:PORT/<tenant>/gql-apps/<tenant>___<name>

4.2.3. Resynchronizing existing GraphQL applications

Under certain conditions, such as after restoring individual collections from a backup, it may happen that previously created GraphQL applications are no longer synchronized. In such cases, all tenant-specific GraphQL applications must be resynchronized.

To resynchronize all existing GraphQL applications of all tenants, a POST request must be made to the HTTP endpoint /_logic/sync-gql-apps.

4.3. GraphQL API

Each of the GraphQL applications defined via the management API (see GraphQL) provides a GraphQL API endpoint. This endpoint can be used to retrieve data (see Fetching data).

4.3.1. Fetching data

The GraphQL API can be queried via the following HTTP endpoints:

https://REST-HOST:PORT/graphql/<app-uri>

To query data, send a POST request to the desired endpoint and specify the query with JSON in the request body, e.g.:

Querying data via GraphQL using curl
curl -i -X POST \
  -H “Content-Type: application/json” \
  -H “Authorization: Bearer $API_KEY” \
  -d '{“query”: "query ($lang: [String!]){products(_language: $lang) {name description categories {name} picture {name binaryUrl width height}}}“, ”variables“: {‘lang’: [”EN"]}}' \
https://REST-HOST:PORT/graphql/<app-uri>

A more detailed example of how to use GraphQL can be found in the appendix.

Similar to page size limits for REST queries, there is also a limit on the number of results for GraphQL queries. The default value is 20 documents. Please use pagination arguments if more documents need to be queried. Further information can be found in the Field to Field mapping section of the Object Mappings chapter of the RESTHeart documentation.

4.3.2. Model Context Protocol (MCP) Server for AI (Alpha)

We offer an experimental MCP server that helps AI agents interact better with the FirstSpirit GraphQL API. It helps AI create valid GraphQL queries and check their functionality. For more information, see the CaaS MCP Server documentation and the FirstSpirit GraphQL API documentation.

5. Appendix

5.1. Examples

5.1.1. Change stream example

Usage of Change Streams with Javascript and Browser API
<script type="module">
  import PersistentWebSocket from 'https://cdn.jsdelivr.net/npm/pws@5/dist/index.esm.min.js';

  // Replace this with your API key (needs read access for the preview collection)
  const apiKey = "your-api-key";

  // Replace this with your preview collection url (if not known copy from CaaS Connect Project App)
  // e.g. "https://REST-HOST:PORT/my-tenant-id/f948bb48-4f6b-4a8a-b521-338c9d352f2b.preview.content"
  const previewCollectionUrl = new URL("your-preview-collection-url");

  const pathSegments = previewCollectionUrl.pathname.split("/");
  if (pathSegments.length !== 3) {
    throw new Error(`The format of the provided url '${previewCollectionUrl}' is incorrect and should only contain two path segments`);
  }

  (async function(){
    // Retrieving temporary auth token
    const token = await fetch(new URL(`_logic/securetoken?tenant=${pathSegments[1]}`, previewCollectionUrl.origin).href, {
      headers: {'Authorization': `Bearer ${apiKey}`}
    }).then((response) => response.json()).then((token) => token.securetoken).catch(console.error);

    // Establishing WebSocket connection to the change stream "crud"
    // ("crud" is the default change stream that the CaaS Connect module provides)
    const wsUrl = `wss://${previewCollectionUrl.host + previewCollectionUrl.pathname}`
      + `/_streams/crud?securetoken=${token}`;
    const pws = new PersistentWebSocket(wsUrl, { pingTimeout: 60000 });

    // Handling change events
    pws.onmessage = event => {
      const {
        documentKey: {_id: documentId},
        operationType: changeType,
      } = JSON.parse(event.data);
      console.log(`Received event for '${documentId}' with change type '${changeType}'`);
    }
  })();
</script>

5.1.2. GraphQL example application

This chapter describes a use case for a GraphQL application as an example. For this we will outline the individual steps that belong to the creation of a GraphQL application and its later use.

Create the GraphQL app definition

In the example scenario, a GraphQL application is created that can be used to query data records located in the CaaS. The data sets used here are the products from the example project of the fictitious company “Smart Living”. Image references and product categories in the data sets are resolved directly.

The entire command to create the GraphQL app definition for this example scenario looks like this.

Example of a full GraphQL app definition
curl --location --request PUT 'https://REST-HOST:PORT/mycorp-dev/gql-apps/mycorp-dev___products' \
--header 'Authorization: Bearer <PERMITTED_APIKEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "descriptor": {
        "name": "products",
        "description": "example app to fetch product relevant information from SLG",
        "enabled": true,
        "uri": "mycorp-dev___products"
    },

    "schema": "type Picture{ name: String! identifier: String! binaryUrl: String! width: Int! height: Int! } type Category{ name: String! identifier: String! } type Product{ name: String! identifier: String! description: String categories: [Category] picture: Picture } type Query{ products(_language: [String!] = [\"DE\", \"EN\"]): [Product] }",

    "mappings": {
        "Category": {
            "name": "displayName",
            "identifier": "_id"
        },
        "Picture": {
            "name": "displayName",
            "identifier": "_id",
            "binaryUrl": "resolutionsMetaData.ORIGINAL.url",
            "width": "resolutionsMetaData.ORIGINAL.width",
            "height": "resolutionsMetaData.ORIGINAL.height"
        },
        "Product": {
            "name": "displayName",
            "identifier": "_id",
            "description": "formData.tt_abstract.value",
            "picture": {
                "db": "mycorp-dev",
                "collection": "d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content",
                "find": {
                    "identifier": {
                        "$fk": "formData.tt_media.value.0.formData.st_media.value.identifier"
                    },
                    "locale.identifier": {
                        "$fk": "locale.identifier"
                    }
                }
            },
            "categories": {
                "db": "mycorp-dev",
                "collection": "d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content",
                "find": {
                    "identifier": {
                        "$in": {
                            "$fk": "formData.tt_categories.value.identifier"
                        }
                    },
                    "locale.identifier": {
                        "$fk": "locale.identifier"
                    }
                }
            }
        },
        "Query": {
            "products": {
                "db": "mycorp-dev",
                "collection": "d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content",
                "find": {
                    "locale.identifier": { "$in": { "$arg": "_language" } },
                    "entityType": "product"
                }
            }
        }
    }
}'

When creating a GraphQL app definition, the schema must be specified as a JSON string. For better readability, we recommend formatting the schema more appropriately.

Schema of the GraphQL app definition

The schema used for the example contains the following definitions.

Example of a formatted GraphQL schema
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Picture {
    name: String!
    identifier: String!
    binaryUrl: String!
    width: Int!
    height: Int!
}

type Category {
    name: String!
    identifier: String!
}

type Product {
    name: String!
    identifier: String!
    description: String
    categories: [Category]
    picture: Picture
}

type Query {
    products(_language: [String!] = ["DE", "EN"]): [Product]
}

Lines 1, 9 and 14 of the schema are the starting point for the type definitions of the objects used in the GraphQL app. In addition, each GraphQL schema contains a query type (line 22) that defines what data can be queried by a GraphQL app. More details about schemas in GraphQL can be found in the GraphQL documentation.

In line 23 we define a query with the name products, which returns a collection of [Product]. To specify the languages in which we need this data, we add the _language variable. As most of our customers are German or English, we also add a default value of ["DE", "EN"]. This marks the variable as optional.

Mapping the GraphQL app definition

The GraphQL app definition mapping represents the connection between the schema and the data in the database. Each type described in the schema generally requires an explicit entry, so this part of a GraphQL app definition is usually the longest. There may be situations where the fields in the type should be named exactly as the keys of the data. In this special case, no explicit entry in the mapping is necessary. For details about the mapping in GraphQL app definition, see the corresponding chapter in RESTHeart documentation.
The following example is an excerpt from creating a GraphQL app definition and clarifies some use cases.

Example of a GraphQL mapping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
  "Category": {
    "name": "displayName",
    "identifier": "_id"
  },
  "Picture": {
    "name": "displayName",
    "identifier": "_id",
    "binaryUrl": "resolutionsMetaData.ORIGINAL.url",
    "width": "resolutionsMetaData.ORIGINAL.width",
    "height": "resolutionsMetaData.ORIGINAL.height"
  },
  "Product": {
    "name": "displayName",
    "identifier": "_id",
    "description": "formData.tt_abstract.value",
    "picture": {
      "db": "mycorp-dev",
      "collection": "d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content",
      "find": {
        "identifier": {
          "$fk": "formData.tt_media.value.0.formData.st_media.value.identifier"
        },
        "locale.identifier": {
          "$fk": "locale.identifier"
        }
      }
    },
    "categories": {
      "db": "mycorp-dev",
      "collection": "d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content",
      "find": {
        "identifier": {
          "$in": {
            "$fk": "formData.tt_categories.value.identifier"
          }
        },
        "locale.identifier": {
          "$fk": "locale.identifier"
        }
      }
    }
  },
  "Query": {
    "products": {
      "db": "mycorp-dev",
      "collection": "d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content",
      "find": {
        "locale.identifier": {
          "$in": {
            "$arg": "_language"
          }
        },
        "entityType": "product"
      }
    }
  }
}

The first use case considered is the so-called field to field mapping. In this type of mapping, a field in the type is assigned a corresponding attribute of the data. An example of this can be seen in line 3, where the field Category.name from the schema refers to attribute displayName from the data.

The second use case is the field to query mapping. Here a field in the type is mapped to the result of a data query. An example of such a mapping can be found in line 45ff: the field Query.products is mapped by the data found in the REST Interface under /mycorp-dev/d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content and correspond to the filters entityType": "product" and "locale.identifier": { "$in": { "$arg":"_language" } }. This means that exactly those products are queried which are located in the defined source, represent an entity of “product” and use one of the language abbreviations passed in the “_language” argument.

Another example of a “field to query mapping” can be found starting at line 29. In this mapping definition, the product categories, which are maintained in separate records, are identified using a foreign key relationship. The complete entry from line 29-42 states that the product.categories field will list all product categories that are under /mycorp-dev/d8db6f24-0bf8-4f48-be47-5e41d8d427fd.preview.content, whose identifier is stored in the formData.tt_categories.value.identifier field, and whose locale.identifier exactly matches what is in the product record as locale.identifier. Since a product can reference multiple categories in the dataset under formData.tt_categories.value.identifier, the key $in is used here.

If multiple filters are specified in a "find", they are automatically joined, eliminating the need for an additional parenthesized "$and.

Using the GraphQL app

Requests can now be made to a GraphQL application using this app definition.

GraphQL query example
curl --location --request POST 'https://REST-HOST:PORT/graphql/mycorp-dev___products' \
--header 'Authorization: Bearer <PERMITTED_APIKEY>' \
--header 'Content-Type: application/json' \
--data-raw '{"query": "query($lang: [String!]){products(_language: $lang) {name description categories {name} picture {name binaryUrl width height}}}", "variables": {"lang": ["DE"]}}'

This request example shows how to call the GraphQL app using curl. The app is always available at /graphql/<descriptor.uri>. Through this query, product data is retrieved depending on the variable $lang. The variable is passed as a value to the _language argument defined in the schema. Since a default value for _language is included in the schema, specifying a value is optional in this scenario. Further details on query arguments and variables can be found in the GraphQL documentation.

5.1.3. GraphQL example application using Aggregations

Aggregations can be used to create complex queries to the CaaS. For example, it’s possible to dynamically add a computed attribute to the returned documents. Much more complex aggregations can be created. A full list of possible aggregation stages and operators can be found here.

Example of a GraphQL app with an aggregation inside the query mapping
{
  "_id": "mytenantid-dev___pagerefs",
  "descriptor": {
    "name": "pagerefs",
    "description": "Query PageRefs",
    "enabled": true,
    "uri": "mytenantid-dev___pagerefs"
  },
  "schema": "type PageRef { _id: String projectId: String } type Query{ pageRefs(projectId: String): [PageRef] }",
  "mappings": {
    "PageRefs": {
      "count": "count"
    },
    "Query": {
      "pageRefs":{
        "db": "mytenantid-dev",
        "collection": "641154a9-b90c-4b10-a5f7-38677cbb5abc.release.content",
        "stages": [
          { "$match": { "fsType":"PageRef" }},
          { "$addFields": { "projectId": { "$arg": "projectId" } } }
        ]
      }
    }
  }
}

5.2. Tutorials

5.2.1. Getting started with GraphQL Apps

This tutorial serves as an introduction to creating and using GraphQL apps. To complete it you need to be familiar with the REST Interface in terms of authentication and querying documents.

GraphQL apps allow you to query your CaaS documents via your own GraphQL API endpoints. These endpoints also make the document data accessible in a specific format/schema that you define.

In the following steps we will be creating such a GraphQL app and use it to query document data. We’ll be using curl to send HTTP requests, but you can use other alternative tools.

  1. Create sample documents

    Let’s create a collection to store some sample documents.

    # set these once so you can re-use them for other commands
    TENANT='YOUR-TENANT-ID'
    API_KEY='YOUR-API-KEY'
    
    curl --location --request PUT "https://REST-HOST:PORT/$TENANT/posts" \
    --header "Authorization: Bearer $API_KEY"

    And create the documents.

    # you can execute this multiple times to create many documents
    curl --location "https://REST-HOST:PORT/$TENANT/posts" \
    --header "Authorization: Bearer $API_KEY"
    --header 'Content-Type: application/json' \
    --data "{
        \"content\": \"My post created at $(date)..\"
    }"
  2. Define the desired GraphQL schema

    Now that we have documents available, we need to define a schema for accessing their data.

    We’ll save a simple data model for the sample documents we created earlier, but you can create arbitrarily complex data models.

    Save GraphQL schema definition (schema.gql)
    cat > schema.gql << EOF
    type BlogPost {
      content: String!
    }
    
    type Query {
      posts: [BlogPost!]
    }
    EOF
  3. Create the GraphQL API endpoint

    The next step is to create the GraphQL app using our schema, which automatically provisions a new API endpoint.

    First, however, we need define a couple of things so that CaaS knows how to provision our new endpoint and how to fetch/map the documents to our schema. We do this by creating a GraphQL app definition.

    Save GraphQL app definition (app.json)
    cat > app.json << EOF
    {
        "descriptor": {
            "name": "myposts",
            "description": "Example app to fetch blog posts.",
            "enabled": true,
            "uri": "${TENANT}___myposts"
        },
        "mappings": {
            "Query": {
                "posts": {
                    "db": "$TENANT",
                    "collection": "posts",
                    "find": {
                        "content": { "\$exists": true }
                    }
                }
            }
        }
    }
    EOF

    Now that we have prepared a schema (schema.gql) and a corresponding app definition (app.json), we can use them both to create a GraphQL app using the REST Interface.

    Creating GraphQL app
    curl -X PUT \
    -H "Authorization: Bearer $API_KEY" \
    -F app=@app.json \
    -F schema=@schema.gql \
    https://REST-HOST:PORT/$TENANT/gql-apps/${TENANT}___myposts
  4. Query data using the new endpoint

    At this point, CaaS has automatically provisioned a new GraphQL API endpoint using our definitions. The endpoint is available at

    https://REST-HOST:PORT/graphql/\{YOUR-TENANT-ID}___myposts

    and we can query our documents using the new endpoint.

    curl --location "https://REST-HOST:PORT/graphql/${TENANT}___myposts" \
    --header "Authorization: Bearer $API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{"query":"{ posts { content } }","variables":{}}'
  5. Congratulations on querying data using your own GraphQL app! You should see a result similar to this.

    {
      "data": {
        "posts": [
          {
            "content": "My post created at Tue Aug  8 17:08:32 CEST 2023.."
          },
          {
            "content": "My post created at Tue Aug  8 17:12:23 CEST 2023.."
          }
        ]
      }
    }

The default page size for GraphQL queries is 20. If you need to query more documents than that, please use pagination arguments. For more information see section Field to Field mapping of RestHearts Object Mappings chapter.

6. Help

The Technical Support of the Crownpeak Technology GmbH provides expert technical support covering any topic related to the FirstSpirit™ product. You can get and find more help concerning relevant topics in our community.

7. Disclaimer

This document is provided for information purposes only. Crownpeak Technology GmbH may change the contents hereof without notice. This document is not warranted to be error-free, nor subject to any other warranties or conditions, whether expressed orally or implied in law, including implied warranties and conditions of merchantability or fitness for a particular purpose. Crownpeak Technology GmbH specifically disclaims any liability with respect to this document and no contractual obligations are formed either directly or indirectly by this document. The technologies, functionality, services, and processes described herein are subject to change without notice.