GraphQL Data Specification

This document lays out GraphQL schema design principles & standards for building data APIs.

This is ideal for GraphQL or federated GraphQL schemas that need to:

  1. Provide a standardized API
  2. Provide a composable and fleixble design
  3. Balance domain-driven and consumer-driven design
  4. Support lightweight and high-velocity iterations on schema design

1Schema design principles

When building a GraphQL schema, the schema design process should follow the following principles:

  1. The GraphQL schema design process allows multiple stakeholders to own parts of the schema.
  2. Domain owners design their part of the GraphQL schema in a way that accurately reflects the domain. These might require keeping in mind the needs of multiple API consumers domain driven design.
  3. Consumers should be able to drive independent additions to the GraphQL schema that represent consumer specific needs.
  4. Any entity in the GraphQL schema should either represent a resource, a unit of data, or a method, a unit of business logic.

2Objects

Every data object in the described domain, e.g., a User, an Order, or a Product in an e-commerce application, has a GraphQL type.

2.1Example

type User {
  user_id: Int!
  name: String
}

3Global ID

An id field in the object can represent a globally unique id for the object. This allows the Object type to implement the Relay Node interface.

3.1Example

type User implements Node {
  id: ID!
  user_id: Int!
  name: String
}

4Selection (Models)

Models are collections of objects that can be queried in standardized ways and represent standardized selection, query or read operations.

4.1Single Object Selection

If one or more fields can uniquely identify an object in a model, a root field should allow querying the object using those fields.

4.1.1Example

type Query {
  user_by_id(user_id: Int!): User
}

4.2Multi Object Selection

To fetch multiple objects, a root field should return a list of objects.

4.2.1Example

type Query {
  users: [User]
}

By default semantics, this should return all the objects in the model. Additional input paramenters can control the set of objects returned by the model.

5Filtering

A where input parameter for a model allows querying a model based on some condition on one or more of its fields. This includes any relationship fields that have been defined on the object.

A particular field will support specific comparison operators. The where input paramenter should support operators for AND / OR / NOT to compose a filter expression.

5.1Example

type Query {
  users(where: User_boolean_exp): [User]
}

input User_boolean_exp {
  _and: [User_boolean_exp!]
  _or: [User_boolean_exp!]
  _not: User_boolean_exp
  user_id: Int_comparison_exp
  first_name: String_comparison_exp
  last_name: String_comparison_exp
  email: String_comparison_exp
}

input Int_comparison_exp {
  _eq: Int
  _lt: Int
  _gt: Int
  _lte: Int
  _gte: Int
  _in: [Int!]
}

input String_comparison_exp {
  _eq: String
  _in: [Int!]
  _like: String
}

5.2Filter expression grammar

FieldComparisonExpression
FieldOperatorUserInputValue
Field
ObjectField1
ObjectField2
ObjectField3
...

6Sorting

OpenDD can generate an input for sorting the objects when querying a model based on one or more of its fields. This includes any relationship fields that have been defined on the object. Multiple fields can be used as the sort key, with the order of specification determining which one gets applied first.

6.1Example

type Query {
  users(where: [User_order_by_exp]): [User]
}

input User_order_by_exp {
  user_id: OrderByDirection
  first_name: OrderByDirection
  last_name: OrderByDirection
  email: OrderByDirection
}

enum OrderByDirection {
  Asc,
  Desc
}

7Pagination

OpenDD can generate input arguments for paginating through the objects returned when querying a model.

7.1Example

type Query {
  users(limit: Int, offset: Int): [User]
}

8Aggregation

TODO: Add after v3 supports aggregation.

9Relay

The GraphQL API generated by OpenDD implements the Relay API and has a node query root field that can be used to retrieve specific objects using their global ID.

9.1Example

type Query {
  node(id: ID!): Node
}

10Commands

Commands are functions which take in some arguments and produce an output. The semantics of commands except for arguments and output are opaque.

Commands can be made available for invocation either at the query root or the mutation root of the command GraphQL API.

10.1Example

type Mutation {
  validateDiscountCoupon(coupon_code: String): bool
}

11Relationships

Objects can be augmented with related information by defining a relationship from the object to a model or a command. This way, queries can be flexibly composed.

Creating a relationship adds a relationship field to the generated object type which can be used to query the related model or command.

11.1Example

type Order {
  order_id: Int!
  product_id: Int!
  user_id: Int!
  user: User // relationship from Order.user_id to User.user_id
}

12Query Composition

For a has-many (array) relationship, the input parameters of multi-object model selection are available to use, so queries across relationships can be flexibly composed.

12.1Example

If there is a has-many relationship from User to orders and a has-one (object) relationship from Order to users:

type User {
  user_id: Int!
  name: String
  orders(
    where: Orders_bool_exp,
    order_by: Orders_order_by_exp,
    limit: Int,
    offset: Int
  ): [Order]
}

Because relationships, filtering, and sorting are semantically meaningful in OpenDD, this allows for efficiently issuing such composable queries without resorting to N+1 database queries, even when the two related models come from different databases.

12.2Example

The following query results in only 2 database lookups, one for users and one for orders, as opposed to 11 lookups if resolving traditionally. Such efficient composition is impossible to achieve with generic resolver-based approaches even when using data loaders.

query {
  users(limit: 10) {
    user_id
    orders(where: { total: { _gt: 100 } }, order_by: { order_date: DESC }) {
      order_id
      product_id
    }
  }
}

13Filtering / Sorting Composition

Relationship fields themselves can be used for filtering, sorting, and aggregating a model containing the source object.

13.1Examples

To query all users who ordered a particular product where orders are not a part of the users model:

query {
  users(where: { orders: { product_id: { _eq: "product_1" } } }) {
    user_id
  }
}

To sort all orders of a particular product by the email of the user that placed the order, where the user email is not a part of the orders model:

query {
  orders(where: { product_id: { _eq: 1 } }, order_by: { user: { email: ASC } }) {
    order_id
  }
}

This is possible since OpenDD has a semantic understanding of relationships and predicates. Doing this would be impossible in generic resolver-based approaches.

§Index

  1. Field
  2. FieldComparisonExpression
  3. ModelBooleanExpression
  4. Where
  1. 1Schema design principles
  2. 2Objects
    1. 2.1Example
  3. 3Global ID
    1. 3.1Example
  4. 4Selection (Models)
    1. 4.1Single Object Selection
      1. 4.1.1Example
    2. 4.2Multi Object Selection
      1. 4.2.1Example
  5. 5Filtering
    1. 5.1Example
    2. 5.2Filter expression grammar
  6. 6Sorting
    1. 6.1Example
  7. 7Pagination
    1. 7.1Example
  8. 8Aggregation
  9. 9Relay
    1. 9.1Example
  10. 10Commands
    1. 10.1Example
  11. 11Relationships
    1. 11.1Example
  12. 12Query Composition
    1. 12.1Example
    2. 12.2Example
  13. 13Filtering / Sorting Composition
    1. 13.1Examples
  14. §Index