Skip to main content

GraphQL specifics

Fire Arrow tries to follow the HL7 FHIR GraphQL spec as closely as possible. This section contains some implementation notes and specific additions to the official standard.

Queries

Queries are modeled after the entity names they are trying to access, for example:

  • Patient retrieves a single patient entity
  • PatientList retrieves a list of patients, filtered by search criteria
  • PatientConnection retrieves a list of patients, filtered by search criteria.

Patient only accepts the _id parameter, PatientList and PatientConnection support the search parameters for the Patient entity. Supported search parameters can be found at the bottom of each entity description, for example here. All queries support the standard search parameters.

The difference between PatientList and PatientConnection is pagination support. PatientList is aimed at retrieving a known small set of entities. Depending on the FHIR server, the list of results is limited to 100 or 1000 entities. PatientConnection has the same limit but returns a search cursor for each returned page that refers to the next and/or previous page.

Mutations

Mutations are again modeled after the corresponding entity names:

  • PatientCreate creates a patient entity.
  • PatientUpdate updates a patient entity.
  • PatientDelete deletes a patient entity.

Delete operations deviate from the spec in that they return a reference to the deleted entity on success and null (and/or an exception) on failure. This has been done to let the client know if the operation was successful or not.

Resolving References

Fire Arrow adds the ability to resolve References "in place". If the client knows the referenced entity's type (either by specification or through convention), it can make use of the _resolvedResource property which immediately fetches the referenced entity.

Example:

query {
Patient(_id: "c6a5ef32-72b7-45e1-8faa-9e89807b5192") {
name {
given
family
}
managingOrganization {
reference
_resolvedResource {
... on Organization {
name
}
}
}
}
}

This will retrieve the name of the patient as well as the name of managing organization of the patient. Since managingOrganization is only available as Reference on Patient, retrieving the name of Organization would normally have to be done in a second, independent query. A client using _resolvedResource can perform both in a single query.

_resolvedResource uses an independently validated request. Even if access is permitted on the parent resource, access may not be permitted to a reference resource. This ensures that clients that have access to one entity cannot accidentally use this entity as a backdoor to other entities by traversing referenced documents.

Search Parameters

FHIR allows specifying search parameters multiple times. GraphQL however only allows specifying a parameter once. This means that search behavior with Fire Arrow follows the following conventions:

  • To specify multiple search parameters, concatenate search values with the & character.
query {
ProcedureConnection(date: "ge2010-01-01&le2011-12-31") {
edges {
resource {
... on Procedure {
id
}
}
}
}
}
  • To use search modifiers, use the modifier at the beginning of the search string.
query {
ProcedureConnection(text: ":contains=cardiovascular") {
edges {
resource {
... on Procedure {
id
}
}
}
}
}

It is possible to specify multiple search modifiers or multiple search values for the same search parameter.

Search Cursors

If a client sends a search cursor for paginated search using a connection query, other query parameters will silently be ignored. This enforces consistency of search parameters between pages. The search cursor returned by Fire Arrow encodes both the backend server's cursor as well as any other parameters requested by the FHIR backend. Even if the FHIR backend would support dynamically changing the page size, Fire Arrow will prevent it.

Example:

query {
PatientConnection(_count: 2) {
count
offset
pageSize
first
next
last
edges {
mode
score
resource {
... on Patient {
id
name {
given
family
}
}
}
}
}
}

This query will generate a search cursor. The following query will retrieve the next page, enforcing a count value of 2:

query {
PatientConnection(_cursor: "MiZlcjk3ZjVsUlRiU2hnYkdPcWFHQm1ZRzVrYVc1c2FXRnBiR1pnWUdwYVN3QUFBRC8vdz09") {
count
offset
pageSize
first
next
last
edges {
mode
score
resource {
... on Patient {
name {
given
family
}
}
}
}
}
}

Custom Search Parameters

FHIR allows the definition of custom search parameters by creating SearchParameter resources on the FHIR server. Fire Arrow's GraphQL schema is statically typed and included at compile time, so it can't include server-specific search parameters into the schema.

To use custom search parameters, specify them via the _customSearchParameter parameter in list and connection queries:

query {
PatientList(_customSearchParameter: "insurance-status=insured") {
id
}
}

This will add the custom search parameter insurance-status with the value insured to the query and return corresponding results.

Inheritance Model

FHIR has an elaborate type hierarchy which is not directly compatible with GraphQL. In FHIR, any type can be inherited from and becomes a new type. In GraphQL, types can only inherit from interfaces and interfaces must always be abstract.

This for example makes the following hierarchy impossible:

  • Age, Duration, Count and Distance inherit from Quantity (requiring it to be an interface in GraphQL)
  • Quantity is also used directly in Extension.valueQuantity or Observation.valueQuantity (requiring it to be a type in GraphQL)

There are two potential solutions to the problem:

  1. Model interfaces as explicit interface types and have concrete types inherit the interface types. (bigger schema, additional types, more compliant typing)
  2. Ensure types have the same structure but don't explicitly follow the inheritance tree (smaller schema, type names are easier to follow)

Fire Arrow implements the latter approach, making all types conform to the FHIR specification by structure (meaning, they have the same properties with the same name and type) but not by inheritance. This approach may be reconsidered depending on experience in various deployments.

Applying PlanDefinitions

A PlanDefinition can be turned into a CarePlan resource by calling PlanDefinition.$apply. This operation is not implemented in all FHIR servers and is not well specified. Fire Arrow contains its own implementation that can be invoked through the PlanDefinitionApply mutation.

PlanDefinitionApply will construct a CarePlan either from a client-supplied PlanDefinition object or by referencing an existing PlanDefinition on the server.

The returned CarePlan object has the following specifics:

  • Each action in the PlanDefinition input will be turned into a RequestGroup with a single RequestGroupAction.
  • The period of the resulting CarePlan is set to start when PlanDefinitionApply was called. The end time will be null if none of the PlanDefinition's actions have a duration. If any action has a duration, the maximum duration will be used to calculate the CarePlan's end time. Dependencies between actions are not considered.

The mutation requires these prerequisites:

  • Clients must have the create permission on PlanDefinitionApply via the Allowed validator. Allowed is the only permissible validator for this request.
  • Clients must additionally have the read permission to the referenced PlanDefinition resource via any validator, or alternatively submit their own PlanDefinition resource.
  • Clients must have the read permission to the referenced subject resource.

The configuration parameter allow_client_to_apply_submitted_plandefinition controls if clients are allowed to submit PlanDefinition objects instead of referring to an object on the server. This is a security measure to prevent malicious clients to submit hand-crafted PlanDefinition objects for process-driven systems that rely on process integrity. When the setting is turned off, only PlanDefinition resources already contained on the server can be used to create a CarePlan through PlanDefinitionApply.

The configuration parameter remove_plan_definition_actions_without_definitions removes empty actions (actions that neither contain definitionCanonical nor definitionUri) during PlanDefinitionApply. This supports use cases where actions are only used for sequencing, such as determining the duration of the resulting CarePlan or determining timings of dependent actions.

tip

To ensure that CarePlans are only created through trusted PlanDefinition resources, turn allow_client_to_apply_submitted_plandefinition off and at the same time don't enable create and update privileges on CarePlan.

The configuration parameter propagate_plan_definition_extensions will copy all extensions in the PlanDefinition resource to the resulting CarePlan resource.

tip

Sometimes a PlanDefinition resource needs to contain important data that must be available later either to the backend and/or the client for processing. Examples include adherence bonuses for patients, treatment-specific parameters, etc. When implementing these as extensions in the PlanDefinition resource, propagage_plan_definition_extensions will make these available in the resulting CarePlan resource. This has the following benefits:

  • The client receives the configuration in the CarePlan immediately without having to look up the PlanDefinition.
  • PlanDefinition resources can remain hidden and don't have to be made discoverable to the client. (important if the totality of PlanDefinition resources contain sensitive information, such as different treatment options per country)
  • Backend services can retrieve version 1 by using a history query to discover the values that were set during PlanDefinitionApply, allowing even untrusted clients to modify the CarePlan without damaging sensitive treatment configurations.

Since this feature has some complexities to it, it shall be considered a beta feature at this point in time and some of its behaviors may change.