Data modeling - Lists & Fields
A Schema Definition (often abbreviated to 'Schema') is defined by
- a set of Lists
- containing one or more Fields
- which each have a Type
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
  },
});
Todo, containing a single Field task, with a Type of TextLists
You can create as many lists as your project needs:
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
  },
});
And each list can have as many fields as you need.
KeystoneJS will process each List, converting it into a series of GraphQL CRUD (Create, Read, Update, Delete) operations. For example, the above lists will generate;
type Mutation {
  createTodo(..): Todo
  updateTodo(..): Todo
  deleteTodo(..): Todo
  createUser(..): User
  updateUser(..): User
  deleteUser(..): User
}
type Query {
  allTodos(..): [Todo]
  Todo(..): Todo
  allUsers(..): [User]
  User(..): User
}
type Todo {
  id: ID
  task: String
}
type User {
  id: ID
  name: String
  email: String
}
(NOTE: Only a subset of all the generated types/mutations/queries are shown here. To see a more complete example follow the Quick Start.)
Customizing Lists & Fields
Both lists and fields can accept further options:
keystone.createList('Todo', {
  fields: {
    task: { type: Text, isRequired: true },
  },
  adminConfig: {
    defaultPageSize: 20,
  },
});
In this example, the adminConfig options will apply only to the Todo list
(setting how many items are shown per page in the Admin UI).
The isRequired option will ensure an API error
is thrown if a task value is not provided when creating/updating items.
For more List options, see the createList() API docs.
There are many different field types available, each specifying their own options.
Related Lists
One of KeystoneJS' most powerful features is defining Relationships between Lists.
Relationships are a special field type in KeystoneJS used to generate rich GraphQL operations and an intuitive Admin UI, especially useful for complex data modeling requirements.
Why Relationships?
Already know Relationships? Skip to Defining Relationships below.
To understand the power of Relationships, let's imagine a world without them:
keystone.createList('Todo', {
  fields: {
    task: { type: Text, isRequired: true },
    createdBy: { type: Text },
  },
});
In this example, every todo has a user it belongs to (the createdBy field). We
can query for all todos owned by a particular user, update the user, etc.
Let's imagine we have a single item in our Todo list:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Use KeystoneJS | Tici | 
We could query this data like so:
query {
  allTodos {
    task
    createdBy
  }
}
# output:
# {
#   allTodos: [
#     { task: 'Use KeystoneJS', createdBy: 'Tici' }
#   ]
# }
Everything looks great so far. Now, let's add another task:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Use KeystoneJS | Tici | 
| 2 | Setup linter | Tici | 
query {
  allTodos {
    task
    createdBy
  }
}
# output:
# {
#   allTodos: [
#     { task: 'Use KeystoneJS', createdBy: 'Tici' }
#     { task: 'Setup linter', createdBy: 'Tici' }
#   ]
# }
Still ok.
What if we add a new field:
keystone.createList('Todo', {
  fields: {
    task: { type: Text, isRequired: true },
    createdBy: { type: Text },
    email: { type: Text },
  },
});
Todo| id | task | createdBy | email | 
|---|---|---|---|
| 1 | Use KeystoneJS | Tici | tici@example.com | 
| 2 | Setup Linter | Tici | tici@example.com | 
query {
  allTodos {
    task
    createdBy
    email
  }
}
# output:
# {
#   allTodos: [
#     { task: 'Use KeystoneJS', createdBy: 'Tici', email: 'tici@example.com' }
#     { task: 'Setup linter', createdBy: 'Tici', email: 'tici@example.com' }
#   ]
# }
Now we're starting to see multiple sets of duplicated data (createdBy +
email are repeated). If we wanted to update the email field, we'd have to
find all items, change the value, and save it back. Not so bad with 2 items, but
what about 300? 10,000? It can be quite a big operation to make these changes.
We can avoid the duplicate data by moving it out into its own User list:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Use KeystoneJS | 1 | 
| 2 | Setup Linter | 1 | 
User| id | name | email | 
|---|---|---|
| 1 | Tici | tici@example.com | 
The createdBy field is no longer a name, but instead refers to the id of an
item in the User list (commonly referred to as data
normalization).
This gives us only one place to update email.
Now that we have two different lists, to get all the data now takes two queries:
query {
  allTodos {
    task
    createdBy
  }
}
# output:
# {
#   allTodos: [
#     { task: 'Use KeystoneJS', createdBy: 1 }
#     { task: 'Setup linter', createdBy: 1 }
#   ]
# }
We'd then have to iterate over each item and extract the createdBy id, to be
passed to a query such as:
query {
  User(where: { id: "1" }) {
    name
    email
  }
}
# output:
# {
#   User: { name: 'Tici', email: 'tici@example.com' }
# }
Which we'd have to execute once for every User that was referenced by a
Todo's createdBy field.
Using Relationships makes this a lot easier.
Defining Relationships
Relationships are defined using the Relationship field type, and require at
least 2 configured lists (one will refer to the other).
const { Relationship } = require('@keystonejs/fields');
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
    createdBy: { type: Relationship, ref: 'User' },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
  },
});
This is a to-single relationship from the Todo
list to an item in the User list.
To query the data, we can write a single query which returns both the Todos
and their related Users:
query {
  allTodos {
    task
    createdBy {
      name
      email
    }
  }
}
# output:
# {
#   allTodos: [
#     { task: 'Use KeystoneJS', createdBy: { name: 'Tici', email: 'tici@example.com' } }
#     { task: 'Setup linter', createdBy: { name: 'Tici', email: 'tici@example.com' } }
#   ]
# }
A note on definitions:
- To-single / To-many refer to the number of related items (1, or more than 1).
- One-way / Two-way refer to the direction of the query.
- Back References refer to a special type of two-way relationships where one field can update a related list's field as it changes.
To-single Relationships
When you have a single related item you want to refer to, a to-single relationship allows storing that item, and querying it via the GraphQL API.
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
    createdBy: { type: Relationship, ref: 'User' },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
  },
});
Here we've defined the createdBy field to be a Relationship type, and
configured its relation to be the User list by setting the ref option.
A query for a to-single relationship field will return an object with the requested data:
query {
  Todo(where: { id: "<todoId>" }) {
    createdBy {
      id
      name
    }
  }
}
# output:
# {
#   Todo: {
#     createdBy: { id: '1', name: 'Tici' }
#   }
# }
The data stored in the database for the createdBy field will be a single ID:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Use KeystoneJS | 1 | 
| 2 | Setup Linter | 1 | 
User| id | name | email | 
|---|---|---|
| 1 | Tici | tici@example.com | 
To-many Relationships
When you have multiple items you want to refer to from a single field, a to-many relationship will store an array, also exposing that array via the GraphQL API.
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
    todoList: { type: Relationship, ref: 'Todo', many: true },
  },
});
A query for a to-many relationship field will return an array of objects with the requested data:
query {
  User(where: { id: "<userId>" }) {
    todoList {
      task
    }
  }
}
# output:
# {
#   User: {
#     todoList: [
#       { task: 'Use KeystoneJS' },
#       { task: 'Setup linter' },
#     ]
#   ]
# }
The data stored in the database for the todoList field will be an array of
IDs:
Todo| id | task | 
|---|---|
| 1 | Use KeystoneJS | 
| 2 | Setup Linter | 
| 3 | Be Awesome | 
| 4 | Write docs | 
| 5 | Buy milk | 
User| id | name | email | todoList | 
|---|---|---|---|
| 1 | Tici | tici@example.com | [1, 2] | 
| 2 | Jess | jess@example.com | [3, 4, 5] | 
Two-way Relationships
In the to-single and to-many examples above, we were only querying in one direction; always from the list with the Relationship field.
Often, you will want to query in both directions (aka two-way). For example: you may want to list all Todo tasks for a User and want to list the User who owns a Todo.
A two-way relationship requires having a Relationship field on both lists:
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
    createdBy: { type: Relationship, ref: 'User' },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
    todoList { type: Relationship, ref: 'Todo', many: true },
  }
});
Here we have two relationships:
- A to-single createdByfield on theTodolist, and
- A to-many todoListfield on theUserlist.
Now it's possible to query in both directions:
query {
  User(where: { id: "<userId>" }) {
    todoList {
      task
    }
  }
  Todo(where: { id: "<todoId>" }) {
    createdBy {
      id
      name
    }
  }
}
# output:
# {
#   User: {
#     todoList: [
#       { task: 'Use KeystoneJS' },
#       { task: 'Setup linter' },
#     ]
#   ],
#   Todo: {
#     createdBy: { id: '1', name: 'Tici' }
#   }
# }
The database would look like:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Use KeystoneJS | 1 | 
| 2 | Setup Linter | 1 | 
| 3 | Be Awesome | 2 | 
| 4 | Write docs | 2 | 
| 5 | Buy milk | 2 | 
User| id | name | email | todoList | 
|---|---|---|---|
| 1 | Tici | tici@example.com | [1, 2] | 
| 2 | Jess | jess@example.com | [3, 4, 5] | 
Note the two relationship fields in this example know nothing about each other.
They are not specially linked. This means if you update data in one place, you
must update it in both. To automate this and link two relationship fields, read
on about Relationship Back References below.
Relationship Back References
There is a special type of two-way relationship where one field can update a related list's field as it changes. The mechanism enabling this is called Back References.
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
    createdBy: { type: Relationship, ref: 'User' },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
    todoList { type: Relationship, ref: 'Todo', many: true },
  }
});
In this example, when a new Todo item is created, we can set the createdBy
field as part of the mutation:
mutation {
  createTodo(data: {
    task: 'Learn Node',
    createdBy: { connect: { id: '1' } },
  }) {
    id
  }
}
See the Relationship API docs for more on connect.
If this was the first Todo item created, the database would now look like:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Learn Node | 1 | 
User| id | name | email | todoList | 
|---|---|---|---|
| 1 | Tici | tici@example.com | [] | 
Notice the Todo item's createdBy field is set, but the User item's
todoList does not contain the ID of the newly created Todo!
If we were to query the data now, we would get:
query {
  User(where: { id: "1" }) {
    todoList {
      id
      task
    }
  }
  Todo(where: { id: "1" }) {
    createdBy {
      id
      name
    }
  }
}
# output:
# {
#   User: {
#     todoList: []
#   ],
#   Todo: {
#     createdBy: { id: '1', name: 'Tici' }
#   }
# }
Back References solve this problem.
To setup a back reference, we need to specify both the list and the field in
the ref option:
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
    // The `ref` option now includes which field to update
    createdBy: { type: Relationship, ref: 'User.todoList' },
  },
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
    todoList: { type: Relationship, ref: 'Todo', many: true },
  },
});
This works for both to-single and to-many relationships.
Now, if we run the same mutation:
mutation {
  createTodo(data: {
    task: 'Learn Node',
    createdBy: { connect: { id: '1' } },
  }) {
    id
  }
}
Our database would look like:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Learn Node | 1 | 
User| id | name | email | todoList | 
|---|---|---|---|
| 1 | Tici | tici@example.com | [1] | 
query {
  User(where: { id: "1" }) {
    todoList {
      id
      task
    }
  }
  Todo(where: { id: "1" }) {
    createdBy {
      id
      name
    }
  }
}
# output:
# {
#   User: {
#     todoList: [{ id: '1', task: 'Learn Node' }]
#   ],
#   Todo: {
#     createdBy: { id: '1', name: 'Tici' }
#   }
# }
We can do the same modification for the User list, and reap the same rewards
for creating a new User:
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
    // The `ref` option now includes which field to update
    createdBy: { type: Relationship, ref: 'User.todoList' },
  }
});
keystone.createList('User', {
  fields: {
    name: { type: Text },
    email: { type: Text },
    todoList { type: Relationship, ref: 'Todo.createdBy', many: true },
  }
});
In this case, we'll create the first task along with creating the user. For
more info on the create syntax, see
the Relationship API docs.
mutation {
  createUser(data: {
    name: 'Tici',
    email: 'tici@example.com',
    todoList: { create: [{ task: 'Learn Node' }] },
  }) {
    id
  }
}
The data would finally look like:
Todo| id | task | createdBy | 
|---|---|---|
| 1 | Learn Node | 1 | 
User| id | name | email | todoList | 
|---|---|---|---|
| 1 | Tici | tici@example.com | [1] |