Skip to content

Latest commit

 

History

History
781 lines (619 loc) · 16.2 KB

schema.md

File metadata and controls

781 lines (619 loc) · 16.2 KB

Data modelling

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 },
  },
});

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.

Keystone 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 the generated types/mutations/queries are shown here. For more details, see the GraphQL introduction guide.

Customising lists and 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 Keystone' most powerful features is defining Relationships between Lists.

Relationships are a special field type in Keystone 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:

id task createdBy
1 Use Keystone Tici

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
id task createdBy
1 Use Keystone Tici
2 Setup linter Tici
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
id task createdBy email
1 Use Keystone Tici [email protected]
2 Setup Linter Tici [email protected]
query {
  allTodos {
    task
    createdBy
    email
  }
}

# output:
# {
#   allTodos: [
#     { task: 'Use Keystone', createdBy: 'Tici', email: '[email protected]' }
#     { task: 'Setup linter', createdBy: 'Tici', email: '[email protected]' }
#   ]
# }

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 Keystone 1
2 Setup Linter 1
User
id name email
1 Tici [email protected]

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: '[email protected]' }
# }

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 Keystone', createdBy: { name: 'Tici', email: '[email protected]' } }
#     { task: 'Setup linter', createdBy: { name: 'Tici', email: '[email protected]' } }
#   ]
# }

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 Keystone 1
2 Setup Linter 1
User
id name email
1 Tici [email protected]

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 Keystone' },
#       { task: 'Setup linter' },
#     ]
#   ]
# }

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

Todo
id task
1 Use Keystone
2 Setup Linter
3 Be Awesome
4 Write docs
5 Buy milk
User
id name email todoList
1 Tici [email protected] [1, 2]
2 Jess [email protected] [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
id task createdBy
1 Use Keystone 1
2 Setup Linter 1
3 Be Awesome 2
4 Write docs 2
5 Buy milk 2
User
id name email todoList
1 Tici [email protected] [1, 2]
2 Jess [email protected] [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 [email protected] []

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 [email protected] [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 [email protected] [1]