Skip to Page NavigationSkip to Page NavigationSkip to Content
Keystone 6 is now in General Availability!

A new & improved GraphQL API

As we move closer to a General Availability release for Keystone 6, we've taken the opportunity to make the experience of working with Keystone’s GraphQL API easier to program and reason about.

This guide describes the improvements we've made, and walks you through the steps you need to take to upgrade your Keystone projects.

If you get stuck, or want to discuss these changes, reach out to us in the Keystone community slack.

Example Schema

To illustrate the changes, we’ll refer to the Task list in the following schema, from our Task Manager example project.

export const lists = {
Task: list({
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
}),
isComplete: checkbox(),
assignedTo: relationship({ ref: 'Person.tasks', many: false }),
tags: relationship({ ref: 'Tag', many: true }),
finishBy: timestamp(),
},
}),
Person: list({
fields: {
name: text({ validation: { isRequired: true } }),
tasks: relationship({ ref: 'Task.assignedTo', many: true }),
},
}),
Tag: list({
fields: {
name: text(),
},
}),
};

Query

We’ve changed the names of our top-level queries so they’re easier to understand. We also took this opportunity to remove deprecated and unused legacy features.

Changes

ActionItemBeforeAfter
🔁   RenamedGenerated query for a single itemTask()task()
🔁   RenamedGenerated query for multiple itemsallTasks()tasks()
🔁   RenamedPagination argument to align with arguments provided by Prismafirsttake
  RemovedLegacy search argumentsearchwhere
  RemovedDeprecated sortBy argumentsortByorderBy
  RemovedDeprecated _allTasksMeta query_allTasksMeta()tasksCount()

We’ve also changed the format of filters used in TaskWhereInput. See Filter changes for more details.

Example

// Before
type Query {
allTasks(
where: TaskWhereInput! = {}
search: String
sortBy: [SortTasksBy!]
@deprecated(reason: "sortBy has been deprecated in favour of orderBy")
orderBy: [TaskOrderByInput!]! = []
first: Int
skip: Int! = 0
): [Task!]
Task(where: TaskWhereUniqueInput!): Task
_allTasksMeta(
where: TaskWhereInput! = {}
search: String
sortBy: [SortTasksBy!]
@deprecated(reason: "sortBy has been deprecated in favour of orderBy")
orderBy: [TaskOrderByInput!]! = []
first: Int
skip: Int! = 0
): _QueryMeta
@deprecated(
reason: "This query will be removed in a future version. Please use tasksCount instead."
)
tasksCount(where: TaskWhereInput! = {}): Int
...
}
// After
type Query {
tasks(
where: TaskWhereInput! = {}
orderBy: [TaskOrderByInput!]! = []
take: Int
skip: Int! = 0
): [Task!]
task(where: TaskWhereUniqueInput!): Task
tasksCount(where: TaskWhereInput! = {}): Int
...
}

Filters

The filter arguments used in queries have been updated to accept a filter object for each field, rather than having all the filter options available at the top level.

An example of a query in the old format is:

allTasks(
where: {
label_starts_with: "Hello",
finishBy_lt: "2022-01-01T00:00:00.000Z",
isComplete: true
}
) { id }

Using the new filter syntax, this becomes:

tasks(
where: {
label: { startsWith: "Hello" }
finishBy: { lt: "2022-01-01T00:00:00.000Z" }
isComplete: { equals: true }
}
) { id }

There is a one-to-one correspondence between the old filters and the new filters. No filter functionality has been removed or added, however the individual filters are now in a nested object, and the names have changed from snake_case to camelCase.

Note: The old filter syntax used { fieldName: value } to test for equality. The new syntax requires you to make this explicit, and write { fieldName: { equals: value} }.

See the Filters Guide for a detailed walk through the new filtering syntax.

See the API docs for a comprehensive list of all the new filters for each field type.

Mutations

All generated CRUD mutations have the same names and return types, but their inputs have changed.

  • update and delete mutations no longer accept id or ids to indicate which items to update. We now use where so you can select the item based on any of its unique fields.
  • The types used for create and update mutations have been updated.
  • All inputs are now non-optional.

Create mutation

BeforeAfter
createTask(data: TaskCreateInput): TaskcreateTask(data: TaskCreateInput!): Task
createTasks(data: [TasksCreateInput]): [Task]createTasks(data: [TaskCreateInput!]!): [Task]
// Before
mutation {
createTask(data: { label: "Upgrade keystone" }) {
id
}
}
mutation {
createTasks(
data: [
{ data: { label: "Upgrade keystone" } }
{ data: { label: "Build great products" } }
]
) {
id
}
}
// After
mutation {
createTask(data: { label: "Upgrade keystone" }) {
id
}
}
mutation {
createTasks(
data: [
{ label: "Upgrade keystone" },
{ label: "Build great products" }
]
) {
id
}
}

Update mutation

BeforeAfter
updateTask(id: ID!, data: TaskUpdateInput): TaskupdateTask(where: TaskWhereUniqueInput!, data: TaskUpdateInput!): Task
updateTasks(data: [TasksUpdateInput]): [Task]updateTasks(data: [TaskUpdateArgs!]!): [Task]
// Before
mutation {
updateTask(id: "cksdyag9w0000pioj44kinqsp", data: { isComplete: true }) {
id
}
updateTasks(
data: [
{ id: "cksdyaga50007pioj1oc37msr", data: { isComplete: true } }
{ id: "cksdyj6wd0000epoj0585uzbq", data: { isComplete: true } }
]
) {
id
}
}
// After
mutation {
updateTask(
where: { id: "cksdyag9w0000pioj44kinqsp" }
data: { isComplete: true }
) {
id
}
updateTasks(
data: [
{ where: { id: "cksdyaga50007pioj1oc37msr" }, data: { isComplete: true } }
{ where: { id: "cksdyj6wd0000epoj0585uzbq" }, data: { isComplete: true } }
]
) {
id
}
}

Delete mutation

BeforeAfter
deleteTask(id: ID!): TaskdeleteTask(where: TaskWhereUniqueInput!): Task
deleteTasks(ids: [ID!]): [Task]deleteTasks(where: [TaskWhereUniqueInput!]!): [Task]
// Before
mutation {
deleteTask(id: "cksdyaga50007pioj1oc37msr") {
id
}
deleteTasks(ids: ["cksdyjrbj0007epojilbv3d6k", "cksdyjrbp0014epoja2uddwl1"]) {
id
}
}
// After
mutation {
deleteTask(where: { id: "cksdyag9w0000pioj44kinqsp" }) {
id
}
deleteTasks(
where: [
{ id: "ckrlp28lf001908lu9tyzxhuq" }
{ id: "ckroflp7h0019t9lulhw6pggp" }
]
) {
id
}
}

Input Types

We’ve updated the input types used for relationship fields in update and create operations, removing obsolete options and making the syntax between the two operations easier to differentiate.

  • There are now separate types for create and update operations.
  • Inputs for create operations no longer support the disconnect or disconnectAll options. These options didn't do anything during a create operation in the previous API.
  • For to-one relationships, the disconnect option is now a Boolean, rather than accepting a unique input. If you only have one related item, there's no need to specify its value when disconnecting it.
  • For to-many relationships, the disconnectAll operation has been removed in favour of a new set operation, which allows you to explicitly set the connected items. You can use { set: [] } to achieve the same results as the old { disconnectAll: true }.

Example

// Before
input TasksUpdateInput {
id: ID!
data: TaskUpdateInput
}
input TaskUpdateInput {
label: String
priority: TaskPriorityType
isComplete: Boolean
assignedTo: PersonRelateToOneInput
tags: TagRelateToManyInput
finishBy: String
}
input TasksCreateInput {
data: TaskCreateInput
}
input TaskCreateInput {
label: String
priority: TaskPriorityType
isComplete: Boolean
assignedTo: PersonRelateToOneInput
tags: TagRelateToManyInput
finishBy: String
}
input PersonRelateToOneInput {
create: PersonCreateInput
connect: PersonWhereUniqueInput
disconnect: PersonWhereUniqueInput
disconnectAll: Boolean
}
input TagRelateToManyInput {
create: [TagCreateInput]
connect: [TagWhereUniqueInput]
disconnect: [TagWhereUniqueInput]
disconnectAll: Boolean
}
// After
input TaskUpdateArgs {
where: TaskWhereUniqueInput!
data: TaskUpdateInput!
}
input TaskUpdateInput {
label: String
priority: TaskPriorityType
isComplete: Boolean
assignedTo: PersonRelateToOneForUpdateInput
tags: TagRelateToManyForUpdateInput
finishBy: String
}
input TaskCreateInput {
label: String
priority: TaskPriorityType
isComplete: Boolean
assignedTo: PersonRelateToOneForCreateInput
tags: TagRelateToManyForCreateInput
finishBy: String
}
input PersonRelateToOneForUpdateInput {
create: PersonCreateInput
connect: PersonWhereUniqueInput
disconnect: Boolean
}
input PersonRelateToOneForCreateInput {
create: PersonCreateInput
connect: PersonWhereUniqueInput
}
input TagRelateToManyForUpdateInput {
disconnect: [TagWhereUniqueInput!]
set: [TagWhereUniqueInput!]
create: [TagCreateInput!]
connect: [TagWhereUniqueInput!]
}
input TagRelateToManyForCreateInput {
create: [TagCreateInput!]
connect: [TagWhereUniqueInput!]
}

Upgrade Checklist

While there are a lot of changes to this API, we've put a lot of effort into making the upgrade process as smooth as possible. If you get stuck or have questions, reach out to us in the Keystone community slack to get the help you need.

Before you begin: check that your project doesn't rely on any of the features we've marked as deprecated in this document, or the search argument to filters. If you do, apply the recommended substitute.

  1. Update top level queries. Be sure to rename Task to task and allTasks to tasks for all your queries.
  2. Update filters. Find and replace all the old Keystone filters with their new equivalent.
  3. Update mutation arguments to match the new input types. Make sure you replace { id: "..."} with {where: { id: "..."} } in your update and delete operations.
  4. Update relationship inputs to create and update operations. Ensure you've replaced usage of { disconnectAll: true } with { set: [] } in to-many relationships, and have used { disconnect: true } rather than { disconnect: { id: "..."} } in to-one relationships.

Finally, make sure you apply corresponding changes to filters and input arguments when using the Query API.


That's everything! While we acknowledge that API changes are an inconvenience, we believe the time spent navigating these upgrades will be offset many times over by a more fun and productive developer experience going forward.