Common GraphQL patterns

This section describes some common patterns that we use in the Totara API across services. See also the GraphQL development best practices page for additional guidance on how we recommend developing services.

Formats

GraphQL provides the opportunity for the client to specify not just what information they wish to receive, but also the format of that information. Two common examples of data that may be available in different formats are date/time data and rich-text strings.

Date formatting

With date-time strings we provide a core_date_format input enum, which offers a range of possible date formats. These can typically be specified via a format argument, for example:

query get_status_timestamps {
    totara_webapi_status {
        date_only: timestamp(format: DATELONG)
        time_only: timestamp(format: TIME)
        date_and_time: timestamp(format: DATETIMELONG)
    }
}

which would return:

{
    "data": {
        "totara_webapi_status": {
            "date_only": "3/10/2022",
            "time_only": "10:46 AM",
            "date_and_time": "3/10/2022, 10:46"
        }
    }
}

Rich-text string formatting

Similarly, we also provide a core_format input enum, which can be used to specify how you want some text fields to be output. Note that not all fields will support all output formats; the documentation for a specific field should describe the available options.

This can be used to request the data in the format that is most appropriate for how you want to use it.

Filtering

For queries that return a collection of results, it is common to want to pass in data to filter the collection to only the desired or most relevant results in responses.

A common pattern we use for this is to provide a filters input field, which specifies a structure for the filters that can be applied and the specific data to filter by. For example:

extend type Query {
  mod_perform_my_subject_instances(
    filters: mod_perform_subject_instance_filters
    options: mod_perform_subject_instance_options
    pagination: core_pagination_input
  ): mod_perform_subject_sections_page!
}

input mod_perform_subject_instance_filters {
  about_role: core_id
  activity_type: [core_id!]
  exclude_complete: param_boolean
  overdue: param_boolean
  participant_progress: [mod_perform_participant_instance_progress_status!]
  search_term: param_text
}

Here the filter input type offers a range of different filtering options: by a single ID, by a set of IDs, a boolean value, a set of items of a more specific type, and a text search string.

All filters are optional, so only those passed will be applied.

Look out for the filters field on queries and a matching input type with the _filters suffix to see how a specific query can be filtered.

Pagination

It is common for queries to provide a mechanism to limit the amount of data returned in a single response, in order to prevent the response from being too large. Totara uses a mixture of offset-based pagination (typically used to split data into pages and request a specific page of results) and cursor-based pagination (typically used to provide a 'show more' link, where additional pages can be loaded as required). It is up to the service to specify which of these it supports.

Currently we have a single core input type core_pagination_input which can be used for either mechanism - you can either pass 'cursor' and 'limit' for cursor-based pagination, or 'limit' and 'page' for offset-based pagination.

We also provide an interface core_pageable_result which is commonly used by query return types to provide a consistent response structure for paginated results:

interface core_pageable_result {
  """
  Total number of records returned by the request (not the number returned in this result/page, but the overall count).
  """
  total: Int!
  """
  Opaque string containing information to allow the system to identify the next set of results that should be returned
  after this one. This value can be passed into a query that uses core_pagination_input to fetch the next page.
  """
  next_cursor: String!
}

See Pagination with GraphQL for more details on how and why we implement different pagination types.

Sorting

Another common requirement is the need to sort the results within a collection in a specific way. We offer the core_sort_input input type, which is typically used by services which support sorting of results.

Typically this is offered via a sort property in the input argument. Different services may offer sorting by a single column (sort: core_sort_input argument) or by multiple columns (sort: [core_sort_input!]! argument). For example:

query {
  example_query(
    input: {
      sort: {
        column: 'time_modified',
        direction: ASC
      }
    }
  ) {
    id
    name
    time_modified
  }
}

Reference input types

When using an API it is common to want to reference a specific instance of an object in the system. Two examples of when this might be useful are:

  1. To perform a specific action on an item. For example: to specify the user you want to delete.
  2. To link an item to another item. For example: to specify the user who you want to be assigned as another user's manager.

Internally in Totara we use an internal database id as the unique identifier for each record, but when interacting with a system via the API you may not always have access to the database id. If not, you might need to make a completely separate extra request to locate the id from another piece of information that you do have (for example, you might know the user's username or email instead). This can cause unwanted additional work, both when writing integrations and on an ongoing basis when making requests.

Reference types solve this problem by providing a flexible system for identifying a single record. Each reference type implementation will be for a specific object (such as users, jobs, or tenants) and will define some specific fields that can be passed to identify individual records for that type. For users, for example, any of id, username, email or idnumber can be passed:

""" Schema for user reference has all fields as optional """
input core_user_user_reference {
  """
  Internal database id of the user.
  """
  id: core_id
  """
  Username of the user.
  """
  username: String
  """
  The id number of the user.
  """
  idnumber: String
  """
  Email address of the user.
  """
  email: String
}

The user records in the system will be filtered by the fields that are passed, and if a single record can be identified it will be used. If zero records or more than one record are found then an error will be returned.

In some cases, additional filtering may be applied to remove unwanted records from the system. For example, a tenant user will never see users from another tenant, and deleted users are also excluded.

Errors

See Using GraphQL APIs for details on response codes and the structure of responses, including when errors occur. The amount of information in error responses will depend on error-level configuration; see Enabling debugging in GraphQL APIs.