Skip to content
KeystoneJS LogoKeystoneJS (α)

Schema - Lists & Fields

KeystoneJS is comprised of 3 distinct pieces, which can be described as:

Schema => ({ AdminUI, GraphQL })

A Schema Definition (often abbreviated to 'Schema') is defined by

  • a set of Lists
  • containing one or more Fields
  • which each have a Type.
Minimal Schema Example
keystone.createList('Todo', {
  fields: {
    task: { type: Text },
  },
});
Create a List called Todo, containing a single Field task, with a Type of Text

Lists

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.

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
idtaskcreatedBy
1Use KeystoneTici

We could query this data like so:

query {
  allTodos {
    task
    createdBy
  }
}

# output:
# {
#   allTodos: [
#     { task: 'Use Keystone', createdBy: 'Tici' }
#   ]
# }

Everything looks great so far. Now, let's add another task:

Todo
idtaskcreatedBy
1Use KeystoneTici
2Setup linterTici
query {
  allTodos {
    task
    createdBy
  }
}

# output:
# {
#   allTodos: [
#     { task: 'Use Keystone', 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
idtaskcreatedByemail
1Use KeystoneTicitici@example.com
2Setup LinterTicitici@example.com
query {
  allTodos {
    task
    createdBy
    email
  }
}

# output:
# {
#   allTodos: [
#     { task: 'Use Keystone', 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
idtaskcreatedBy
1Use Keystone1
2Setup Linter1
User
idnameemail
1Ticitici@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 Keystone', 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('@keystone-alpha/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 Keystone', 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.
  • Backlinks 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
idtaskcreatedBy
1Use Keystone1
2Setup Linter1
User
idnameemail
1Ticitici@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 viat 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 Keystone' },
#       { task: 'Setup linter' },
#     ]
#   ]
# }

The data stored in the database for the todoList field will be an array of IDs:

Todo
idtask
1Use Keystone
2Setup Linter
3Be Awesome
4Write docs
5Buy milk
User
idnameemailtodolist
1Ticitici@example.com[1, 2]
2Jessjess@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 createdBy field on the Todo list, and
  • A to-many todolist field on the User list.

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 Keystone' },
#       { task: 'Setup linter' },
#     ]
#   ],
#   Todo: {
#     createdBy: { id: '1', name: 'Tici' }
#   }
# }

The database would look like:

Todo
idtaskcreatedBy
1Use Keystone1
2Setup Linter1
3Be Awesome2
4Write docs2
5Buy milk2
User
idnameemailtodolist
1Ticitici@example.com[1, 2]
2Jessjess@example.com[3, 4, 5]

Note the two Relationship fields in this example know nothing about eachother. 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.

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 exaple, 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
idtaskcreatedBy
1Learn Node1
User
idnameemailtodolist
1Ticitici@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 like:

Todo
idtaskcreatedBy
1Learn Node1
User
idnameemailtodolist
1Ticitici@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
idtaskcreatedBy
1Learn Node1
User
idnameemailtodolist
1Ticitici@example.com[1]

Have you found a mistake, something that is missing, or could be improved on this page? Please edit the Markdown file on GitHub and submit a PR with your changes.

Edit Page