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
Action | Item | Before | After |
---|---|---|---|
🔁 Renamed | Generated query for a single item | Task() | task() |
🔁 Renamed | Generated query for multiple items | allTasks() | tasks() |
🔁 Renamed | Pagination argument to align with arguments provided by Prisma | first | take |
❌ Removed | Legacy search argument | search | where |
❌ Removed | Deprecated sortBy argument | sortBy | orderBy |
❌ Removed | Deprecated _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
anddelete
mutations no longer acceptid
orids
to indicate which items to update. We now usewhere
so you can select the item based on any of its unique fields.- The types used for
create
andupdate
mutations have been updated. - All inputs are now non-optional.
Create mutation
Before | After |
---|---|
createTask(data: TaskCreateInput): Task | createTask(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
Before | After |
---|---|
updateTask(id: ID!, data: TaskUpdateInput): Task | updateTask(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
Before | After |
---|---|
deleteTask(id: ID!): Task | deleteTask(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
andupdate
operations. - Inputs for
create
operations no longer support thedisconnect
ordisconnectAll
options. These options didn't do anything during acreate
operation in the previous API. - For to-one relationships, the
disconnect
option is now aBoolean
, 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 newset
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.
- Update top level queries. Be sure to rename
Task
totask
andallTasks
totasks
for all your queries. - Update filters. Find and replace all the old Keystone filters with their new equivalent.
- Update mutation arguments to match the new input types. Make sure you replace
{ id: "..."}
with{where: { id: "..."} }
in yourupdate
anddelete
operations. - Update relationship inputs to
create
andupdate
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.