Implementing GraphQL services
This section describes how to create new GraphQL services within Totara as a developer. As a prerequisite, you should familiarise yourself with the core concepts of GraphQL by working through the GraphQL tutorial first. You may also be interested in Using our APIs.
When implementing GraphQL services, there are a number of number of concepts and files which are needed. This document describes each of them and explains how they fit together. There are five main file locations, three of which use the GraphQL language, and two of which are in PHP:
- Query definitions (GraphQL schema)
- Type definitions (GraphQL schema)
- Persisted queries (GraphQL)
- Query and mutation resolvers (PHP)
- Type resolvers (PHP)
In addition, resolvers can make use of middleware to abstract common code and apply it to multiple resolvers.
Query definitions
Query definitions describe the query entry points into the GraphQL schema. You could think of each query as being like a specific service that an API user can call on. For a request to be successful, the top level of any GraphQL query has to match a valid query definition in the schema. Query definitions have a name, possibly some required and/or optional arguments, and they return a Type. These top-level queries specify what items are returned, but don't specify the content of each item other than the type of the data. The content itself is determined further down the chain by the Type resolvers.
File locations
lib/webapi/*.graphqls - query definitions for all core code (including core components) that will apply to all endpoint types.
lib/webapi/$endpoint_type/*.graphqls - query definitions for core code (including core components) that will apply to a specific endpoint type.
plugindir/webapi/*.graphqls - query definitions for plugins to extend core schema that will apply to all endpoint types.
plugindir/webapi/$endpoint_type/*.graphqls - query definitions for plugins to extend core schema that will apply to a specific endpoint type.
Note there can be multiple files which are merged together to help with schema organisation, but if there is only one file, our convention is to call it schema.graphqls.
Example structure
In the primary schema file lib/webapi/schema.graphqls the structure is:
type Query { core_template(name: param_alphanumext!, component: param_component!, theme: param_theme!) : String! }
In any other *.graphql files, when extending core, the structure is:
extend type Query { totara_job_my_assignments: [totara_job_assignment!]! }
Note that GraphQL does not support namespaces, so in order to avoid collisions and identify the correct resolver, the query definition name is expected to be in the format {$component}_{$queryresolverclassname}.
Type definitions
Type definitions are also stored in the schema files. While their structure is essentially identical to query definitions (technically the top level Query is a type), they are handled differently when a query is actually run, so the distinction is important. Types contain fields which each have their own type. Types eventually have to resolve down to scalar values (meaning a single non-complex value is returned).
File locations
lib/webapi/*.graphqls - type definitions for all core code that will apply to all endpoint types.
lib/webapi/$endpoint_type/*.graphqls - type definitions for all core code that will apply to a specific endpoint type.
plugindir/webapi/*.graphqls - type definitions for plugins to extend core schema that will apply to all endpoint types.
plugindir/webapi/$endpoint_type/*.graphqls - type definitions for plugins to extend core schema that will apply to a specific endpoint type.
Example structure
In the primary schema file lib/webapi/schema.graphqls the structure is:
type core_lang_string { lang: String!, identifier: String! component: String! string: String! }
In plugin *.graphqls files they look the same:
type totara_hierarchy_position_type { id: core_id! fullname(format: core_format = HTML) : String! idnumber : String shortname(format: core_format = HTML) : String description(format: core_format = HTML) : String }
Type definition names are expected to be in the format {$component}_{$typeresolverclassname}.
Persisted queries
Persisted queries are the last concept that uses the GraphQL language. While the concepts above define the GraphQL schema that the server makes available, persisted queries provide a way to request data from the server. Persisted queries make use of a query definition, but specify exactly which fields you want to get back. They can optionally have variables for populating parameters to be passed to the query. At Totara we've made the call to require persisted queries for the AJAX and mobile endpoints, which means the front end can't make arbitrary queries, you have to create a persisted query in a file and reference that. This makes the front-end code simpler and also more secure, as users can't execute arbitrary queries. On the other hand, the developer and external endpoints use arbitrary queries, so when using them you need to specify the full query definition.
Persisted queries start from a top-level query definition, then specify which fields they want. They can be nested to return deep hierarchies of data by taking adv