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:
- Provide a standardized API
- Provide a composable and fleixble design
- Balance domain-driven and consumer-driven design
- 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:
- The GraphQL schema design process allows multiple stakeholders to own parts of the schema.
- 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.
- Consumers should be able to drive independent additions to the GraphQL schema that represent consumer specific needs.
- 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
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.