graphql icon indicating copy to clipboard operation
graphql copied to clipboard

🍇 Thinkmill's GraphQL Style Guide

Thinkmill GraphQL Style Guide

This style guide documents the standards we have developed for designing GraphQL Schemas at Thinkmill.

  • Concepts
  • Workflow and Process (GraphQL as Design System equivalent)
    • https://twitter.com/JedWatson/status/1170867029659791366
    • GraphQL as ideal abstraction layer for the business schema
  • Types
    • Schema Overview
    • See Schemas and Types in the GraphQL Spec
    • Field type conventions and use-cases
      • ID (Scalar)
      • String (Scalar)
      • Boolean (Scalar)
      • Number (Int or Float)
      • Enum (Not quite a scalar type)
      • Date (Not built in)
    • Relationships
  • Queries
    • Standard Arguments
    • Filtering
    • Pagination
    • Sorting
    • Recursion of patterns/Nest Queries(?)
  • Mutations
  • Custom Types/Queries/Mutations (non-CRUD)
  • Authentication
  • Validation and Error Handling
  • Caching
  • Permissions

Concepts

In order to keep the style guide implementation-agnostic, we refer to entities, entity types and entity collections.

In an SQL-backed implementation, the entity type for a User would be the schema; the entity collection would be the users table and an entity would be a record in the table.

Each entity type has a singular and plural label, which we refer to as {Entity} and {Entities} in this style guide. Each entity also contains fields, which map to properties of the entity.

We call entity properties fields, which have field types. Field types map to other entity types or scalar GraphQL Types.

Throughout the guide, we will use the following example schema:

scalar DateTime

enum PriorityEnum {
  LOW
  MEDIUM
  HIGH
}

type Todo {
  id: ID!
  name: String
  priority: PriorityEnum
  dueDate: DateTime
  user: User
}

type User {
  id: ID!
  name: String
  age: Int
  height: Float
  todos: [Todo]
}

Query {
  # Get one todo item
  Todo(id: ID!): Todo
  # Get all todo items
  allTodos: [Todo!]!
}

Mutation {
  addTodo(name: String!, priority: Priority = LOW): Todo!
  removeTodo(id: ID!): Todo!
}
query {
  allTodos {
    name
    users(first: 10) {
      name
    }
  }
}

Workflow and Process

// TODO

  • How do you come up with a schema?
  • System requirements -> schema
  • Identify the queries/mutations you need and use this to inform the data schema
  • How do we get UX designers to think about the queries they need?
  • A graphQL schema can be easily related across the development spectrum
  • Given a UX design, how do you identify the schema requirements (in the same way you'd identify the design system requirements)

Queries

For each entity type we generate the four top level queries:

  • all{Entities}
  • {Entity}
  • _all{Entities}Meta
  • _{Entity}Meta

{Entity}

query {
  User(where: { id: ID }) {
    name
  }
}

Retrieves an entity from a collection. The single entity query has one argument.

all{Entities}

query {
  allUsers {
    id
  }
}

Retrieves all entities from a collection.

_all{Entities}Meta

query {
  _allUsersMeta {
    count
  }
}

Get the total number of entities from a collection.

_{Entity}Meta

// TODO: This should probably be taken out of the spec

query {
  _UserMeta(where: { id: ID }) {
    count
  }
}

Filtering

These apply to queries for the all{Entities} and {all}EntitiesMeta types

where

query {
    allUsers (where: { id_starts_with_i: 'A'} ) {
        id
    }
}

Limit results to those matching the where clause. Where clauses generated will depend on the field types available.

Operators

// TODO: Where does this section belong?

  • AND: [UserWhereInput]
  • OR: [UserWhereInput]

search

Need to consider whether this should be included or whether it's just a KeystoneJS specific thing

Will search the entity collection to limit results. Search logic is defined by the entity type.

query {
  allUsers(search: "Mike") {
    id
  }
}

Sorting

orderBy

query {
  allUsers(orderBy: "name_ASC") {
    id
  }
}

Ascending vs Descending?

Will order the results. The orderBy string should match a field name in the entity collection.

Pagination

first

query {
  allUsers(first: 10) {
    id
  }
}

Select this many results from the entity collection, sorted by the orderBy argument and matching the where and search values. Works like specifying TOP in SQL.

If less results are available, the number of available results will be returned.

skip

query {
  allUsers(skip: 10) {
    id
  }
}

Skip the number of results specified, before returning the number of results specified by the first argument, sorted by the orderBy argument and matching the where and search values.

If the value of skip is greater than the number of available results, zero results will be returned.

Combining query arguments for pagination

When first and skip are used together with the count from _all{Entities}Meta, this is sufficient to implement pagination of an entity collection.

It is important to provide the same where and search arguments to both the all{Entities} and _all{Entities}Meta queries. For example:

query {
  allUsers (search:'a', skip: 10, first: 10) {
    id
  }
  _allUsersMeta(search: 'a') {
    count
  }
}

When first and skip are used together, skip works as an offset for the first argument. For example(skip:10, first:10) selects results 11 through 20.

Both skip and first respect the values of the where, search and orderBy arguments.

Field types

ID

  • {Field}: ID
  • {Field}_not: ID
  • {Field}_in: [ID!]
  • {Field}_not_in: [ID!]

String

  • {Field}: String
  • {Field}_not: String
  • {Field}_contains: String
  • {Field}_not_contains: String
  • {Field}_starts_with: String
  • {Field}_not_starts_with: String
  • {Field}_ends_with: String
  • {Field}_not_ends_with: String
  • {Field}_i: String
  • {Field}_not_i: String
  • {Field}_contains_i: String
  • {Field}_not_contains_i: String
  • {Field}_starts_with_i: String
  • {Field}_not_starts_with_i: String
  • {Field}_ends_with_i: String
  • {Field}_not_ends_with_i: String
  • {Field}_in: [String]
  • {Field}_not_in: [String]

Number

  • {Field}: Int
  • {Field}_not: Int
  • {Field}_lt: Int
  • {Field}_lte: Int
  • {Field}_gt: Int
  • {Field}_gte: Int
  • {Field}_in: [Int]
  • {Field}_not_in: [Int]

Boolean

// TODO

Enum

// TODO

Date

// TODO

Mutations

// TODO