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 have any questions, please don't hesitate to open a GitHub discussion.
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.
updateanddeletemutations no longer acceptidoridsto indicate which items to update. We now usewhereso you can select the item based on any of its unique fields.- The types used for
createandupdatemutations 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
createandupdateoperations. - Inputs for
createoperations no longer support thedisconnectordisconnectAlloptions. These options didn't do anything during acreateoperation in the previous API. - For to-one relationships, the
disconnectoption 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
disconnectAlloperation has been removed in favour of a newsetoperation, 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 have any questions, please don't hesitate to open a GitHub discussion.
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
TasktotaskandallTaskstotasksfor 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 yourupdateanddeleteoperations. - Update relationship inputs to
createandupdateoperations. 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.