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
// Beforetype Query {allTasks(where: TaskWhereInput! = {}search: StringsortBy: [SortTasksBy!]@deprecated(reason: "sortBy has been deprecated in favour of orderBy")orderBy: [TaskOrderByInput!]! = []first: Intskip: Int! = 0): [Task!]Task(where: TaskWhereUniqueInput!): Task_allTasksMeta(where: TaskWhereInput! = {}search: StringsortBy: [SortTasksBy!]@deprecated(reason: "sortBy has been deprecated in favour of orderBy")orderBy: [TaskOrderByInput!]! = []first: Intskip: Int! = 0): _QueryMeta@deprecated(reason: "This query will be removed in a future version. Please use tasksCount instead.")tasksCount(where: TaskWhereInput! = {}): Int...}// Aftertype Query {tasks(where: TaskWhereInput! = {}orderBy: [TaskOrderByInput!]! = []take: Intskip: Int! = 0): [Task!]task(where: TaskWhereUniqueInput!): TasktasksCount(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] |
// Beforemutation {createTask(data: { label: "Upgrade keystone" }) {id}}mutation {createTasks(data: [{ data: { label: "Upgrade keystone" } }{ data: { label: "Build great products" } }]) {id}}// Aftermutation {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] |
// Beforemutation {updateTask(id: "cksdyag9w0000pioj44kinqsp", data: { isComplete: true }) {id}updateTasks(data: [{ id: "cksdyaga50007pioj1oc37msr", data: { isComplete: true } }{ id: "cksdyj6wd0000epoj0585uzbq", data: { isComplete: true } }]) {id}}// Aftermutation {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] |
// Beforemutation {deleteTask(id: "cksdyaga50007pioj1oc37msr") {id}deleteTasks(ids: ["cksdyjrbj0007epojilbv3d6k", "cksdyjrbp0014epoja2uddwl1"]) {id}}// Aftermutation {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
// Beforeinput TasksUpdateInput {id: ID!data: TaskUpdateInput}input TaskUpdateInput {label: Stringpriority: TaskPriorityTypeisComplete: BooleanassignedTo: PersonRelateToOneInputtags: TagRelateToManyInputfinishBy: String}input TasksCreateInput {data: TaskCreateInput}input TaskCreateInput {label: Stringpriority: TaskPriorityTypeisComplete: BooleanassignedTo: PersonRelateToOneInputtags: TagRelateToManyInputfinishBy: String}input PersonRelateToOneInput {create: PersonCreateInputconnect: PersonWhereUniqueInputdisconnect: PersonWhereUniqueInputdisconnectAll: Boolean}input TagRelateToManyInput {create: [TagCreateInput]connect: [TagWhereUniqueInput]disconnect: [TagWhereUniqueInput]disconnectAll: Boolean}// Afterinput TaskUpdateArgs {where: TaskWhereUniqueInput!data: TaskUpdateInput!}input TaskUpdateInput {label: Stringpriority: TaskPriorityTypeisComplete: BooleanassignedTo: PersonRelateToOneForUpdateInputtags: TagRelateToManyForUpdateInputfinishBy: String}input TaskCreateInput {label: Stringpriority: TaskPriorityTypeisComplete: BooleanassignedTo: PersonRelateToOneForCreateInputtags: TagRelateToManyForCreateInputfinishBy: String}input PersonRelateToOneForUpdateInput {create: PersonCreateInputconnect: PersonWhereUniqueInputdisconnect: Boolean}input PersonRelateToOneForCreateInput {create: PersonCreateInputconnect: 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.