GraphQL server (/graphql)#

What is GraphQL?#

From graphql.org:

GraphQL is a query language for APIs and a runtime for fulfilling those queries. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more.

Features:

  • Ask for what you need, get exactly that.

  • Get many resources in a single request.

  • Describe what’s possible with a clear schema.

Why GraphQL?#

GitHub provided a very concise blog of why they switched to GraphQL: https://github.blog/2016-09-14-the-github-graphql-api/

GraphQL represents a massive leap forward for API development. Type safety, introspection, generated documentation, and predictable responses benefit both the maintainers and consumers of our platform.

More so than this, GraphQL maps very well with the data structure of an AiiDA profile, and makes it very intuitive for clients to construct complex queries, for example:

{
  aiidaVersion
  aiidaEntryPointGroups
  nodes(filters: "node_type LIKE '%Calc%' & mtime >= 2018-02-01") {
    count
    rows(limit: 10, offset: 10) {
      uuid
      node_type
      mtime
      incoming {
        count
      }
      outgoing {
        count
      }
    }
  }
}

The GraphQL schema#

The current Graphql schema is:

schema {
  query: RootQuery
}

"""The root query"""
type RootQuery {
  """Maximum number of entity rows allowed to be returned from a query"""
  rowLimitMax: Int

  """Version of aiida-core"""
  aiidaVersion: String

  """Query for a single Comment"""
  comment(id: Int, uuid: String): CommentQuery

  """Query for multiple Comments"""
  comments(filters: FilterString): CommentsQuery

  """Query for a single Log"""
  log(id: Int, uuid: String): LogQuery

  """Query for multiple Logs"""
  logs(filters: FilterString): LogsQuery

  """Query for a single Node"""
  node(id: Int, uuid: String): NodeQuery

  """Query for multiple Nodes"""
  nodes(filters: FilterString): NodesQuery

  """Query for a single Computer"""
  computer(id: Int, uuid: String): ComputerQuery

  """Query for multiple Computers"""
  computers(filters: FilterString): ComputersQuery

  """List of the entrypoint group names"""
  aiidaEntryPointGroups: [String]

  """List of the entrypoint names in a group"""
  aiidaEntryPoints(group: String!): EntryPoints

  """Query for a single Group"""
  group(id: Int, uuid: String, label: String): GroupQuery

  """Query for multiple Groups"""
  groups(filters: FilterString): GroupsQuery

  """Query for a single User"""
  user(id: Int, email: String): UserQuery

  """Query for multiple Users"""
  users(filters: FilterString): UsersQuery
}

"""Query an AiiDA Comment"""
type CommentQuery {
  """Unique id (pk)"""
  id: Int

  """Universally unique id"""
  uuid: ID

  """Creation time"""
  ctime: DateTime

  """Last modification time"""
  mtime: DateTime

  """Content of the comment"""
  content: String

  """Created by user id (pk)"""
  user_id: Int

  """Associated node id (pk)"""
  dbnode_id: Int
}

"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar DateTime

"""Query all AiiDA Comments."""
type CommentsQuery {
  """Total number of rows of comments"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [CommentQuery]
}

"""A string adhering to the AiiDA filter syntax."""
scalar FilterString

"""Query an AiiDA Log"""
type LogQuery {
  """Unique id (pk)"""
  id: Int

  """Universally unique id"""
  uuid: ID

  """Creation time"""
  time: DateTime

  """The loggers name"""
  loggername: String

  """The log level"""
  levelname: String

  """The log message"""
  message: String

  """Metadata associated with the log"""
  metadata: JSON

  """Associated node id (pk)"""
  dbnode_id: Int
}

"""
Custom scalar type for JSON values that could be:
String, Boolean, Int, Float, List or Object.
"""
scalar JSON

"""Query all AiiDA Logs."""
type LogsQuery {
  """Total number of rows of logs"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [LogQuery]
}

"""Query an AiiDA Node"""
type NodeQuery {
  """Unique id (pk)"""
  id: Int

  """Universally unique id"""
  uuid: ID

  """Node type"""
  node_type: String

  """Process type"""
  process_type: String

  """Label of node"""
  label: String

  """Description of node"""
  description: String

  """Creation time"""
  ctime: DateTime

  """Last modification time"""
  mtime: DateTime

  """Created by user id (pk)"""
  user_id: Int

  """Associated computer id (pk)"""
  dbcomputer_id: Int

  """Variable attributes of the node"""
  attributes(
    """return an exact set of attributes keys (non-existent will return null)"""
    filter: [String]
  ): JSON

  """Variable extras (unsealed) of the node"""
  extras(
    """return an exact set of extras keys (non-existent will return null)"""
    filter: [String]
  ): JSON

  """Comments attached to a node"""
  comments: CommentsQuery

  """Logs attached to a process node"""
  logs: LogsQuery

  """Query for incoming nodes"""
  incoming(filters: FilterString): LinksQuery

  """Query for outgoing nodes"""
  outgoing(filters: FilterString): LinksQuery

  """Query for ancestor nodes"""
  ancestors(filters: FilterString): NodesQuery

  """Query for descendant nodes"""
  descendants(filters: FilterString): NodesQuery
}

"""Query all AiiDA Links."""
type LinksQuery {
  """Total number of rows of nodes"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [LinkQuery]
}

"""A link and its end node."""
type LinkQuery {
  link: LinkObjectType
  node: NodeQuery
}

type LinkObjectType {
  """Unique id (pk)"""
  id: Int

  """Unique id (pk) of the input node"""
  input_id: Int

  """Unique id (pk) of the output node"""
  output_id: Int

  """The label of the link"""
  label: String

  """The type of link"""
  type: String
}

"""Query all AiiDA Nodes"""
type NodesQuery {
  """Total number of rows of nodes"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [NodeQuery]
}

"""Query an AiiDA Computer"""
type ComputerQuery {
  """Unique id (pk)"""
  id: Int

  """Universally unique id"""
  uuid: ID

  """Computer name"""
  label: String

  """Identifier for the computer within the network"""
  hostname: String

  """Description of the computer"""
  description: String

  """Scheduler plugin type, to manage compute jobs"""
  scheduler_type: String

  """Transport plugin type, to manage file transfers"""
  transport_type: String

  """Metadata of the computer"""
  metadata: JSON
  nodes(filters: FilterString): NodesQuery
}

"""Query all AiiDA Computers"""
type ComputersQuery {
  """Total number of rows of computers"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [ComputerQuery]
}

"""
Return type from an entry point group and its list of registered names.
"""
type EntryPoints {
  group: String
  names: [String]
}

"""Query an AiiDA Group"""
type GroupQuery {
  """Unique id (pk)"""
  id: Int

  """Universally unique id"""
  uuid: ID

  """Label of group"""
  label: String

  """type of the group"""
  type_string: String

  """Created time"""
  time: DateTime

  """Description of group"""
  description: String

  """extra data about for the group"""
  extras: JSON

  """Created by user id (pk)"""
  user_id: Int
  nodes: NodesQuery
}

"""Query all AiiDA Groups"""
type GroupsQuery {
  """Total number of rows of groups"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [GroupQuery]
}

"""Query an AiiDA User"""
type UserQuery {
  """Unique id (pk)"""
  id: Int

  """Email address of the user"""
  email: String

  """First name of the user"""
  first_name: String

  """Last name of the user"""
  last_name: String

  """Host institution or workplace of the user"""
  institution: String
  nodes(filters: FilterString): NodesQuery
}

"""Query all AiiDA Users"""
type UsersQuery {
  """Total number of rows of users"""
  count: Int
  rows(
    """Maximum number of rows to return (no more than 100)"""
    limit: Int = 100

    """Skip the first n rows"""
    offset: Int = 0

    """Field to order rows by"""
    orderBy: String = "id"

    """Sort field in ascending order, else descending."""
    orderAsc: Boolean = true
  ): [UserQuery]
}

Data Limits and Pagination#

The maximum number of rows of data returned is limited. To query this limit use:

{ rowLimitMax }

Use the offset option in conjunction with limit in order to retrieve all the rows of data over multiple requests. For example, for pages of length 50:

Page 1:

{
  nodes {
    count
    rows(limit: 50, offset: 0) {
      attributes
    }
  }
}

Page 2:

{
  nodes {
    count
    rows(limit: 50, offset: 50) {
      attributes
    }
  }
}

Filtering#

The filters option for computers, comments, groups, logs, nodes, and users, accepts a FilterString, which maps a string to the filters input of the QueryBuilder (see the reference table for more information).

For example:

{ nodes(filters: "node_type ILIKE '%Calc%' & mtime >= 2018-02-01") { count } }

maps to:

QueryBuilder().append(Node, filters={"node_type": {"ilike": "%Calc%"}, "mtime": {">=": datetime(2018, 2, 1, 0, 0)}}).count()

The syntax is defined by the following EBNF Grammar:

Query Plugins#

All top-level queries are plugins.

A plugin is defined as a QueryPlugin object, which simply includes three items:

  • name: The name by which to call the query

  • field: The graphene field to return (see graphene types reference)

  • resolver: The function that resolves the field.

For example:

from aiida_restapi.graphql.plugins import QueryPlugin
import graphene as gr

def resolver(parent, info):
  return "halloworld!"

myplugin = QueryPlugin(
  name="myQuery",
  field=gr.String(description="Return some data"),
  resolver=resolver
)

Would be called like:

{ myQuery }

and return:

{
  "myQuery": "halloworld!"
}

(TODO: loading plugins as entry points)

REST Migration Guide#

This section helps AiiDA users migrate API calls between the REST API built into aiida-core and the GraphQL API of this plugin.

Most of the listed calls are taken from the aiida-core documentation.

General#

http://localhost:5000/api/v4/server/endpoints
http://localhost:5000/graphql

It is important to note that (in contrast to REST) with GraphQL

  • you select the fields you want to retrieve, and

  • you can combine multiple queries in one request.

Nodes#

http://localhost:5000/api/v4/nodes?id=in=45,56,78
{
  nodes(filters: "id IN 45,56,78") {
    count
    rows {
      id
    }
  }
}
http://localhost:5000/api/v4/nodes?limit=2&offset=8&orderby=-id
{
  nodes {
    rows(limit: 2, offset: 8, orderBy: "id", orderAsc: false) {
      id
    }
  }
}
http://localhost:5000/api/v4/nodes?attributes=true&attributes_filter=pbc1
{
  nodes {
    rows {
      attributes(filter: ["pbc1"])
    }
  }
}
http://localhost:5000/api/v4/nodes/full_types

NOT YET SPECIFICALLY IMPLEMENTED (although this needs further investigation, because full types is basically not documented anywhere)

http://localhost:5000/api/v4/nodes/download_formats

Not implemented for GraphQL, please use the REST API for this use case.

http://localhost:5000/api/v4/nodes/12f95e1c
{ node(uuid: "dee1f869-c45e-40d9-9f9c-f492f4117f13") { uuid } }

Partial UUIDs are not yet implemented (but you can also select using id).

http://localhost:5000/api/v4/nodes/de83b1/links/incoming?limit=2
{
  node(id: 1011) {
    incoming {
      rows(limit: 2) {
        link {
          label
          type
        }
        node {
          id
          label
        }
      }
    }
  }
}
http://localhost:5000/api/v4/nodes/de83b1/links/incoming?full_type="data.dict.Dict.|"
{
  node(id: 1011) {
    incoming(filters: "node_type == 'data.dict.Dict.'") {
      count
      rows {
        link {
          label
          type
        }
        node {
          id
          label
        }
      }
    }
  }
}
http://localhost:5000/api/v4/nodes/a67fba41/links/outgoing?full_type="data.dict.Dict.|"
{
  node(id: 1011) {
    outgoing(filters: "node_type == 'data.dict.Dict.'") {
      count
      rows {
        link {
          label
          type
        }
        node {
          id
          label
        }
      }
    }
  }
}
http://localhost:5000/api/v4/nodes/ffe11/contents/attributes
{ node(uuid: "dee1f869-c45e-40d9-9f9c-f492f4117f13") { attributes } }
http://localhost:5000/api/v4/nodes/ffe11/contents/attributes?attributes_filter=append_text,is_local
{ node(uuid: "dee1f869-c45e-40d9-9f9c-f492f4117f13") { attributes(filter: ["append_text", "is_local"]) } }
http://localhost:5000/api/v4/nodes/ffe11/contents/comments
{
  node(id: 1011) {
    comments {
      count
      rows {
        content
      }
    }
  }
}

Repository based queries are not yet implemented:

http://localhost:5000/api/v4/nodes/ffe11/repo/list
http://localhost:5000/api/v4/nodes/ffe11/repo/contents?filename="aiida.in"

Not implemented for GraphQL, please use the REST API for this use case.

http://localhost:5000/api/v4/nodes/fafdsf/download?download_format=xsf
http://localhost:5000/api/v4/nodes?mtime>=2019-04-23
{
  nodes(filters: "mtime>=2019-04-23") {
    count
    rows {
        uuid
    }
  }
}

Processes#

NOT YET IMPLEMENTED

http://localhost:5000/api/v4/processes/8b95cd85/report
http://localhost:5000/api/v4/calcjobs/sffs241j/input_files

Computers#

http://localhost:5000/api/v4/computers?limit=3&offset=2&orderby=id
{
  computers {
    count
    rows(limit: 3, offset: 3, orderBy: "id") {
      id
    }
}
http://localhost:5000/api/v4/computers/5d490d77
{
  computer(uuid: "3d09ebd4-4bda-44c1-86c3-530a778911d5") {
    label
  }
}

Partial UUIDs are not yet implemented (but you can also select using id).

http://localhost:5000/api/v4/computers/?scheduler_type=in="slurm","pbs"
{
  computers(filters: "scheduler_type IN slurm,pbs") {
    count
    rows {
      scheduler_type
    }
  }
}
http://localhost:5000/api/v4/computers?orderby=+name
{
  computers {
    rows(orderBy: "name") {
      id
    }
  }
}
http://localhost:5000/api/v4/computers/page/1?perpage=5
{
  computers {
    rows(limit: 5) {
      id
    }
  }
}

Users#

http://localhost:5000/api/v4/users/
{
  users {
    count
    rows {
      id
    }
  }
}
http://localhost:5000/api/v4/users/?first_name=ilike="aii%"
{
  users(filters: "first_name ILIKE 'aii%'") {
    count
    rows {
      email
      first_name
      last_name
      institution
    }
  }
}

Groups#

http://localhost:5000/api/v4/groups/?limit=10&orderby=-user_id
{
  groups {
    count
    rows(limit: 10, orderBy: "user_id", orderAsc: false) {
      id
    }
  }
}
http://localhost:5000/api/v4/groups/a6e5b
{
  group(uuid: "3d09ebd4-4bda-44c1-86c3-530a778911d5") {
    id
    label
    nodes {
      count
    }
  }
}

Partial UUIDs are not yet implemented (but you can also select using id).