Extending GraphQL APIs

Implementing new services

Extending schema

Any plugin in Totara can create its own GraphQL schema that is combined by the schema loader into a single schema for use. See the main documentation for information about Implementing GraphQL services.

Some limitations and guidelines on schema definitions are outlined here.

Use of component name prefix

When defining new schema properties, always prefix the name with the 'frankenstyle' component name to namespace the schema in order to avoid naming collisions.

For example, an activity module called 'test' would prefix with 'mod_test_', whereas a block called 'example' would prefix with 'block_example_'.

You can define your own object types, input types, scalars and enums directly:

type mod_test_mytype {
  id: core_id!
  my_obj: mod_test_my_sub_type!
}

type mod_test_my_sub_type {
  abc: String!
  def: Int!
}

input mod_test_my_input_type {
  id: core_id!
}

enum mod_test_my_enum {
  VALUE1
  VALUE2
  VALUE3
}

You can also add queries and mutations by extending the Query and Mutation types:

extend type Query {
  mod_test_my_query(input: mod_test_my_input_type!): mod_test_mytype!
}

extend type Mutation {
  mod_test_my_mutation(name: String!): Int!
}

Note that while GraphQL does support extending object types via the extend keyword, it does not behave like normal class inheritance. You are not creating a new type that inherits the original, but actually modifying the original type with more properties, so normally that's not what you want.

Therefore, you will typically need to duplicate the schema if you want to create a slightly modified version of an existing type.

Creating resolvers

You will need to implement the resolver class for any type you define in your plugin's GraphQL schema. See Implementing GraphQL services for information about resolver file locations.

For a new resolver, extend the appropriate core class, typically one of:

  • core\webapi\type_resolver
  • core\webapi\query_resolver
  • core\webapi\mutation_resolver

Extending resolvers

Alternatively, if you want very similar behaviour to an existing resolver, you can extend it and override any behaviour you want:

class custom_user_resolver extends core\webapi\resolver\type\user {
    public static function resolve(string $field, $user, array $args, execution_context $ec) {
        if ($field === 'my_custom_field') {
            return 'my_custom_value';
        }
        // Call the parent to handle any fields we don't override.
        return parent::resolve($field, $user, $args, $ec);
	}
}

Global middleware

Normally middleware is assigned via the get_middleware() method in resolver classes, which controls when middleware is loaded. In Totara 17 we added a new type of middleware called global middleware, which can be assigned to apply to all resolvers of a specific endpoint type. This allows you to ensure that your middleware is always executed on all requests to a specific endpoint.

Global middleware is initially declared in the get_middleware() method within the endpoint_type class (see the server/totara/webapi/classes/endpoint_type/ folder).

Note that global middleware is executed for query, mutation AND type resolvers.

Be aware that middleware that is applied to type resolvers will be called every time a field is resolved, which means it can be executed many times per request. You may want to implement a request cache or exit early from your resolvers in some situations if this is not what you want.

Global middleware hook

The API code defines a hook totara_webapi\hook\api_hook which allows any plugin to watch it and modify the structure of the global middleware. The hook provides access to the endpoint_type, component and resolver, so middleware can be inserted or modified based on the values of those items.

See Hooks developer documentation for general information about hooks and hook watchers.

A simple example of a watcher that would add an additional middleware class might be:

namespace local_example\watcher;

use totara_webapi\hook\api_hook;

class example_watcher {

    /**
     * @param api_hook $api_hook
     * @return void
     */
    public static function watch(api_hook $api_hook) {
        // You can make your changes conditional on info available in the hook, for example:
        $component = $api_hook->component;				// Example: string 'mod_perform' or 'core'
        $resolver = $api_hook->resolver;				// Example: string 'core\webapi\resolver\query\user_users'
        $endpoint_type = $api_hook->endpoint_type;		// Example: 'totara_webapi\endpoint_type\external' object

        $current_middleware = $api_hook->middleware;

        $api_hook->middleware = array_merge($current_middleware, local_example_custom_middleware::class);
    }
}

Pre- and post- request hook

As of Totara 17, there are now pre- and post-request hooks, which execute near the start and end of GraphQL requests. They have access to the request object and the execution context. We use the pre-request hook to implement OAuth 2.0 authentication for the external API. See the watcher implementation in server/totara/api/classes/watcher/handle_request_pre_watcher.php  for an example of how this can be used.

If you want to implement a custom authentication mechanism instead of OAuth 2.0, you might want to disable that watcher. That can be done via a setting in config.php:

// Warning this will allow unauthenticated requests to the external API.
$CFG->forced_plugin_settings['totara_api'] = array(
     'disable_oauth2_authentication' =>  1
);

Note that if disabled, the request will no longer be allocated a user, so you will need to implement a separate watcher to set the user (using core\session\manager::set_user()) for any request where a user session is required. For other functionality (such as client rate-limiting middleware) to work, you may also need to set the client variable on execution_context via:

$execution_context->set_variable('client', $client);

Endpoint types

In Totara 17, GraphQL endpoints were migrated to use classes to define their behaviour. This minimises the core changes required by moving the behaviour of each endpoint type into its class. It should be possible to create new endpoint types if required, by extending the base endpoint_type class located in server/totara/webapi/classes/endpoint_type/. See the other classes in that folder for examples of how to define an endpoint type.

It would also be necessary to create a new PHP file to act as the actual endpoint file. This can make use of the new base api_controller class located in server/totara/webapi/classes/controllers/.

Normally it would not be necessary to create custom endpoints, unless you want to define different global middleware or otherwise change behaviour for some requests compared to others.